API Error Code Catalog

View as Markdown

This catalog covers stable codes reachable through API-key-authenticated /api/v1 routes. It excludes internal X Bookmarks codes and codes emitted only by dashboard sessions, OAuth, billing, or other non-API-key routes.

Codes can appear at the top level, inside warnings[], inside validation.errors[], per URL in mediaResults[], or per connection in issues on the analytics routes. Match on the code, not the human-readable message.

For retry patterns and idempotency guidance, see Error Handling.

Request and scheduling codes

Code HTTP Location Meaning Retryable? Recovery
IDEMPOTENCY_IN_FLIGHT 409 top-level code Another request with this idempotency key is still running, or its claim was superseded Yes, shortly Retry the identical request with the same key
IDEMPOTENCY_KEY_CONFLICT 422 top-level code The key was already used with a different body or resource No, unchanged Replay the original request or use a new key for the new operation
IDEMPOTENCY_BODY_TOO_COMPLEX 400 top-level code The body is too deeply nested to hash safely for idempotency No Simplify the request body or omit Idempotency-Key if safe retries are not required
PLATFORM_SETTING_UNKNOWN 400 top-level code A platformSettings path is outside the strict allowlist No Remove or correct the path named by field
PLATFORMS_REQUIRED 400 top-level code Scheduling would leave the post with no target platforms No Add at least one platform, or keep the post as a draft
INVALID_CONTENT 400 top-level code update-post received a content value that is not a string No Send content as a string; field names the offending key
INVALID_PLATFORMS 400 top-level code update-post received a platforms value that is not a valid, duplicate-free array of connection IDs No Send a de-duplicated array of IDs from GET /platform-connections; field names the offending key
INVALID_PLATFORM_CONNECTION 400 top-level code A platform ID added by an update-post edit is not a connection owned by the acting user No Use IDs from GET /platform-connections; the offending IDs are listed in invalidPlatforms
INVALID_PLATFORM_ID 400 top-level code A stored or supplied platform key could not be parsed while reconciling platform posts No Send IDs in the <platform>-<platformId> form returned by GET /platform-connections
POST_NOT_EDITABLE 400 top-level code The post group, or one of its platform posts, is past the editable draft/scheduled window No Create a new post; published and failed posts cannot be edited
POST_PUBLISH_IN_PROGRESS 409 top-level code Publishing has already started for the group or one of its platform posts Only after re-checking Re-read with GET /get-post; retry only while the post is still draft or scheduled
POST_GROUP_VERSION_CONFLICT 409 top-level code Another write committed first; this request changed nothing Yes, after re-reading Re-read with GET /get-post, rebase the edit on the current state, and retry
SCHEDULED_TIME_IN_PAST 400 top-level code Strict mode rejected a time at least five minutes in the past No Send a future ISO 8601 UTC time
INVALID_PUBLISH_NOW 400 top-level code publishNow on create-post or update-post is not a boolean No Send true or false, or omit the field
PUBLISH_NOW_CONFLICT 400 top-level code publishNow: true was combined with a future scheduledTime or with status: "draft" No Drop scheduledTime (server time is used) and send no status or scheduled
INVALID_SCHEDULED_TIME 400 top-level code GET /account-context received a scheduledTime query without Z/offset or with an impossible date No Send an ISO 8601 date-time with Z or an offset, or omit it
SCHEDULED_TIME_COERCED 200 warnings[].code A past time was clamped to server time during the warn-first ramp Not an error Read the returned scheduledTime and fix the caller to send a future time
MEDIA_URL_RATE_LIMITED 429 top-level code The fixed-window allowance of 60 ingested URLs was exceeded Yes Wait the Retry-After seconds, then retry; preserve the idempotency key when applicable
INVALID_MEDIA_FILE 400 top-level code attach-media received a field other than file or fileName, a malformed file reference, a download_url that fails the static URL checks, or an invalid file name. Nothing was downloaded. No, unchanged Send only file (with download_url and file_id) and an optional fileName, then schedule separately with update-post
FIRST_COMMENT_INVALID 400 top-level code Invalid firstComment object or firstComments[] list (up to three), including invalid delay or both fields together No Fix the field named by field; use one input form
FIRST_COMMENT_PLATFORM_UNKNOWN 400 top-level code firstComment.platforms names something other than a platform type (for example a connection ID) No Use types such as linkedin or twitter; allowed lists the valid types
FIRST_COMMENT_TOO_LONG 400 top-level code The comment exceeds a targeted platform's limit, or the absolute 25,000-character cap No Shorten it; platform, limit, count and unit say which limit was hit
FIRST_COMMENT_PLATFORM_NOT_TARGETED 200 warnings[].code firstComment.platforms lists a type the post is not published to; it is ignored Not an error Remove the type or add a target on that platform
FIRST_COMMENT_UNSUPPORTED_PLATFORMS 200 warnings[].code Some targets are on platforms without first-comment support; they will be skipped Not an error Nothing required; restrict firstComment.platforms to silence it

Plan and workspace limit codes

These top-level codes are LimitExceededError responses with HTTP 403.

Code Meaning Retryable? Recovery
CONNECTIONS_OVER_LIMIT The workspace already has more connected channels than its plan permits After account changes Disconnect channels or upgrade before scheduling
CHANNEL_LIMIT_REACHED Adding or attaching would exceed the channel allowance After account changes Disconnect a channel or upgrade
SCHEDULE_HORIZON_REACHED The requested time is farther ahead than the plan permits No, unchanged Choose an earlier time or upgrade
SCHEDULED_POST_LIMIT_REACHED The active scheduled-post queue is full After capacity frees Wait for or delete an existing scheduled post, or upgrade
POST_LIMIT_REACHED The monthly account/connection post allowance is exhausted After reset or upgrade Wait for periodEnd or upgrade
PLATFORM_NOT_AVAILABLE The plan does not allow one or more selected platforms No, unchanged Remove the platforms listed in the top-level disallowedPlatforms array or upgrade

Media completion codes

These codes are returned by POST /complete-media/{mediaFileId}. Its error responses carry only error and code, and none sends a Retry-After header.

Code HTTP Location Meaning Retryable? Recovery
INVALID_MEDIA_FILE_ID 400 top-level code mediaFileId is not a Mongo ObjectId No Use the mediaId returned by get-upload-url
MEDIA_FILE_NOT_FOUND 404 top-level code No media row belongs to the caller for that ID No Check the ID and API-key owner; upload again if it was deleted
MEDIA_FILE_INDETERMINATE_TYPE 400 top-level code Neither stored type nor MIME prefix identifies image, video, or PDF No Re-upload with the correct Content-Type
PROBE_FAILED 400 top-level code The bytes failed byte-level validation in this call (PROBE_TIMEOUT when probing took too long). The file is now failed, and later calls return MEDIA_VALIDATION_FAILED. Also returned for a file that failed earlier without a recorded failure code No Delete the file and upload a valid one
MEDIA_VALIDATION_FAILED 400 top-level code The file already failed validation, in an earlier complete-media call or a scheduling check No, unchanged Delete the file with DELETE /media/:mediaId and upload a valid one
MEDIA_UPLOAD_MISSING 400 top-level code No object appeared in storage within five minutes of the first complete-media call, so the file is now failed. Also returned for a file that failed this way earlier No, unchanged Delete the file with DELETE /media/:mediaId, upload it again, and use the new mediaId
MEDIA_FILE_MISSING_URL 500 top-level code The media row has no storage URL No client-side retry contract Upload again; contact support if newly created rows repeat this state
PROBE_VERSION_UNVERIFIED 503 top-level code The object version could not be verified before or after probing Yes Retry complete-media after a few seconds
PROBE_DOWNLOAD_FAILED 503 top-level code A transient S3/CDN download prevented probing Yes Retry complete-media after a few seconds
PROBE_VERSION_CHANGED 503 top-level code The uploaded object changed while it was being validated Yes, after upload stabilizes Stop overwriting the presigned object and retry
MEDIA_UPLOAD_PENDING 503 top-level code The uploaded object is not visible in storage yet: its PUT has not finished, or never happened Yes Finish the PUT, then retry complete-media after a few seconds. If there is still no object five minutes after the first complete-media call for the file, the file is marked failed and the call returns 400 MEDIA_UPLOAD_MISSING
MEDIA_STATE_PERSIST_FAILED 503 top-level code The completion request or the validation result could not be saved Yes Retry complete-media after a few seconds
PROBE_BUSY 503 top-level code Too many media validations are running at once Yes Retry complete-media after a few seconds
MEDIA_FILE_RACE 409 top-level code The row was deleted or failed concurrently during probing No Re-upload and use the new media ID

Media scheduling codes

update-post checks every attached media file whenever the post's resulting status is scheduled. A file still in uploading is validated on the spot, and the request waits up to about 23 seconds for that. create-post has no such check: the only media it can attach comes from mediaUrls, which is validated while it downloads.

When the check does not pass, the post was not scheduled and none of its fields were saved. Media validation progress is kept: a file the check marked ready or failed stays that way, and a check still running when the response is sent can finish afterwards. Media from mediaUrls in the same request is removed again, and an Idempotency-Key is released, so a retry with the same key runs the request again instead of replaying the error.

Code HTTP Location Meaning Retryable? Recovery
MEDIA_UPLOAD_PENDING 503 top-level code An attached upload has no object in storage yet: its PUT has not finished, or never happened Yes Finish the PUT, then retry after Retry-After seconds. Every attempt returns this code while the bytes are missing. Once complete-media has been called for the file and five minutes pass without an object, the file is marked failed and scheduling returns 400 MEDIA_UPLOAD_MISSING
MEDIA_VALIDATION_PENDING 503 top-level code Validation did not finish within the wait: storage or probing was temporarily unavailable, or a large file is still being checked. pendingCode names the last transient result Yes Keep the same mediaId and retry after Retry-After seconds. A check that finishes after the response is kept for the retry
MEDIA_UPLOAD_MISSING 400 top-level code The upload never produced an object in storage No, unchanged Delete the file with DELETE /media/:mediaId, upload it again, and schedule with the new mediaId
MEDIA_VALIDATION_FAILED 400 top-level code The uploaded bytes are not valid media: corrupt, unsupported, or too slow to probe No, unchanged Delete the file, upload a supported one, and schedule again
MEDIA_REFERENCE_MISSING 400 top-level code The post references a media record that no longer exists No, unchanged Remove the reference with DELETE /post/:postGroupId/media/:mediaId (MCP prune_media_reference), then schedule again
MEDIA_NOT_READY 400 top-level code Fallback for a file that is not usable and has no more specific code, such as a failed file without a recorded failure code, or an upload whose media type cannot be determined After fixing media Delete and re-upload the file, then schedule again

The 503 responses carry a Retry-After: 5 header and this body:

{
  "code": "MEDIA_UPLOAD_PENDING",
  "error": "Media upload is still pending. The post was not scheduled; finish the upload and retry scheduling.",
  "mediaFileId": "507f1f77bcf86cd799439012",
  "mediaStatus": "uploading",
  "pendingCode": "MEDIA_UPLOAD_PENDING",
  "attempts": 18,
  "retryable": true,
  "retryAfterSec": 5,
  "suggestions": ["Media bytes are not uploaded or complete yet. PUT the file bytes to the presigned uploadUrl …"]
}

mediaFileId is the first attached file that is not ready, and attempts counts the validation passes made during the wait. suggestions holds one recovery hint, worded for MCP tools when the request carries x-publora-client: mcp.

The 400 responses return { error, code, mediaFileId, mediaStatus, reason, retryable, suggestions }. reason is the file's recorded failure reason or null, and retryable is false once the file has failed.

Earlier versions of this page listed MEDIA_VALIDATION_PENDING as a 200 warning. Scheduling no longer succeeds while validation is pending; it returns the 503 above. complete-media reports a missing or pending upload with its own codes; see Media completion codes.

mediaUrls per-URL codes

When create/update or attach-media ingestion fails, the request returns HTTP 400 with error: "Media ingestion failed"; the URL/fetch codes below can appear in mediaResults[].code. On attach-media, a download_url that fails the static checks (not a URL, not HTTPS, embedded credentials, a non-443 port, a private or loopback IP address, or a localhost, .local or .internal name) is rejected before any download with top-level INVALID_MEDIA_FILE. Everything found later, including DNS results, redirects, fetch errors, size, format and probe failures, is reported in mediaResults[]. Probe failures are forwarded into the same field using the applicable code from Media completion codes. The batch is all-or-nothing.

Code HTTP Location Meaning Retryable? Recovery
MEDIA_URL_INVALID 400 mediaResults[].code The value is not a URL No Supply a valid public URL
MEDIA_URL_PROTOCOL 400 mediaResults[].code The URL is not HTTPS No Use https://
MEDIA_URL_CREDENTIALS 400 mediaResults[].code The URL embeds a username or password No Use a credential-free URL, such as a signed HTTPS URL
MEDIA_URL_PORT 400 mediaResults[].code The URL uses a non-default HTTPS port No Serve it on port 443
MEDIA_URL_BLOCKED_HOST 400 mediaResults[].code The host name is blocked (localhost, .local, .internal), the URL uses a private or reserved IP address, or any address the host or a redirect target resolves to is private or reserved No Use a publicly reachable host
MEDIA_URL_DNS 400 mediaResults[].code Defensive code for a DNS answer that holds no addresses. The system resolver reports that case as a failed lookup instead, so this code is not currently expected: a nonexistent domain and a host with no A or AAAA records are both reported as MEDIA_URL_FETCH_FAILED Not currently expected If observed, correct the host's DNS, then retry the whole request
MEDIA_URL_REDIRECT 400 mediaResults[].code A redirect lacked a Location header After the origin is fixed Use the final URL or fix the redirect
MEDIA_URL_TOO_MANY_REDIRECTS 400 mediaResults[].code The fetch exceeded the redirect limit No, unchanged Use a direct URL
MEDIA_URL_HTTP_ERROR 400 mediaResults[].code The remote server returned a non-200 response After the origin is available Check access/expiry and retry with a working URL
MEDIA_URL_INGEST_TIMEOUT 400 mediaResults[].code The request-wide media ingestion budget expired Conditionally Use a faster origin, smaller media, or the presigned upload flow
MEDIA_URL_TOO_LARGE 400 mediaResults[].code One image/video exceeds the URL-ingestion byte cap No Use a smaller file or get-upload-url
MEDIA_URL_AGGREGATE_TOO_LARGE 400 mediaResults[].code Combined downloaded bytes exceed 300 MiB No Reduce the batch or use presigned uploads
MEDIA_URL_UNSUPPORTED_FORMAT 400 mediaResults[].code Magic-byte detection found an unsupported format No Convert to a supported image/video format
MEDIA_URL_FETCH_FAILED 400 mediaResults[].code An unexpected network/download failure occurred. This includes a failed DNS lookup, whether the domain does not exist or the host has no A or AAAA records Conditionally Verify the URL and the host's DNS, and retry once; otherwise use presigned upload
MEDIA_URL_PROBE_FAILED 400 mediaResults[].code Defensive fallback if a future failed probe supplies no specific code; current probeAndPersistMedia failure branches always supply one Not currently expected If observed, convert or re-upload valid media and report the unexpected fallback
MEDIA_URL_SKIPPED 400 mediaResults[].code This URL was not attempted after another URL failed After fixing the root failure Correct the failing URL and retry the complete batch
MEDIA_URL_ROLLED_BACK 400 mediaResults[].code This URL succeeded, but its media was removed because another URL failed After fixing the root failure Correct the failing URL and retry the complete batch

Mastodon and Bluesky analytics codes

POST /post-statistics reports per-connection problems in the issues object of a 200 response, keyed by connection ID (<platform>-<platformId>); the affected postedId values are null in stats. POST /profile-statistics reports the same conditions in unavailable (AUTH_REVOKED, FORBIDDEN) and rateLimited. See Mastodon and Bluesky Statistics.

Code HTTP Location Meaning Retryable? Recovery
ANALYTICS_PLAN_REQUIRED 403 top-level code The plan has no analytics feature No, unchanged Upgrade to a plan that includes analytics
ANALYTICS_REQUEST_TIMEOUT 504 top-level code The request exceeded its 90-second budget Yes Retry with fewer posts per request
CONNECTION_NOT_FOUND 200 issues[<connection>] No connection of the caller matches that platform and platform ID No, unchanged Re-read GET /platform-connections
AUTH_REVOKED 200 issues[<connection>], unavailable Mastodon rejected the stored token (HTTP 401) After reconnecting Reconnect the account
FORBIDDEN 200 issues[<connection>], unavailable Mastodon refused analytics access (HTTP 403, scope or moderation). Publishing is unaffected. Later Retry later; reconnect if it persists
RATE_LIMITED 200 issues[<connection>] The platform is in a cooldown after answering 429 Yes, after the cooldown Retry later; the response also carries rateLimited: true
FETCH_FAILED 200 issues[<connection>] A transient failure reaching the platform Yes Retry later

LinkedIn symbolic errors

These LinkedIn endpoints use the error field for stable symbolic values rather than a separate code field.

Value HTTP Meaning Retryable? Recovery
REACTION_ALREADY_EXISTS 409 The actor already has a reaction on the target post No, unchanged Delete the existing reaction, then apply the requested type
LINKEDIN_TOKEN_EXPIRED 401 The stored LinkedIn access token is expired No Reconnect the LinkedIn account
LINKEDIN_RATE_LIMITED 429 LinkedIn is rate-limiting the analytics resource/account Yes Wait the returned retryAfter seconds
LINKEDIN_REQUEST_TIMEOUT 504 The backend request deadline elapsed while waiting for LinkedIn Yes Retry the read/interaction with normal backoff

Post validation codes

Scheduling validation returns HTTP 400. These codes appear in validation.errors[].code. A successful request returns no validation object at all, so validation warnings are not part of the success contract.

Code Meaning Recovery
CONTENT_TOO_LONG Content exceeds the selected platform's character limit Shorten it or change the target platform/account tier
CONTENT_OR_MEDIA_REQUIRED The post has neither usable text nor media Add content or media
MEDIA_REQUIRED A selected platform requires media Attach supported images or video
MEDIA_SIZE_EXCEEDED A file exceeds the platform byte limit Compress or replace it
MEDIA_COUNT_EXCEEDED Too many media items are attached Delete extras and schedule again
MEDIA_TYPE_NOT_SUPPORTED The media type is unsupported for the platform Convert or remove it
MEDIA_DIMENSIONS_INVALID Image/video dimensions violate a platform rule Resize the media
MEDIA_ASPECT_RATIO_INVALID Aspect ratio violates a platform rule Crop or resize the media
VIDEO_REQUIRED The selected platform requires video Attach a supported video
VIDEO_DURATION_EXCEEDED Video is longer than the platform maximum Trim it
VIDEO_DURATION_TOO_SHORT Video is shorter than the platform minimum Use a longer video
IMAGES_NOT_SUPPORTED The selected platform does not accept images for this post type Remove images or change the target
DOCUMENTS_NOT_SUPPORTED The selected platform does not accept document media Remove the document or target LinkedIn
PLATFORM_SETTING_REQUIRED A required platform-specific setting is absent Supply the field identified in the error
PLATFORM_SETTING_INVALID A platform-specific setting has an invalid value Use one of the reported accepted values
PLATFORM_NOT_SUPPORTED The validator has no contract for the target platform Remove the unsupported target

All validation codes above are non-retryable with the unchanged request.

Threads chain codes

These codes concern multi-part Threads chains only. A single Threads post never triggers the permission check.

At scheduling

Code HTTP Location Meaning Retryable? Recovery
THREADS_PERMISSION_REQUIRED 400 validation.errors[].code The connection's token does not grant threads_manage_replies, or the connection no longer exists. Nothing was published No, unchanged Reconnect that Threads account in Publora Channels, approve all requested permissions, then schedule again
TOKEN_EXPIRED 400 validation.errors[].code The stored Threads token is absent, or Meta reports it invalid No, unchanged Reconnect the Threads account
THREADS_PERMISSIONS_UNAVAILABLE 503 top-level code Publora could not complete the permission check itself. This is not an account problem and never means reconnect Usually Retry scheduling in a few seconds. A lookup that keeps failing is a Publora-side configuration problem no retry will clear — contact support
THREAD_PART_TOO_LONG 400 validation.errors[].code A resolved thread part exceeds 500 characters. Automatic splitting never produces one, but a [n/m] part you wrote yourself is not re-split, so an oversized one lands here No, unchanged Shorten that part; field names its index
INVALID_PLATFORM_CONTENT 400 validation.errors[].code A stored thread part is not a text string No, unchanged Edit or remove the invalid part

THREADS_PERMISSIONS_UNAVAILABLE has its own envelope. Unlike the validation codes, it returns { "code": "THREADS_PERMISSIONS_UNAVAILABLE", "error": "…" } with no validation object.

THREADS_PERMISSION_REQUIRED carries one of two messages. A connection whose token lacks the grant returns "Reconnect your Threads account and allow reply publishing to publish a thread with multiple posts."; a target whose connection no longer exists returns "Reconnect your Threads account before scheduling a thread." Match on the code, not the message.

There is no way to pre-check the grant. GET /platform-connections does not report granted scopes, so a missing permission first surfaces as THREADS_PERMISSION_REQUIRED when you schedule a chain.

At publishing

These appear in posts[].error.code from GET /get-post, never as an HTTP status.

Code Meaning Retryable? Recovery
THREADS_INVALID_THREAD_PART A part was empty, not text, or over 500 characters including its numbering No Fix the content and create a new post
THREAD_PARTIALLY_PUBLISHED Some parts published, then one failed. The published parts are live and Publora will not republish the chain No Read threadParts, then publish only the missing parts
PUBLISH_OUTCOME_UNKNOWN Threads accepted a publish request without returning an ID, so the outcome is genuinely unknown. Automatic retry is disabled to avoid duplicates No Check the live Threads account before posting anything further
THREADS_PERMISSION_REQUIRED The grant disappeared between scheduling and publishing. Nothing was published No Reconnect the account and schedule again

Never resubmit a chain after THREAD_PARTIALLY_PUBLISHED or PUBLISH_OUTCOME_UNKNOWN. Parts marked published in threadParts already exist on the platform; recreating the post duplicates them.

Workspace attachment codes

These lowercase codes are top-level response fields from POST /workspace/users or POST /workspace/users/attach.

Code HTTP Meaning Recovery
email_exists 409 A user with the requested email already exists Inspect the returned attachment status; use the attach flow when allowed
missing_identifier 400 Neither email nor user ID was supplied Send one identifier
invalid_user_id 400 The user ID is malformed Send a Mongo ObjectId
invalid_email 400 The email format is invalid Correct the email
user_not_found 404 No active target user was found Check the identifier
identifier_mismatch 400 Supplied email and user ID identify different users Correct one identifier
user_inactive 409 The target user is inactive Reactivate the user before attaching
cannot_attach_self 400 The workspace owner was supplied as the target Choose another user
user_is_workspace_owner 409 The target owns a workspace A workspace owner cannot be attached as managed
user_in_another_workspace 409 The target already belongs to another workspace Detach it from the other workspace first
user_has_active_subscription 409 The target has a blocking subscription Resolve that subscription before attaching
attach_consent_required 403 Attaching an existing standalone user requires its consent token Supply the target user's access token
invalid_attach_consent 403 The consent token is invalid or revoked Obtain a fresh token from the target user
attach_consent_mismatch 403 The token belongs to another user Supply the target user's own token

Source coverage

  • Top-level scheduling and idempotency responses: backend/src/app/routes/apiRoute.js.
  • Plan/workspace entitlement responses: backend/src/app/services/limitsService.js.
  • Scheduling guards and media state: backend/src/app/helpers/scheduledPlatformsGuard.js, mediaStatusTransitions.js, mediaUploadPolicy.js, probeMediaUpload.js, and apiValidationRecovery.js.
  • Per-URL ingestion: backend/src/app/helpers/mediaUrlIngest.js and mediaUrlSecurity.js.
  • File-reference input for attach-media: backend/src/app/helpers/mediaFileInput.js.
  • Validation codes actually emitted by scheduling: backend/src/app/services/postValidationService.js and errors/ValidationError.js.
  • Threads chain permission and publish errors: backend/src/app/helpers/threadsPermissions.js, backend/src/app/services/threadsPostPreflight.js, and scheduler-service/src/app/controllers/threadsController.js.
  • Workspace attachment responses: backend/src/app/controllers/workspaceApiController.js.
  • LinkedIn interaction/analytics errors: backend/src/app/controllers/linkedinController.js.
  • Mastodon/Bluesky analytics responses: backend/src/app/controllers/platformAnalyticsController.js and backend/src/app/services/platformAnalytics/.