Posts

Create a post

Publish now or schedule one post to up to 20 social accounts, with shared media and per-account text and options.

POST https://social-publish-api.onrender.com/v1/posts

Creates a post for one or more connected accounts. Each account becomes a target that is validated, published and tracked separately, so one platform failing doesn't affect the others.

The request is validated against every target platform's rules before anything is saved. If any target is invalid, nothing is created or charged and you get a 400 listing the problems per account. On success, 1 credit per target is charged and the targets are queued: immediately with publishNow: true, or for scheduledFor.

Headers

HeaderRequiredDescription
x-access-keyYes*Your API key. *Or send Authorization: Bearer <key>.
content-typeYesapplication/json
Idempotency-KeyNoAny unique string (for example a UUID). Retrying with the same key returns the original post instead of creating a duplicate. See Idempotency.

Body

FieldTypeRequiredDescription
contentstringYesPost text, 1–10,000 characters. Used for every target without its own content. Platform limits are lower; see the platform guides.
targetsobject[]Yes1–20 accounts to post to.
targets[].accountIdstring (UUID)YesAccount ID from List accounts. Must be active.
targets[].contentstringNoReplaces content for this account only (1–10,000 characters).
targets[].optionsobjectNoPlatform-specific options, for example { "privacyLevel": "SELF_ONLY" } for TikTok. See Platform Options.
mediaobject[]NoUp to 10 items, shared by all targets. Default [].
media[].urlstringYesPublic http(s) URL, or media://<asset-id> for a file in your media library. Max 2,048 characters.
media[].typestringYesimage, video or document. document (PDF) is only accepted by LinkedIn.
media[].altTextstringNoAlt text, up to 1,000 characters (used where the platform supports it).
media[].titlestringNoTitle shown on a document card, up to 400 characters (documents only).
publishNowbooleanNo*true publishes right away. Default false.
scheduledForstringNo*ISO 8601 date-time with a time zone (Z or an offset), in the future.

* Send either publishNow: true or scheduledFor. If both are sent, publishNow: true wins. A scheduledFor more than one minute in the past is rejected.

Duplicate account IDs in targets are merged: each account is posted to once, using the last entry for that account.

Request

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: 4d6e9a8c-launch-2026-11-02" \
  -d '{
    "content": "Our new release is live. Faster uploads, a calendar view and more.",
    "media": [
      { "url": "https://cdn.example.com/release.jpg", "type": "image", "altText": "Release banner" }
    ],
    "targets": [
      { "accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f" },
      {
        "accountId": "3c8d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        "content": "Our new release is live. Here is what our team shipped this quarter and why it matters for your workflow.",
        "options": { "visibility": "PUBLIC" }
      }
    ],
    "scheduledFor": "2026-11-02T15:00:00Z"
  }'
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": "4d6e9a8c-launch-2026-11-02",
  },
  body: JSON.stringify({
    content: "Our new release is live. Faster uploads, a calendar view and more.",
    media: [{ url: "https://cdn.example.com/release.jpg", type: "image", altText: "Release banner" }],
    targets: [
      { accountId: "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f" },
      {
        accountId: "3c8d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        content:
          "Our new release is live. Here is what our team shipped this quarter and why it matters for your workflow.",
        options: { visibility: "PUBLIC" },
      },
    ],
    scheduledFor: "2026-11-02T15:00:00Z",
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
const post = body.data; // res.status is 201 (created) or 200 (idempotent replay)
import os, requests

res = requests.post(
    "https://social-publish-api.onrender.com/v1/posts",
    headers={
        "x-access-key": os.environ["RELAYCAST_API_KEY"],
        "Idempotency-Key": "4d6e9a8c-launch-2026-11-02",
    },
    json={
        "content": "Our new release is live. Faster uploads, a calendar view and more.",
        "media": [{"url": "https://cdn.example.com/release.jpg", "type": "image", "altText": "Release banner"}],
        "targets": [
            {"accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f"},
            {
                "accountId": "3c8d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                "content": "Our new release is live. Here is what our team shipped this quarter and why it matters for your workflow.",
                "options": {"visibility": "PUBLIC"},
            },
        ],
        "scheduledFor": "2026-11-02T15:00:00Z",
    },
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
post = body["data"]

Response

201 Created with the new post. With publishNow: true the post status is publishing; with scheduledFor it is scheduled. Targets start as queued.

Response 201
{
  "data": {
    "id": "a3f8c2d1-9b7e-4c6a-8f5d-2e1b0c9a8d7f",
    "content": "Our new release is live. Faster uploads, a calendar view and more.",
    "media": [
      { "url": "https://cdn.example.com/release.jpg", "type": "image", "altText": "Release banner" }
    ],
    "status": "scheduled",
    "scheduledFor": "2026-11-02T15:00:00.000Z",
    "request": {
      "content": "Our new release is live. Faster uploads, a calendar view and more.",
      "targets": [
        { "accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f" },
        {
          "accountId": "3c8d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
          "content": "Our new release is live. Here is what our team shipped this quarter and why it matters for your workflow.",
          "options": { "visibility": "PUBLIC" }
        }
      ],
      "media": [
        { "url": "https://cdn.example.com/release.jpg", "type": "image", "altText": "Release banner" }
      ],
      "publishNow": false,
      "scheduledFor": "2026-11-02T15:00:00Z"
    },
    "createdAt": "2026-10-05T09:30:12.441Z",
    "updatedAt": "2026-10-05T09:30:12.441Z",
    "targets": [
      {
        "id": "e1d2c3b4-a596-4877-8695-a4b3c2d1e0f9",
        "accountId": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f",
        "platform": "twitter",
        "status": "queued",
        "content": null,
        "options": {},
        "attempts": 0,
        "externalPostId": null,
        "externalUrl": null,
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": null,
        "createdAt": "2026-10-05T09:30:12.441Z",
        "updatedAt": "2026-10-05T09:30:12.441Z"
      },
      {
        "id": "f0e9d8c7-b6a5-4493-8281-706f5e4d3c2b",
        "accountId": "3c8d1e2f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
        "platform": "linkedin",
        "status": "queued",
        "content": "Our new release is live. Here is what our team shipped this quarter and why it matters for your workflow.",
        "options": { "visibility": "PUBLIC" },
        "attempts": 0,
        "externalPostId": null,
        "externalUrl": null,
        "errorCode": null,
        "errorMessage": null,
        "publishedAt": null,
        "createdAt": "2026-10-05T09:30:12.441Z",
        "updatedAt": "2026-10-05T09:30:12.441Z"
      }
    ]
  }
}

The fields are described in Get a post. To follow progress, poll GET /v1/posts/{id} or subscribe to webhook events.

Idempotency

Send an Idempotency-Key header to make retries safe:

SituationResult
First request with a key201, post created and credits charged
Any later request with the same key in the same project200 with the original post; nothing is created or charged
Two concurrent requests with the same keyOne creates the post, the other gets 200 with it

The key is checked before the body is validated, and the body of a replay is not compared with the original, so use a new key for every distinct post. Keys are scoped to the project. See Idempotency & Retries.

Errors

StatusCodeWhen
400invalid_jsonThe body isn't valid JSON.
400validation_errorSchema errors (details is a list of issues with path and message), content that a platform would reject (details is keyed by account ID), or scheduledFor in the past.
400unknown_accountOne or more accountIds don't exist in this project. details.missing lists them.
400account_inactiveOne or more accounts need reconnecting. details.accountIds lists them.
400unknown_mediaA media:// reference (in media or in options) isn't in this project's media library. details.missing lists them.
401unauthorizedMissing or invalid API key.
402insufficient_creditsThe project has fewer credits than targets. The message says how many are needed.
429rate_limitedRate limit exceeded for this key.
Response 400
{
  "error": {
    "code": "validation_error",
    "message": "Content is not valid for some platforms",
    "details": {
      "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716": [
        { "field": "options.privacyLevel", "message": "Choose who can view this TikTok post" }
      ],
      "8a7b6c5d-4e3f-4a1b-9c8d-7e6f5a4b3c2d": [
        { "field": "media", "message": "YouTube needs exactly one video" }
      ]
    }
  }
}
Response 402
{
  "error": {
    "code": "insufficient_credits",
    "message": "This post needs 2 credits"
  }
}

After the post is created

  • Each target is published independently. Temporary failures are retried automatically with backoff; permanent failures set the target to failed with errorCode and errorMessage, and its credit is refunded.
  • The post status rolls up from its targets: published when all succeeded, failed when all failed, partial when some failed.
  • To cancel a scheduled post, delete it from Posts in the dashboard; targets that haven't been published yet are cancelled and refunded. There is no delete endpoint in the API.

See Posts and Scheduling for the full lifecycle.

This page is also available as Markdown.