Every error from /api/v2 is a problem document (RFC 9457), sent as application/problem+json. This page lists every problem type with its causes and the fix, and says which errors are worth retrying.
{
"type": "https://developers.plnnrly.com/api/v2/problems/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "3 fields failed validation.",
"instance": "/api/v2/worklogs",
"request_id": "01K4B7Q9YV3PZ2N8T1E6M0XR4S",
"errors": [
{ "pointer": "/date", "code": "date_format", "detail": "The date does not match the format Y-m-d." },
{ "pointer": "/minutes", "code": "min", "detail": "The minutes must be at least 1." },
{ "pointer": "/description", "code": "required", "detail": "The description field is required." }
]
}
Reading a problem document
| Member | What it holds |
|---|---|
type | The problem type. Switch on this. Its last segment is the slug this page uses as an anchor. |
title | A short name, fixed per type. Do not parse it. |
status | The HTTP status, repeated in the body. |
detail | An explanation for a person. It may name values from your request. Do not parse it. |
instance | The request path, without the query string. |
request_id | The same id as the X-Request-Id response header. Send it when you contact support. |
Some types add members of their own. They are listed with each type below, and the problems schema documents every member field by field.
Switch on type, not on the status code. Several problem types share 403, and several share 409, each with a different fix.
The type values are stable identifiers. Open one in a browser and it lands on its entry on this page.
Language. title, detail and the detail of every errors entry are always in English, whatever language your organisation or your user account uses.
Over MCP. The api_read and api_write tools return the same problem documents, as tool errors.
Problem types at a glance
There are 26 problem types, plus the ones only the file upload sends, listed at the end of this page.
| Status | Problem types | What to do |
|---|---|---|
400 | invalid-query-parameter, unsupported-parameter, invalid-cursor, malformed-request, idempotency-key-required | Fix the request. Never send it again unchanged. |
401 | unauthenticated, token-expired | Check the token and the host. |
403 | insufficient-token-scope, insufficient-permission, forbidden-by-policy | Read the type: a token fix, a role fix, or a record you may not touch. |
404 | endpoint-not-found, resource-not-found | Tell "nothing answers here" apart from "no such record". |
405 | method-not-allowed | Use a method from the Allow header. |
406 | not-acceptable | Leave Accept out, or allow application. |
408 | request-timeout | File upload only. Send the file again. |
409 | duplicate-resource, state-conflict, idempotency-key-reused, idempotency-key-in-flight | Depends on the type: some are about the record, others about the idempotency key. |
410 | endpoint-retired | Move to the replacement in replaced_by. |
412 | precondition-failed | Re-read, re-apply your change, and retry. |
413 | payload-too-large | File upload only. The file is too large. |
415 | unsupported-media-type | Send Content-Type: . |
422 | validation-failed, unprocessable-relationship | Fix the body. errors points at each fault. |
429 | rate-limit-exceeded | Wait the seconds in Retry-After. |
500 | internal-error | Retry if it is safe, then contact support. |
503 | tenant-unavailable | Retry a few times. If it persists, ask an administrator. |
400 Bad Request
Invalid query parameter
A query parameter is wrong in one of two ways:
- Its name is unknown: a typo, or a parameter this API does not have, such as
?limit=or?version=. - Its name is known, but it is used wrongly here: a bad value, a filter, operator, sort key or include name this resource does not declare, too many values, or a collection-only parameter such as
sortsent to a single record.
Carries errors, one entry per fault, each with the parameter and a code. The detail of an entry lists what is allowed.
No query parameter is ever silently ignored. A misspelt filter is refused, so it can never widen your results without you noticing.
Unsupported parameter
The parameter is one this API knows, but this operation never accepts it:
fields: there are no sparse fieldsets. Chooseview=summaryorview=detailinstead.url_ttl_minutes: reserved, and accepted on no operation.trashedon a resource that does not archive,forceoutside aDELETE,qorincludewhere the resource has none,metaon a resource without actions,includeorviewon aDELETE, andwith_totalon a resource that always or never counts.
Carries errors.
Faults are checked in stages, so one response may not name every fault in your request. An unknown name is reported first. Fix what is reported, and send the request again.
Body keys are not query parameters. An unknown key in a request body is validation-failed.
Invalid cursor
The cursor could not be decoded, or it was issued for a different sort, filter set, search (q), trashed or view. Changing per_page does not invalidate a cursor.
Start again from the first page. Always follow links.next, and never build or edit a cursor yourself. See Pagination.
Malformed request
The request could not be read at all. You meet this type in two places:
- The file upload, when you send the bytes to an
upload_url: the body could not be read, or its length disagrees withContent-Length. - MCP: an
api_readorapi_writecall whose path or query cannot form a request. Itserrorsuse the codesinvalid,too_manyandtoo_deep.
A JSON body that does not parse is not this type. It is read as an empty body: see Write-body faults.
Idempotency key required
This POST refuses to run without an Idempotency-Key header. 3 operations require one, and each says so on its reference page. Add a key and send the request again. See Idempotency.
401 Unauthorized
Every 401 from /api/v2 comes with the header WWW-Authenticate: Bearer realm="plnnrly-api".
Unauthenticated
The request carries no usable token. The causes:
- no
Authorizationheader, or one that is notBearer <token> - a malformed or unknown token
- a revoked token
- an expired token
- a token from another organisation: a token works only on the address of the organisation it was created in
Check the host and the token. If the token expired or was revoked, create a new one, as API tokens explains. An app that signs in with OAuth refreshes its access token instead, see Sign in with OAuth.
Token expired
Declared in the contract, not sent today: an expired token answers unauthenticated. Handle both types the same way. If this type is ever sent, it carries expired_at.
403 Forbidden
Several types share this status, and each needs a different fix. Always read the type.
Insufficient token scope
The token is valid, but its scopes do not cover this operation. required_ability lists the abilities, any one of which would have worked, such as ["api:read:project_drive", "api:read:customer_drive"].
Create a token that has one of them. The user may well hold the permission already. An app that signs in with OAuth asks for the scope when the user signs in. See Scopes and permissions.
Insufficient permission
The token is broad enough, but the user's role lacks a plnnrly permission. required_permission names it, such as projects::read.
An administrator has to change the role. No token can work around it. You also get this type when you write a field that needs an extra permission, or name another user in a user filter such as filter[user_id].
Forbidden by policy
The token and the role are fine, but this record refuses this caller, for example because of who owns it. There is no extra member: the detail says why.
Over MCP, a delete while Allow destructive actions is off is refused with this type. See Permissions and safety.
404 Not Found
Endpoint not found
Nothing answers this request here. The causes, with the detail you see:
| Cause | detail |
|---|---|
| No route matches the path. Check it against the reference. | No API endpoint matches this request. |
The host is wrong or old: a typo, a former address, or the old plnnrly.com address of an organisation that now uses its own domain. Use the address you sign in on. | No API is served on this host. |
| An administrator has switched the REST API off. Every request answers this, whatever the token. | The REST API is not enabled for this organisation. |
| The endpoint belongs to an area your organisation does not use, such as billing. | This endpoint is not available on this organisation. |
Who can switch the REST API on is described in For administrators.
Resource not found
The endpoint exists, but the record does not, as far as you can tell. It does not exist, it is archived and trashed leaves it out, or you may not know that it exists.
A record you may not see answers 404, not 403, so its existence does not leak. To read an archived record, add trashed=with where the operation accepts it.
405 Method Not Allowed
Method not allowed
The path exists, but not with this method. The Allow header lists the methods it accepts.
There is no PUT anywhere in this API. Change a record with PATCH, and send only the fields you change.
406 Not Acceptable
Not acceptable
The Accept header rules out JSON. Leave the header out, or include application/json or */*. A partial wildcard such as application/* is refused.
409 Conflict
Some of these types are about the record, and the others are about the idempotency key.
Duplicate resource
Another record already uses this value. conflict.field and conflict.value name it. Choose another value.
State conflict
The request is valid, but the record's current state does not allow it. Examples: a workflow transition that is not allowed, starting a sprint that has already started, or deleting a project with logged time.
conflict.reason is always present and says which. The other conflict keys depend on it:
{
"type": "https://developers.plnnrly.com/api/v2/problems/state-conflict",
"title": "State conflict",
"status": 409,
"detail": "The target status is not reachable from this work item's current status.",
"instance": "/api/v2/work-items/812/actions/transition",
"request_id": "01K4B7Q9YV3PZ2N8T1E6M0XR4S",
"conflict": {
"reason": "illegal_transition",
"from_workflow_status_id": 3,
"to_workflow_status_id": 9
}
}
Actions and state changes lists the reasons each action raises, and Deleting and restoring covers what blocks a delete. The problems schema lists every reason.
Idempotency key reused
The Idempotency-Key was already used for a different request: another body, or another path. A key belongs to the request it first succeeded with. Use a new key for a new request.
Idempotency key in flight
The first request with this key has not finished yet. The response carries Retry-After: 1. Wait, then send the same request with the same key. If the first request succeeded, you get its stored response. If it failed, your retry runs as a new request.
410 Gone
Endpoint retired
The endpoint has passed its sunset date and no longer answers. sunset is the date it stopped working. replaced_by names the route that replaces it, such as api.v2.projects.show, or is null.
No endpoint is retired today. An endpoint is deprecated before it retires, and says so in its response headers: see Headers and versioning.
412 Precondition Failed
Precondition failed
Your If-Match no longer matches, because the record changed since you read it. The response carries the record's current ETag header. It also carries current_version, which is set on work items and null on everything else.
Re-read the record, apply your change again, and retry with the new ETag. Do not resend your old change with the new tag: that overwrites exactly the change the check caught. See Concurrency and ETags.
415 Unsupported Media Type
Unsupported media type
A request body arrived with a Content-Type other than application/json. A charset parameter is fine. There is no form encoding and no multipart on /api/v2. Files go through the upload flow, which sends the bytes in a separate request: see Files: upload and download.
422 Unprocessable Content
Validation failed
The body was read, and some of it is wrong. errors has one entry per fault, each with a pointer to the field and a code. Common cases:
- a field that is missing or in the wrong format, such as
requiredordate_format - a body key the operation does not accept:
prohibited, see Write-body faults - an id that does not exist, or that you cannot see:
exists - a JSON body that does not parse: it is read as an empty body, so it usually fails as missing fields or as an empty
PATCH, see Write-body faults - an
Idempotency-Keylonger than 255 characters: the entry hasparameterset toIdempotency-Keyand the codemax
Unprocessable relationship
A record you refer to exists, but it cannot be used here. Examples: a sprint on another project, or one that is already completed; a folder in another drive; a work item linked to itself; an upload ticket created for another record. Carries errors.
An id that does not exist, or that you cannot see, is validation-failed with the code exists instead. The one exception is after_work_item_id on the move action, which reports it here.
429 Too Many Requests
Rate limit exceeded
A rate budget is used up: your token's, your organisation's, or the per-IP limit that applies before the token is checked. rate_limit.limit is the ceiling that ran out. rate_limit.retry_after and the Retry-After header give the seconds to wait. See Rate limits.
The file upload sends this type too, when more than three uploads are in flight for one token. That response carries Retry-After: 5 and no rate_limit member.
500 Internal Server Error
Internal error
Something failed on our side. The detail is fixed and says nothing about the cause, but the request_id finds the whole request in our logs. See Retrying, then Contacting support.
503 Service Unavailable
Tenant unavailable
Your organisation cannot answer requests right now. It is still being set up, it is suspended or archived, or the service is down for maintenance. The response carries Retry-After, which is 30 seconds when the organisation itself is unavailable.
Retry a few times. A suspended or archived organisation does not recover on its own, so stop retrying if it persists and contact an administrator.
Problem types from the file upload
When you send a file's bytes to its upload_url, most answers are problem documents with only type, title, status and detail, without instance or request_id. A 401, an expired or changed upload_url and the per-minute upload limit answer with a plain JSON message instead, so switch on the status code there. The types below appear only on this URL. In each case the upload ticket stays open: see Files: upload and download.
Payload too large
413. The file is larger than 10 MB, according to its Content-Length or to the bytes that arrived. A file this size cannot be uploaded.
Request timeout
408. No data arrived for 30 seconds, or the upload took longer than 120 seconds. Send the file again while the ticket is still valid.
The errors array
These types carry an errors array: invalid-query-parameter, unsupported-parameter, validation-failed and unprocessable-relationship. Over MCP, malformed-request carries one too. Each entry describes one fault and has exactly one of pointer or parameter.
| Field | Meaning |
|---|---|
pointer | An RFC 6901 pointer into the request body, such as /minutes or /label_ids. |
parameter | The query parameter, brackets included, such as filter[start_date][gte]. |
code | The machine-readable fault. Switch on this. |
detail | An explanation of this one fault, for a person. |
The code values:
unknown_parameterunsupported_parameterunknown_filterunknown_operatorunknown_sort_keyunknown_include_pathdepth_exceededcount_exceededout_of_rangeinvalid_formatpaired_filter_missingmutually_exclusiverequiredintegernumericstringbooleanarrayminmaxbetweensizedistinctdate_formatdecimalemailurluuidregexafterafter_or_equalbeforeexistsuniquerequired_withprohibitedin
A validation rule that is not in this list reports its own name in snake case. New codes may be added, so treat an unknown code as unhandled and show its detail, rather than failing.
Write-body faults
Request bodies are closed: every key you send must be one the operation accepts. What happens when a body is wrong:
| You send | You get |
|---|---|
A key the operation does not accept, such as id or updated_at | 422 , code prohibited, one entry per key at /<key>. The detail lists the writable fields. |
A PATCH with no fields | 422 , code required at the pointer /. |
| A body that is not valid JSON | It is read as an empty body. Most operations then answer the 422 for missing fields, or for an empty PATCH. An operation with no required field, such as POST , succeeds as if you had sent {}. Make sure your client sends valid JSON. |
| A field that needs a permission you lack | Either 403 , with a detail such as Writing , or the field is dropped and the rest of the write is applied, with an input_ignored warning. Each field's description in the reference says which. |
A body with another Content-Type | 415 . |
Warnings are not errors
A 2xx response may carry meta.warnings. A warning never changes the status code. Read the warnings on every write: an input_ignored warning means a field you sent was not saved. See Truncation for the include_truncated warning.
Retrying
Retry only what is safe to retry, and back off between attempts.
| Response | Retry? | Wait |
|---|---|---|
429 | Yes. | The seconds in Retry-After. |
503 | Yes, a few times. A suspended or archived organisation does not recover on its own. | The seconds in Retry-After. |
409 idempotency-key-in-flight | Yes, with the same key and body. | 1 second. |
412 | Only after re-reading and re-applying your change. | No wait needed. |
500, or no answer at all | A GET: yes. A POST with an Idempotency-Key: yes, with the same key. A POST without a key: do not send it again until you have checked whether it went through, for example by reading the collection. A PATCH or DELETE: read the record first to see whether it went through. | Back off, and stop after a few attempts. |
| Anything else | No. Fix the request first. | Does not apply. |
Contacting support
If an error does not make sense, contact plnnrly support and send:
- the
request_idfrom the response - your own
X-Request-Id, if you sent one - the time of the request, in UTC
- the operation: its method and path
Never send your token.