Envelope
All errors follow this minimum shape:
{ "error": "<machine_code>", "message": "<human_readable>" }Some errors include extra fields:
- 403
insufficient_scopeadds"required": [...] - 501
not_implementedadds"ticket": "..."pointing to the work item that will land the handler
Status code → error code reference
| HTTP | error code | When |
|---|---|---|
| 400 | invalid_request | Body validation failed (e.g. webhook topics[] empty, target_url not HTTPS). Fix the request and retry. |
| 401 | invalid_token | Missing/malformed Bearer header, expired access token, install uninstalled or revoked. Refresh; if persistent, re-run install OAuth. |
| 401 | invalid_install | The install record was deactivated mid-request. Re-install. |
| 401 | invalid_client | The AppApiKey behind your install was disabled. Contact the marketplace admin. |
| 403 | tenant_scope_violation | A :orgId or :entityId in the URL doesn't match your token's scope. Fix the URL — never put another tenant's IDs there. |
| 403 | insufficient_scope | The route requires a scope your token doesn't have. Re-install with the additional scope. |
| 429 | rate_limited | Too many requests. See Rate limits. |
| 501 | not_implemented | The endpoint exists in the route table but its handler is not yet shipped. The ticket field references the tracking work item. |
Idempotency and retries
Today, every shipped /public/v1/* endpoint is either a read (idempotent — always safe to retry) or a webhook subscription mutation (treat POST with the same body as create-once at the application layer).
When the curated read+write subset for invoices/bills/customers ships (see 501 stubs in the route table), it will accept an Idempotency-Key header. Until then, do not retry a non-501 POST blindly.