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#
| Status | When | Retry? |
|---|---|---|
400 | Validation 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. |
401 | Missing or invalid API key. See Authentication. | No. |
403 | No active plan ("code": "trial_expired"), email not verified, or a plan limit such as the number of connected accounts. | No. |
404 | No such resource in your account ("Post not found", "Preset not found", "Webhook not found", "Account not found"). | No. |
409 | The 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. |
422 | POST /api/posts/{id}/publish got an Idempotency-Key that was already used for a different request. | No. Use a new key. |
429 | Rate 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, 504 | Octopost 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:
| Endpoint | 200/201 | 207 |
|---|---|---|
POST /api/posts with published: true | 201, status: "published" | status: "partial" or "failed", success: false |
POST /api/posts/{id}/publish | 200, status: "published" | status: "partial" (success: true) or "failed" (success: false) |
POST /api/posts/{id}/retry | 200, everything live | status: "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 }
}
}errorsays what went wrong and what to do.errorCode, when present, is one of the codes below. Some failures have noerrorCode.retryableistruefor temporary problems (network rate limits, timeouts, server errors on the network's side). Retry those withPOST /api/posts/{id}/retry. Scheduled and queued posts retry them automatically.retryable: falseneeds 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#
errorCode | Meaning | What to do |
|---|---|---|
AUTH_FAILED | The network rejected the account's sign-in. | Reconnect the account in the dashboard. |
TOKEN_REFRESH_FAILED | Octopost couldn't renew the account's sign-in. | Reconnect the account. |
RATE_LIMIT | The network is rate limiting the account. | Retry later (retryable: true). |
PLAN_LIMIT | Your plan's monthly X allowance is used up. | Wait for next month or change plan. |
DUPLICATE_CONTENT | The network rejected it as a duplicate of a recent post. | Change the text. |
MEDIA_UPLOAD_FAILED | An image or video couldn't be downloaded or uploaded (too big, wrong format, URL not reachable). | Fix the media; the message says which file. |
TIMEOUT | The network didn't answer in time. | Retry (retryable: true). |
THREAD_PARENT_MISSING | A thread part couldn't attach to the part before it. | Retry; live parts are kept. |
INTERRUPTED | Publishing stopped part way (for example a server restart). | Check results, then retry. |
NO_POST_ID | The network accepted the request but returned no post id. | Check the network before retrying. |
PLATFORM_ERROR | The network had a server error (5xx). | Retry (retryable: true). |
FORBIDDEN | The network refused the post (HTTP 403), often a permission or content rule. | Read error; fix before retrying. |
REJECTED | The 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;
}