# Posts

Create a post for one or many accounts, customise it per account, and read back per-platform results.


A post is one request that publishes to up to 20 accounts at once. You create it with `POST /v1/posts` and read its results with `GET /v1/posts/{id}`.

## Create a post

```bash tab="cURL"
curl -X POST https://social-publish-api.onrender.com/v1/posts \
  -H "x-access-key: $RELAYCAST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d0c3f0e-launch-post" \
  -d '{
    "content": "We just shipped dark mode 🌙",
    "targets": [
      { "accountId": "8f1c2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b" },
      {
        "accountId": "2b7d9e1f-3a4c-4d5e-8f6a-7b8c9d0e1f2a",
        "content": "Dark mode is here. Read the full changelog on our blog."
      }
    ],
    "media": [{ "url": "https://example.com/dark-mode.png", "type": "image", "altText": "App in dark mode" }],
    "publishNow": true
  }'
```

```ts tab="Node.js"
const res = await fetch("https://social-publish-api.onrender.com/v1/posts", {
  method: "POST",
  headers: {
    "x-access-key": process.env.RELAYCAST_API_KEY!,
    "Content-Type": "application/json",
    "Idempotency-Key": "7d0c3f0e-launch-post",
  },
  body: JSON.stringify({
    content: "We just shipped dark mode 🌙",
    targets: [
      { accountId: "8f1c2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b" },
      {
        accountId: "2b7d9e1f-3a4c-4d5e-8f6a-7b8c9d0e1f2a",
        content: "Dark mode is here. Read the full changelog on our blog.",
      },
    ],
    media: [{ url: "https://example.com/dark-mode.png", type: "image", altText: "App in dark mode" }],
    publishNow: true,
  }),
});
if (!res.ok) throw new Error(JSON.stringify(await res.json()));
const { data: post } = await res.json();
```

```python tab="Python"
res = requests.post(
    "https://social-publish-api.onrender.com/v1/posts",
    headers={
        "x-access-key": os.environ["RELAYCAST_API_KEY"],
        "Idempotency-Key": "7d0c3f0e-launch-post",
    },
    json={
        "content": "We just shipped dark mode 🌙",
        "targets": [
            {"accountId": "8f1c2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b"},
            {
                "accountId": "2b7d9e1f-3a4c-4d5e-8f6a-7b8c9d0e1f2a",
                "content": "Dark mode is here. Read the full changelog on our blog.",
            },
        ],
        "media": [{"url": "https://example.com/dark-mode.png", "type": "image", "altText": "App in dark mode"}],
        "publishNow": True,
    },
)
res.raise_for_status()
post = res.json()["data"]
```

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content` | string | Yes | Shared text, 1–10,000 characters. Each platform also applies its own limit. |
| `targets` | array | Yes | 1–20 accounts to publish to. |
| `targets[].accountId` | UUID | Yes | An account ID from `GET /v1/accounts`. |
| `targets[].content` | string | No | Replaces `content` for this account only (1–10,000 characters). |
| `targets[].options` | object | No | Platform-specific settings. See [Platform Options](/docs/platform-options). |
| `media` | array | No | Up to 10 items, shared by every target. See [Media & Uploads](/docs/media). |
| `media[].url` | string | Yes | Public `http(s)` URL or `media://<id>` library reference. |
| `media[].type` | string | Yes | `image`, `video` or `document` (PDF, LinkedIn only). |
| `media[].altText` | string | No | Alt text, up to 1,000 characters, used where the platform supports it. |
| `media[].title` | string | No | Document title (documents only), up to 400 characters. |
| `publishNow` | boolean | One of | `true` publishes immediately. |
| `scheduledFor` | string | One of | ISO 8601 time with offset, e.g. `2026-10-12T09:00:00Z`. See [Scheduling](/docs/scheduling). |

You must send either `publishNow: true` or `scheduledFor`. If you send both, `publishNow` wins.

## Validation

Relaycast validates the whole post for every target before anything is saved: character limits, media counts and types, required options. If anything fails, nothing is created or charged, and you get `400` with the problems grouped by account ID:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Content is not valid for some platforms",
    "details": {
      "2b7d9e1f-3a4c-4d5e-8f6a-7b8c9d0e1f2a": [
        { "field": "content", "message": "Bluesky allows 300 characters (this post has 342)" }
      ]
    }
  }
}
```

Fix the content (or use `targets[].content` to shorten it for that account) and send again. Other request errors such as `unknown_account` and `account_inactive` are listed in [Errors](/docs/errors).

> [!TIP]
> The dashboard [Playground](/docs/playground) runs the same validation live as you type, without creating or charging anything.

## The response

A new post returns `201 Created`:

```json
{
  "data": {
    "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "content": "We just shipped dark mode 🌙",
    "media": [{ "url": "https://example.com/dark-mode.png", "type": "image", "altText": "App in dark mode" }],
    "status": "publishing",
    "scheduledFor": "2026-10-05T14:03:11.000Z",
    "request": { "...": "the request body as submitted" },
    "createdAt": "2026-10-05T14:03:11.000Z",
    "updatedAt": "2026-10-05T14:03:11.000Z",
    "targets": [
      {
        "id": "0a1b2c3d-...",
        "accountId": "8f1c2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
        "platform": "linkedin",
        "status": "queued",
        "content": null,
        "options": {},
        "attempts": 0,
        "externalPostId": null,
        "externalUrl": null,
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": null,
        "createdAt": "2026-10-05T14:03:11.000Z",
        "updatedAt": "2026-10-05T14:03:11.000Z"
      }
    ]
  }
}
```

A target's `content` is `null` when it uses the shared `content`. A replay with the same `Idempotency-Key` returns `200` with the original post, see [Idempotency & Retries](/docs/idempotency).

## Get results

Publishing happens in the background. Fetch the post to see each target's outcome:

```bash
curl https://social-publish-api.onrender.com/v1/posts/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
  -H "x-access-key: $RELAYCAST_API_KEY"
```

- A **published** target has `externalPostId`, `externalUrl` (a link to the live post) and `publishedAt`.
- A **failed** target has `errorCode` and `errorMessage`. Codes are `media_error`, `invalid_content`, `auth_revoked`, `rate_limited` and `platform_error`, see [Errors](/docs/errors#publish-errors).
- The post's `status` rolls up the targets: `published`, `partial` or `failed`. See [Core Concepts](/docs/core-concepts#post-statuses).

Unknown post IDs, or posts from another project, return `404 not_found`.

## List posts

`GET /v1/posts` returns your most recent posts, newest first, with their targets. Use `limit` (1–100, default 20):

```bash
curl "https://social-publish-api.onrender.com/v1/posts?limit=50" -H "x-access-key: $RELAYCAST_API_KEY"
```

There's no cursor pagination, so for a full history use the **Posts** page in the dashboard, which supports search, filters and a calendar view.

## Editing and deleting

The public API doesn't have endpoints to edit, cancel or delete a post. To cancel a post that hasn't published yet, delete it from the **Posts** page in the dashboard: pending targets are cancelled and their credits refunded. Targets that already published stay live on the platform.

## Related

- [Create a post](/docs/api/create-post), [Get a post](/docs/api/get-post), [List posts](/docs/api/list-posts)
- [Scheduling](/docs/scheduling)
- [Notifications & Webhooks](/docs/notifications)
