Errors
Errors are JSON. Most carry a message, and validation failures carry a Laravel-shaped errors object.
{
"message": "The given data was invalid.",
"errors": {
"ideas.0.headline": ["The ideas.0.headline field is required."]
}
}Status codes
| Code | Meaning |
|---|---|
200 | Success |
201 | Created (register, media upload, member invite) |
204 | Success, no body (DELETE /device-tokens) |
403 | Wrong or missing brand, or you lack permission |
404 | Not found, or belongs to a brand you are not in |
409 | A setup flow expired (passkey registration, 2FA setup) |
422 | Validation failed, or a plan limit was reached |
429 | Throttled |
500 | Server error |
Expired tokens do not return 401
Read this before writing a retry loop
An unauthenticated request to the API does not return 401. It returns HTTP 200 with this body:
{
"remark": "unauthenticated",
"status": "error",
"message": { "error": ["Unauthorized request"] }
}So a client that only checks response.status will treat a revoked token as a successful call and parse garbage.
Check the body, not just the status:
const response = await fetch(url, { headers })
const body = await response.json()
if (body.remark === 'unauthenticated') {
throw new Error('Postlyra token is invalid or revoked.')
}This is a known quirk rather than a deliberate design. If it changes, it will change to a real 401, so checking for both is the safe thing to write today.
404 versus 403
Resources are scoped to a brand. Asking for a draft, idea or contact that belongs to a different brand returns 404, not 403, because from your side that id does not exist. A 403 means the brand itself was rejected, which almost always means a wrong X-Postlyra-Brand value.
Plan limits
Hitting a plan allowance returns 422, sometimes with a flag:
{ "message": "You have reached your plan limit. Upgrade for more.", "limit_reached": true }You will meet this on POST /users (member seats) and POST /media (storage). See Entitlements to read your allowances before you hit them.