Errors & limits
Errors are plain JSON with a human-readable English message, the same text an AI sees over MCP, so it can explain or fix the problem by itself.
Error format
{
"error": "That time has already passed",
"code": "date_past",
"hint": "Pick a future time in the project's local time (see now in list_projects)."
}Every error has a stable code (snake_case) to branch on, and some add links. Messages can change; codes don't.
{
"error": "The user's Postika subscription is not active (canceled), so this connection can't read or schedule anything in the project \"Acme\". Tell the user to activate it at https://postika.app/billing; it works again right away, without reconnecting.",
"code": "subscription_inactive",
"billing_url": "https://postika.app/billing",
"project_id": "3f6a1d2e-…",
"subscription_status": "canceled"
}Validation errors also list the fields that failed:
{
"error": "Invalid data",
"issues": [
{
"path": "when",
"message": "Format \"YYYY-MM-DD HH:MM\""
},
{
"path": "channel_ids",
"message": "Too small: expected array to have >=1 items"
}
]
}Status codes
| Status | When |
|---|---|
| 200 | Success. The result is in data. |
| 400 | Invalid data, or the request breaks a rule (past date, too long for a network, unknown channel…). |
| 401 | Missing, unknown or expired credential. Includes a WWW-Authenticate header pointing to the OAuth metadata. |
| 402 | The subscription of the project's owner isn't active (subscription_inactive). Checked per project on every request; /projects and /account still answer. |
| 403 | The role doesn't allow it, e.g. an editor approving (approval_forbidden). |
| 404 | The account has no projects yet (no_projects). |
| 409 | The project has no channels (no_channels) or a channel needs reconnecting (channel_disconnected). |
| 429 | Rate limit exceeded. |
| 500 | Something went wrong on our side. Safe to retry. |
Error messages
| Status | Message | Fix |
|---|---|---|
| 401 | Missing API key (Authorization: Bearer ptk_…) | Send Authorization: Bearer ptk_… (or ptat_…). |
| 401 | Invalid API key | The key was deleted, or the access token expired: refresh it. |
| 402 | …subscription is not active… (subscription_inactive) | Activate it at /billing, or ask the project owner. |
| 429 | Too many requests: wait a minute | Wait Retry-After seconds. |
| 400 | Pass the project (project_id): you have several. Use list_projects to see them. | Pass project_id. |
| 400 | That project doesn't exist or you don't have access | Check the ID with GET /projects. |
| 400 | Pick at least one channel | Send at least one channel_id. |
| 400 | A channel doesn't exist in this project | Use IDs from GET /channels of the same project. |
| 400 | Pick a date and time | Send when for schedule and approval. |
| 400 | That time has already passed | Pick a future time (project local time). |
| 400 | X: maximum 280 characters (you have 312) | Shorten it, or choose another channel. |
| 400 | Instagram needs a photo or video | Send media_urls with a public https link to the photo or video. |
| 400 | media_urls[0] (host) can't be used: … (media_fetch_failed) | Use a public https link straight to a JPG, PNG, WebP, GIF, MP4 or MOV. |
| 400 | The post no longer exists | The post was already deleted. |
| 400 | It's being published right now | Try again in a few seconds. |
| 500 | Something went wrong. Please try again. | Retry with backoff. |
OAuth errors
The OAuth endpoints follow RFC 6749 and return { error, error_description }.
| Error | Status | Meaning |
|---|---|---|
| invalid_request | 400 | code, code_verifier or refresh_token missing. |
| invalid_request | 400 | client_id missing (it is always required). |
| invalid_grant | 400 | Code expired or used, PKCE failed, redirect_uri or client mismatch, refresh token expired, already used or issued to another client. |
| invalid_target | 400 | resource isn't this MCP server's URL (RFC 8707). |
| invalid_client | 401 | Unknown client_id, or wrong or missing client secret for an app with a secret. |
| unsupported_grant_type | 400 | Only authorization_code and refresh_token are supported. |
| invalid_redirect_uri | 400 | Registration: 1 to 5 redirect URIs, each https, loopback or an app scheme. |
| invalid_client_metadata | 400 | Registration: unsupported grant or response types, or metadata too large. |
| temporarily_unavailable | 429 | Registration limit reached (20 per IP per hour). Retry later. |
MCP errors
Only authentication (401, with WWW-Authenticate) and rate limiting (429) are HTTP errors, as a JSON-RPC error with code -32001. Everything else, including an inactive subscription, is a tool result with isError: true and the text Error (<code>): <message>, so the AI can read it, tell the user what to do and correct itself. initialize and tools/list always succeed.
{
"content": [
{
"type": "text",
"text": "Error (too_long): X: maximum 280 characters (you have 312). Shorten the text, or create a separate post for this channel."
}
],
"isError": true
}Rate limits
120 requests per minute for each API key or OAuth access token, counted per clock minute (and 60 per IP for requests without a valid credential). REST and MCP requests share the same budget. Requests over the limit get 429 until the minute rolls over.
Every 429 response carries a Retry-After header with the seconds left until the next minute (1 to 60). Wait that long before retrying; the header is exposed to browsers through CORS.
HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/json
{ "error": "Too many requests: wait a minute" }Limits
| Limit | Value |
|---|---|
| Post content | 1 – 5000 chars |
| Channels per post | ≥ 1 |
| days in GET /posts | 1 – 62 |
| API keys per project | 20 |
| OAuth apps per user | 20 |
| Redirect URIs per client | 5 (registration) · 10 (Developers apps) |
| media_urls per post | 10 · photos 15 MB · videos 100 MB |
| MCP request body | 1 MB |
| Requests without a valid credential | 60 / minute / IP |
| Access token | 1 hour |
| Refresh token | 60 days |
| Authorization code | 5 minutes |
Network limits
Checked when a post is scheduled, published or approved. The tightest channel in a post decides.