Getting started
Errors
Every failure is { error: { code, message, params }, meta }; branch on code. The table is ordered by how often you will meet each one.
Code reference
| code | HTTP | Meaning | What to do |
|---|---|---|---|
| unauthenticated | 401 | Missing Bearer credential, or it is invalid, disabled, expired, or not a long-lived credential (popkit_it_…). | Check or re-issue the credential under Settings → Integrations → Long-lived credentials; send Authorization: Bearer <credential>. |
| forbidden | 403 | The credential lacks the required scope, the issuer's workspace role forbids the action, or the requested publish tier is not on the credential's allow-list. params carries scope / action. | Issue a credential with the required scope / tier; role problems need the workspace admin. |
| workspace_not_found | 403 | The workspace bound to the credential was dissolved, or the issuer is no longer an active member or is banned. | Have an admin who is still in the workspace issue a new credential. |
| not_found | 404 | The target does not exist or does not belong to this workspace (not distinguished, to prevent enumeration). | Fetch the id again from the list endpoint; never reuse a replyRowId as a commentId. |
| invalid_input | 400 | Invalid input: missing required field, bad enum, timestamp without timezone, broken cursor, publish tier without Idempotency-Key, etc. message names the fields. | Fix the fields named in message and retry; the same Idempotency-Key may be reused (failures are not cached). |
| plan_limit | 402 | Plan quota exhausted (e.g. scheduled posts this month). params carries limit / used. | Check quotas with GET /v1/workspace; wait for next month or upgrade the plan. |
| no_account | 409 | The target platform has no usable account in this workspace. | Have a user connect an account under Accounts in the app; integrations cannot do this. |
| needs_reauth | 409 | The target account's platform authorization has expired. | Have the user re-authorize in the app; look for status: "expired" in GET /v1/accounts. |
| rate_limited | 429 | This credential exceeded the per-minute cap for the tier, or hit the daily publish / comment-reply cap or the per-account minimum interval. params carries limit / windowSec / retryAfterSec or reason. | Wait for the Retry-After header and retry; daily-cap reasons do not clear until the next day. |
| replay | 409 | A request with the same Idempotency-Key is still in progress. | Retry the same key shortly; once finished you get the first result back with meta.replayed: true. |
| external_writes_off | 503 | External writes are disabled in this environment (test-environment safety valve); publishing / replying will not go out. | Environment configuration, not an input problem; call the production environment. |
| platform_error | 502 | The platform rejected or failed the request (policy, duplicate, 5xx). params.reason carries a machine code. | Inspect per-account errorCode via GET /v1/posts/{postId}; retry through POST /v1/posts/{postId}/publish. |
| internal | 500 | Unclassified server error; it has been logged. | Contact us with the meta.requestId from the response. |
Example
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded for tool create_post (write): 10 calls per 60s",
"params": {
"limit": 10,
"windowSec": 60,
"retryAfterSec": 42
}
},
"meta": {
"requestId": "3f9c1e2a-7d4b-4c1e-9a6f-0b2d8e5c7a19"
}
}Per-account failures
Publishing is asynchronous: a successful status: "publishing" only means the request was accepted. A later failure on one account does not become an HTTP error; it shows up in the post detail as targets[].status = "failed" with an errorCode (e.g. media_required, token_expired). When any account failed the post's status is failed, and publish / retry with an accountId retries just that one.