API Error Code Catalog
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_UNAVAILABLEhas its own envelope. Unlike the validation codes, it returns{ "code": "THREADS_PERMISSIONS_UNAVAILABLE", "error": "…" }with novalidationobject.
THREADS_PERMISSION_REQUIREDcarries 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-connectionsdoes not report granted scopes, so a missing permission first surfaces asTHREADS_PERMISSION_REQUIREDwhen 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_PUBLISHEDorPUBLISH_OUTCOME_UNKNOWN. Parts markedpublishedinthreadPartsalready 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, andapiValidationRecovery.js. - Per-URL ingestion:
backend/src/app/helpers/mediaUrlIngest.jsandmediaUrlSecurity.js. - File-reference input for
attach-media:backend/src/app/helpers/mediaFileInput.js. - Validation codes actually emitted by scheduling:
backend/src/app/services/postValidationService.jsanderrors/ValidationError.js. - Threads chain permission and publish errors:
backend/src/app/helpers/threadsPermissions.js,backend/src/app/services/threadsPostPreflight.js, andscheduler-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.jsandbackend/src/app/services/platformAnalytics/.