---
title: "REST API reference — schedule posts over HTTP | Ravenpost"
description: "Every Ravenpost /v1 endpoint: create and schedule posts across nine networks, upload media, read analytics and per-platform limits. Bearer token, 120 req/min, free on every plan."
url: https://ravenpo.st/docs/api/
---

Developers

# REST API reference

19 endpoints under `https://api.ravenpo.st/v1`. Every one delegates to the same service the dashboard calls, so there is exactly one implementation of “create a post” and no second set of rules. Authentication is a bearer token; no route takes a workspace id, because the token carries one.

[Overview](https://ravenpo.st/docs/)[REST API](https://ravenpo.st/docs/api/)[MCP server](https://ravenpo.st/docs/mcp/)

## Conventions

-   Send `Authorization: Bearer rvp_…` on every request, and `Content-Type: application/json` on anything with a body.
-   Ids are opaque strings. Timestamps are ISO 8601 in UTC; queue slots and send-time recommendations are in the workspace timezone, with weekday 0 = Sunday.
-   Rate limit: 120 requests/minute per IP unless an endpoint says otherwise.
-   `*` marks a required field.

```
curl https://api.ravenpo.st/v1/me -H "Authorization: Bearer rvp_…"
```

## Workspace

GET`/v1/me`

The token’s workspace, its plan limits, current usage and account counts.

The cheapest call to verify a token works. No endpoint takes a workspace id — the token carries exactly one, so a token can never reach another tenant’s data whatever the caller sends.

Response

```
{
  "workspaceId": "wsp_...",
  "plan": { "id": "growth", "name": "Growth",
            "limits": { "socialAccounts": 15, "seats": 3, "monthlyPosts": -1 } },
  "usage": { "socialAccounts": 6, "postsThisMonth": 84 },
  "stats": { "accounts": 6, "scheduled": 12 }
}
```

## Accounts

GET`/v1/accounts`

Connected social accounts — id, platform, username, status.

Access tokens are never returned by any endpoint.

Response

```
[
  { "id": "acc_...", "platform": "INSTAGRAM", "username": "ravenpost",
    "status": "ACTIVE", "connection": "FACEBOOK_LOGIN" }
]
```

## Posts

GET`/v1/posts`

List posts, newest first.

Without \`page\` this returns a plain array, matching the dashboard’s contract; with \`page\` it returns a paginated envelope. Automations should always pass \`page\` — a workspace with thousands of posts otherwise returns all of them in one response.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `status` | query | `DRAFT \| SCHEDULED \| PUBLISHING \| PUBLISHED \| FAILED` | Filter by status. |
| `platform` | query | `string` | Only posts targeting this platform. |
| `type` | query | `string` | Filter by post format. |
| `q` | query | `string` | Caption search. |
| `sort` | query | `string` | Sort order. |
| `page` | query | `number` | Page number — switches the response to an envelope. |
| `limit` | query | `number` | Page size. |

POST`/v1/posts`

Create a post and publish, schedule, queue or save it as a draft.

One post, many destinations: each account in \`accountIds\` becomes a target with its own status, permalink and error. Caption length is validated against the tightest selected platform.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `type`\* | body | `'IMAGE' \| 'CAROUSEL' \| 'REEL' \| 'STORY'` | Post format. Instagram's model, reused as the shared one — a platform that doesn't honour the format falls back to its own default rather than failing. |
| `accountIds`\* | body | `string[]` | Destination accounts, from GET /v1/accounts. One post fans out to all of them. |
| `mediaIds`\* | body | `string[]` | Media assets to attach. Pass \[\] for a text-only post where the platform allows it. |
| `action`\* | body | `'draft' \| 'now' \| 'schedule' \| 'queue'` | What to do with it. 'schedule' needs scheduledAt; 'queue' resolves the next free slot in the workspace posting schedule and fails if none is configured. |
| `caption` | body | `string` | Base caption, used by every platform without an override. |
| `captionOverrides` | body | `Record<Platform, string>` | Per-platform caption. A platform with no entry publishes the base caption. Keys are platform ids from GET /v1/platforms. |
| `scheduledAt` | body | `string (ISO 8601)` | When to publish, for action: 'schedule'. |
| `threadParts` | body | `string[]` | Follow-up posts published in order after the head, on platforms that declare a thread model (X, Bluesky, Mastodon, Threads, Telegram, Discord). Over the platform limit is rejected, never truncated. |
| `firstComment` | body | `string` | Posted as the first comment after publishing. Instagram only. |
| `replyToTweetId` | body | `string` | Publish the head post as a reply under this tweet. Accepts an id or a tweet URL. X only. |
| `quoteTweetId` | body | `string` | Embed this tweet as a quote card. Id or URL. X only, and independent of replyToTweetId. |
| `tiktokOptions` | body | `Record<accountId, object>` | TikTok privacy level, interaction switches and commercial disclosure, keyed by account id. Omitted, a TikTok target publishes SELF\_ONLY — TikTok requires the creator to choose, so we never pick a wider audience for them. |
| `audio` | body | `object` | Reel audio ({ audioId, audioVolume, videoVolume }). Instagram reels only, and only on an account connected through Facebook. |
| `recurrence` | body | `object` | Makes this an evergreen post: each occurrence is created after the previous one publishes. |

Request

```
{
  "type": "IMAGE",
  "caption": "Shipping today.",
  "captionOverrides": { "TWITTER": "Shipping today 🚀" },
  "mediaIds": ["med_..."],
  "accountIds": ["acc_...", "acc_..."],
  "action": "schedule",
  "scheduledAt": "2026-08-04T09:00:00.000Z"
}
```

POST`/v1/posts/bulk`10/min

Create many posts from a CSV.

Returns a per-line report so a caller can retry only the rows that failed, rather than guessing which half of the batch landed.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `csv`\* | body | `string` | The CSV document itself, as a string. |

GET`/v1/posts/:id`

One post with every target’s status, permalink and error.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |

PATCH`/v1/posts/:id`

Edit a draft or scheduled post.

Editing resets the post to draft — pass \`action\` in the same call to re-schedule or publish it. A published post cannot be edited.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |
| `type` | body | `'IMAGE' \| 'CAROUSEL' \| 'REEL' \| 'STORY'` | Post format. Instagram's model, reused as the shared one — a platform that doesn't honour the format falls back to its own default rather than failing. |
| `accountIds` | body | `string[]` | Destination accounts, from GET /v1/accounts. One post fans out to all of them. |
| `mediaIds` | body | `string[]` | Media assets to attach. Pass \[\] for a text-only post where the platform allows it. |
| `action` | body | `'draft' \| 'now' \| 'schedule' \| 'queue'` | What to do with it. 'schedule' needs scheduledAt; 'queue' resolves the next free slot in the workspace posting schedule and fails if none is configured. |
| `caption` | body | `string` | Base caption, used by every platform without an override. |
| `captionOverrides` | body | `Record<Platform, string>` | Per-platform caption. A platform with no entry publishes the base caption. Keys are platform ids from GET /v1/platforms. |
| `scheduledAt` | body | `string (ISO 8601)` | When to publish, for action: 'schedule'. |
| `threadParts` | body | `string[]` | Follow-up posts published in order after the head, on platforms that declare a thread model (X, Bluesky, Mastodon, Threads, Telegram, Discord). Over the platform limit is rejected, never truncated. |
| `firstComment` | body | `string` | Posted as the first comment after publishing. Instagram only. |
| `replyToTweetId` | body | `string` | Publish the head post as a reply under this tweet. Accepts an id or a tweet URL. X only. |
| `quoteTweetId` | body | `string` | Embed this tweet as a quote card. Id or URL. X only, and independent of replyToTweetId. |
| `tiktokOptions` | body | `Record<accountId, object>` | TikTok privacy level, interaction switches and commercial disclosure, keyed by account id. Omitted, a TikTok target publishes SELF\_ONLY — TikTok requires the creator to choose, so we never pick a wider audience for them. |
| `audio` | body | `object` | Reel audio ({ audioId, audioVolume, videoVolume }). Instagram reels only, and only on an account connected through Facebook. |
| `recurrence` | body | `object` | Makes this an evergreen post: each occurrence is created after the previous one publishes. |

DELETE`/v1/posts/:id`

Delete the Ravenpost copy and cancel any pending publish.

This does not take down anything already published. Removing the post on the platform is a separate, opt-in action in the dashboard, and three platforms (Instagram, Threads, TikTok) expose no delete endpoint at all.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |

POST`/v1/posts/:id/schedule`

Move a post to a new time.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |
| `scheduledAt`\* | body | `string (ISO 8601)` | New publish time. |

POST`/v1/posts/:id/publish`

Publish a post immediately, whatever it was scheduled for.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |

POST`/v1/posts/:id/retry`

Re-enqueue only the failed targets.

Targets that already published are left alone, so a retry can’t double-post.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Post id. |

## Media

GET`/v1/media`

The workspace media library.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `folderId` | query | `string` | Limit to one folder. |

POST`/v1/media/uploads`60/min

Step 1 of an upload: get a presigned PUT URL.

The client sends the bytes straight to object storage, so a large video never travels through the API — and never has to be base64-encoded into a request.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `filename`\* | body | `string` | Original filename, used for a readable storage key. |
| `contentType`\* | body | `string` | MIME type of the bytes you are about to PUT. |

Response

```
{ "uploadUrl": "https://...", "storageKey": "...", "url": "https://cdn..." }
```

POST`/v1/media`

Step 2 of an upload: register the uploaded object as an asset.

Returns the asset \`id\` to put in \`mediaIds\`.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `type`\* | body | `'IMAGE' \| 'VIDEO'` | Asset kind. |
| `storageKey`\* | body | `string` | From the presign response. |
| `url`\* | body | `string` | Public URL, from the presign response. |
| `mimeType` | body | `string` | Recorded for the per-platform compatibility check. |
| `width` | body | `number` | Pixels. |
| `height` | body | `number` | Pixels. |
| `durationSec` | body | `number` | Video length. |
| `sizeBytes` | body | `number` | File size, checked against each platform’s cap. |

POST`/v1/media/:id/variants`30/min

Reshape one image into several platform canvases in a single call.

Cropping is saliency-based by default — an off-centre subject survives a 4:5 → 9:16 cut, where a centre crop would keep the wall behind them. Pass \`crop: "centre"\` for posters, screenshots and flat graphics composed around their middle. Two formats with the same dimensions produce one file, not two, and each variant inherits the source’s alt text and folder. Images only.

| Field | In  | Type | Description |
| --- | --- | --- | --- |
| `id`\* | path | `string` | Source image asset id. |
| `formats`\* | body | `('feed_portrait' \| 'feed_square' \| 'landscape' \| 'story')[]` | Canvases to produce: 1080×1350 (4:5), 1080×1080 (1:1), 1080×566 (1.91:1), 1080×1920 (9:16 — also reels and TikTok photos). |
| `fit` | body | `'cover' \| 'contain'` | 'cover' crops to fill (default); 'contain' pads so nothing is cut off. |
| `crop` | body | `'smart' \| 'centre'` | 'smart' is saliency-based and the default; 'centre' is the predictable middle crop. |

Request

```
{
  "formats": ["feed_portrait", "story", "landscape"],
  "crop": "smart"
}
```

Response

```
{
  "sourceId": "med_b2",
  "variants": [
    { "id": "med_c1", "url": "https://cdn...", "format": "feed_portrait", "width": 1080, "height": 1350 },
    { "id": "med_c2", "url": "https://cdn...", "format": "story", "width": 1080, "height": 1920 }
  ]
}
```

## Analytics

GET`/v1/analytics`

Followers per account, a 30-day daily series, and engagement on recent posts.

A \`followers\` of \`null\` means the platform reports none — LinkedIn, Threads and Discord expose no insights and TikTok’s need a scope that is not approved yet. Null means unknown, never zero; an absent post metric was not collected rather than being 0.

GET`/v1/best-times`

Recommended posting hours, measured from this workspace’s own posts.

Overall and per platform, in the workspace timezone. \`score\` is a multiple of a typical post for that account, so 1.8 means “roughly 80% better than normal”. When \`insufficient\` is true there is not enough history and \`slots\` is empty — that is “unknown”, not “no good time”; \`observations\` and \`minObservations\` say how far off it is.

Response

```
{
  "overall": {
    "timezone": "Europe/Istanbul",
    "observations": 46,
    "insufficient": false,
    "minObservations": 12,
    "slots": [
      { "weekday": 2, "minutes": 1140, "score": 1.84, "samples": 5 }
    ],
    "grid": [[null, null, 0.9, "..."]]
  },
  "byPlatform": [{ "platform": "INSTAGRAM", "report": { "...": "..." } }]
}
```

## Reference

GET`/v1/queue-slots`

The weekly posting schedule that \`action: "queue"\` publishes into.

Workspace-wide and shared by every connected account, in the workspace timezone. weekday 0 = Sunday.

GET`/v1/platforms`

Per-platform caption budgets, media rules, formats, thread caps and recommended specs.

Read this instead of hardcoding limits: they change when a platform changes, and this endpoint is generated from the same registry the publisher enforces.

## OpenAPI

The same surface as a machine-readable document, generated from the source this page renders: [/docs/openapi.json](https://ravenpo.st/docs/openapi.json). Request bodies are documented by example rather than a strict schema — the examples are real, and a schema we hadn’t verified field by field would be a more confident kind of wrong.
