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) |
401 | Missing, invalid, expired or revoked token |
403 | Wrong or missing brand, you lack permission, or the account is unverified |
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 |
503 | Maintenance mode |
Unauthenticated requests
A missing, invalid, expired or revoked token returns 401 with this body:
{
"remark": "unauthenticated",
"status": "error",
"message": { "error": ["Unauthorized request"] }
}const response = await fetch(url, { headers })
if (response.status === 401) {
throw new Error('Postlyra token is invalid or revoked.')
}If you integrated before 2026-08-17
These responses used to carry HTTP 200 with the same body, so clients were told to branch on body.remark === 'unauthenticated' instead of the status code. That check still works (the body is unchanged), but the status is now correct, and response.status === 401 is the check to write. Two sibling responses were corrected in the same change: an unverified account now answers 403 (remark: "unverified") and maintenance mode answers 503 (remark: "maintenance_mode_enabled"), both of which also used to be 200.
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 /invitations (member seats, where pending invitations count) and POST /media/assets (storage). See Entitlements to read your allowances before you hit them.
The Posts API adds a couple of its own structured 422 codes: preflight_failed (a connector rejected the post, with per-account issues) and tier_denied (your token's access level does not cover what you asked for).