Errors
Errors follow the standard shape:
{ "statusCode": 400, "message": "invalid_grant: code already used", "error": "Bad Request" }Validation failures return the same shape with message as an array of
per-field problems.
OAuth endpoint errors
Section titled “OAuth endpoint errors”Returned by /oauth/token, /oauth/revoke and /oauth/introspect.
| HTTP | message |
Cause |
|---|---|---|
| 401 | invalid_client |
Unknown or inactive client_id, or wrong client secret. |
| 400 | invalid_grant |
Code not found, or refresh token not found / not yours. |
| 400 | invalid_grant: code already used |
Authorization codes are single-use. |
| 400 | invalid_grant: expired |
Code older than 10 minutes, or refresh token older than 30 days. |
| 400 | invalid_grant: redirect_uri mismatch |
redirect_uri at exchange didn’t exactly match the authorize step. |
| 400 | invalid_grant: PKCE verification failed |
code_verifier doesn’t match the challenge sent at authorize. |
| 400 | invalid_grant: revoked |
Refresh token was revoked (by you, the business, or a family burn). |
| 400 | invalid_grant: refresh token reuse detected |
You replayed a used refresh token — the whole token family is now revoked. Re-authorize. See refresh rotation. |
| 400 | PKCE is required for public clients |
Public client attempted the flow without a code challenge/verifier. |
| 400 | unsupported_grant_type |
Only authorization_code and refresh_token are supported. |
| 400 | This app has not completed review and can only connect to its own sandbox businesses |
Unapproved app authorizing against a non-sandbox business. See Go live. |
| 400 | invalid_request: authorization request not found / …expired |
The consent step outlived its 5-minute window — restart authorization. |
API call errors
Section titled “API call errors”| HTTP | Meaning | What to do |
|---|---|---|
| 401 Unauthorized | Access token expired, revoked, or malformed. | Refresh once (single-flight). If the refresh fails with invalid_grant, the connection is dead — send the user through authorization again. |
403 Insufficient scope. Required: … |
Your token lacks a scope the endpoint requires. | Request the missing scope in a new authorization. The consent screen shows the user the added scopes. You cannot widen a live token. |
403 endpoint not exposed to OAuth clients |
The route isn’t part of the OAuth partner surface. | Check the API reference for the supported surface; don’t retry. |
403 (no insufficient_scope prefix) |
The consenting user’s role in the business doesn’t permit this operation. | Some endpoints (e.g. journal posting, chart-of-accounts changes, payment deletion) require the consenting user to hold an owner or admin role in the business, in addition to a valid scope. A correctly-scoped token from a member-role user still gets this 403. There’s no token fix — the business owner must perform the action, or grant the connecting user a higher role, then re-authorize. See Scopes. |
| 400 Bad Request | Validation failure — message lists the offending fields. |
Fix the payload; don’t retry unchanged. |
| 404 Not Found | Resource doesn’t exist in the consenting business. | Remember IDs are per-business: a customerId from one business doesn’t exist in another. |
| 429 Too Many Requests | Rate limited. | Honor Retry-After, back off with jitter. See Rate limits. |
| 5xx | Transient server error. | Retry with exponential backoff and jitter, max ~3 attempts. Use idempotency keys on writes so retries can’t double-post — see integration patterns. |
The decision tree that matters
Section titled “The decision tree that matters”Most integrations only need this:
- 401 on an API call → refresh once → retry once. Refresh failed? → re-authorize.
- 403 → configuration problem (scopes or surface), not a retry problem. Fix the app, not the request loop.
- 429 / 5xx → back off and retry with the same
Idempotency-Key. - 400 → your bug. Log it, alert, don’t retry.
Never log raw tokens while handling errors — log a truncated suffix or hash if you need correlation.