Docs
Docs/API Reference/Errors

Errors

What Octopost API errors look like, the status codes each endpoint uses, the publish error codes, and which errors are safe to retry.

Octopost reports errors with an HTTP status code and a JSON body. Branch on the status code: most errors don't carry a machine-readable code.

Error shapes#

Most errors have just a message:

{ "error": "Post not found" }

Validation errors (400) add details. When the body fails schema validation, details is a list of issues, each with a path (the field) and a message:

{
  "error": "Invalid request",
  "details": [
    {
      "code": "custom",
      "path": ["scheduledFor"],
      "message": "scheduledFor must be an ISO 8601 string with a timezone offset (e.g., 2026-04-20T21:00:00.000Z)"
    }
  ]
}

A few validation errors use a sentence in details instead:

{
  "error": "Poll validation failed",
  "details": "Polls are only supported on Twitter and Mastodon. Remove poll or remove these platforms: bluesky"
}

No plan (403) carries a code:

{
  "error": "Nothing was posted: this Octopost account has no active plan ... https://octopost.ink/dashboard/billing ...",
  "code": "trial_expired"
}

No API key at all (401) adds fix and docs:

{
  "error": "Unauthorized: no Octopost session cookie or API key on this request.",
  "fix": "Send your key as \"Authorization: Bearer op_...\" (or \"x-api-key: op_...\"). ...",
  "docs": "https://octopost.ink/docs/api/authentication.md"
}

A key that is present but not valid gets { "error": "Unauthorized" }.

Status codes#

StatusWhenRetry?
400Validation failed, the body isn't valid JSON, an upload is too big or not a supported type, or the action doesn't fit the resource's state (for example "Post is already published", "Cannot edit published posts", "Post is not pending approval", "No failed platforms to retry").No. Fix the request first.
401Missing or invalid API key. See Authentication.No.
403No active plan ("code": "trial_expired"), email not verified, or a plan limit such as the number of connected accounts.No.
404No such resource in your account ("Post not found", "Preset not found", "Webhook not found", "Account not found").No.
409The post is being published right now by another request or the scheduler ("code": "already_publishing"), it changed before it could be published ("code": "precondition"), or an Idempotency-Key points at a deleted post.already_publishing: check the post again in a minute instead of retrying.
422POST /api/posts/{id}/publish got an Idempotency-Key that was already used for a different request.No. Use a new key.
429Rate limited. Includes Retry-After. See Rate Limits.Yes, after Retry-After seconds.
500{ "error": "Internal server error" } or a specific message.Yes with backoff. Send an Idempotency-Key on creates and publishes so a retry can't post twice.
502, 503, 504Octopost was briefly unreachable.Yes, with backoff.

207: some or all networks failed#

Publishing endpoints answer 207 when the request worked but the post isn't live everywhere:

Endpoint200/201207
POST /api/posts with published: true201, status: "published"status: "partial" or "failed", success: false
POST /api/posts/{id}/publish200, status: "published"status: "partial" (success: true) or "failed" (success: false)
POST /api/posts/{id}/retry200, everything livestatus: "partial" or "failed"

Always read status and results on these, not just the HTTP code.

Network failures aren't HTTP errors#

When Octopost reaches a network and the network refuses the post (expired sign-in, too long, monthly X allowance used up), the failure is in the post's per-network results:

{
  "status": "partial",
  "results": {
    "twitter": { "platform": "twitter", "status": "published", "postId": "1234567890", "url": "https://x.com/you/status/1234567890" },
    "bluesky": { "platform": "bluesky", "status": "failed", "error": "Bluesky session expired. Reconnect the account.", "errorCode": "AUTH_FAILED", "retryable": false }
  }
}
  • error says what went wrong and what to do.
  • errorCode, when present, is one of the codes below. Some failures have no errorCode.
  • retryable is true for temporary problems (network rate limits, timeouts, server errors on the network's side). Retry those with POST /api/posts/{id}/retry. Scheduled and queued posts retry them automatically.
  • retryable: false needs a fix first, usually reconnecting the account or shortening the text.

The same results are on POST /api/posts, /publish, /retry, GET /api/posts, GET /api/posts/{id} and the webhook payload (as data.results[]). The older per-attempt lists (publishResults, platformResults, retryResults) are still returned for existing clients.

Publish error codes#

errorCodeMeaningWhat to do
AUTH_FAILEDThe network rejected the account's sign-in.Reconnect the account in the dashboard.
TOKEN_REFRESH_FAILEDOctopost couldn't renew the account's sign-in.Reconnect the account.
RATE_LIMITThe network is rate limiting the account.Retry later (retryable: true).
PLAN_LIMITYour plan's monthly X allowance is used up.Wait for next month or change plan.
DUPLICATE_CONTENTThe network rejected it as a duplicate of a recent post.Change the text.
MEDIA_UPLOAD_FAILEDAn image or video couldn't be downloaded or uploaded (too big, wrong format, URL not reachable).Fix the media; the message says which file.
TIMEOUTThe network didn't answer in time.Retry (retryable: true).
THREAD_PARENT_MISSINGA thread part couldn't attach to the part before it.Retry; live parts are kept.
INTERRUPTEDPublishing stopped part way (for example a server restart).Check results, then retry.
NO_POST_IDThe network accepted the request but returned no post id.Check the network before retrying.
PLATFORM_ERRORThe network had a server error (5xx).Retry (retryable: true).
FORBIDDENThe network refused the post (HTTP 403), often a permission or content rule.Read error; fix before retrying.
REJECTEDThe network rejected the post for another reason (other 4xx).Read error; fix before retrying.

Example: handling errors in TypeScript#

async function createPost(content: string, platforms: string[]) {
  const res = await fetch("https://octopost.ink/api/posts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OCTOPOST_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ content, platforms, published: true }),
  });

  const body = await res.json().catch(() => ({}));

  if (res.status === 429) {
    const wait = Number(res.headers.get("Retry-After") ?? 60);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return createPost(content, platforms);
  }
  if (res.status === 403 && body.code === "trial_expired") {
    throw new Error("No active Octopost plan: https://octopost.ink/dashboard/billing");
  }
  if (!res.ok) {
    throw new Error(`Octopost ${res.status}: ${body.error}${body.details ? " " + JSON.stringify(body.details) : ""}`);
  }

  // 207: saved, but not live everywhere
  for (const r of Object.values(body.results ?? {}) as any[]) {
    if (r.status !== "published") console.warn(`${r.platform} ${r.status}: ${r.error} (${r.errorCode ?? "no code"})`);
  }
  return body.post;
}