Errors
Errors follow the standard shape:
{ "statusCode": 400, "message": "Account with code 1200 already exists", "error": "Bad Request", "code": "ACCOUNT_CODE_ALREADY_EXISTS", "params": { "p0": "1200" }}statusCode, message and error are the stable contract. code is a
machine-readable identifier for the specific failure — branch on it instead
of matching message text, which is English prose and may be reworded.
params carries the values interpolated into the message, positionally
({0} → p0, {1} → p1), so you can render your own localized string.
Both code and params are additive and may be absent:
- Validation failures (class-validator) return
messageas an array of per-field strings and carry nocode; treat a missingcodeas “unmapped”, fall back tostatusCode, and never require the field. paramsappears only when the message interpolates values.
Validation failures return the same shape with message as an array of
per-field problems and no code.
The one exception is the standard OAuth endpoints — POST /oauth/token,
/oauth/revoke, /oauth/introspect, /oauth/device/authorize and
/oauth/register. They answer errors the way the OAuth specifications
define, with the code in error and no statusCode or code: see
OAuth endpoint errors.
OAuth endpoint errors
Section titled “OAuth endpoint errors”POST /oauth/token, /oauth/revoke, /oauth/introspect,
/oauth/device/authorize and /oauth/register return standard OAuth error
responses — RFC 6749 §5.2, which revocation (RFC 7009), introspection
(RFC 7662), the device flow (RFC 8628 §3.5) and client registration
(RFC 7591 §3.2.2) all reuse — so any OAuth library can read them:
HTTP/1.1 400 Bad RequestContent-Type: application/jsonCache-Control: no-store
{ "error": "invalid_grant", "error_description": "refresh token reuse detected"}- Branch on
error. It’s one of the codes in the tables below, and its wording never changes. error_descriptionis optional English text for your logs. Don’t match on it and don’t show it to merchants: it may be reworded, and it’s absent from some errors.- Every error response from these endpoints carries
Cache-Control: no-store(andPragma: no-cache). - The body also carries a deprecated
messagemember — the value these endpoints used to send there — for backward compatibility with older OneBooks clients. It will be removed in a later release: don’t read it or rely on it.
| HTTP | error | Headers |
|---|---|---|
| 400 | Every code not listed below | — |
| 401 | invalid_client — client authentication failed | WWW-Authenticate: Basic realm="OneBooks" when you authenticated with HTTP Basic |
| 429 | temporarily_unavailable — a rate limit refused the request | Retry-After — seconds to wait |
| 5xx | server_error — a failure on our side | — |
A 429 reads:
{ "error": "temporarily_unavailable", "error_description": "Too many requests - retry after 42 seconds"}Wait for Retry-After before the next request to that endpoint. Retry a
5xx with backoff, a few times at most.
Token endpoint
Section titled “Token endpoint”| HTTP | error | Cause |
|---|---|---|
| 400 | invalid_request | A parameter is missing or malformed: grant_type, or what the grant needs — code and redirect_uri for an authorization code, refresh_token, device_code or subject_token, or the code_verifier for a code issued with a PKCE challenge. For token exchange, also a missing or unsupported subject_token_type, or an unsupported requested_token_type. |
| 401 | invalid_client | A missing or unknown client_id, an app OneBooks has suspended or that’s been deactivated, or a missing (confidential client) or wrong client secret — on every grant. For token exchange, also a PUBLIC client (the grant is confidential-only). Repeated failures lock the caller out for a minute. |
| 400 | invalid_grant | The code, refresh token, device code or session token can’t be redeemed — see the table below. |
| 400 | unsupported_grant_type | authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code and urn:ietf:params:oauth:grant-type:token-exchange are the grants available to partner apps. |
| 400 | invalid_target | The RFC 8707 resource parameter isn’t a single absolute URI on this API’s origin. |
| 400 | authorization_pending | Device flow: the user hasn’t approved yet. Keep polling, no faster than the interval you were given. |
| 400 | slow_down | Device flow: you polled faster than interval. Back off before the next poll. |
| 400 | access_denied | Device flow: the user denied the request and the device code is consumed. Stop polling; request a new code to try again. |
| 400 | expired_token | Device flow: the device code’s 10-minute window elapsed. Stop polling, request a new code, show the new user_code. |
invalid_grant always means the same thing to your code — this grant is
dead, so don’t retry it — but its error_description says which check
failed, which helps when you read your logs:
error_description | Cause |
|---|---|
| (none) | Code not found, refresh token not found / not yours, or device code not found or issued to another client — or the authorization behind the grant is gone: the user’s consent was revoked, or the business’s installation of your app is no longer ACTIVE (for example, it uninstalled you — which also cancels codes and approved device codes you haven’t redeemed yet). For token exchange, every failure after client authentication: fetch a fresh session token, and never reuse a subject_token. |
code already used | Authorization codes are single-use. |
expired | Code older than 10 minutes, or refresh token older than 30 days. |
redirect_uri mismatch | redirect_uri at exchange didn’t exactly match the authorize step. |
PKCE verification failed | code_verifier doesn’t match the challenge sent at authorize. |
revoked | Refresh token was revoked (by you, the business, or a family burn). |
refresh token reuse detected | You replayed a used refresh token — the whole token family is now revoked. Re-authorize. See refresh rotation. |
Revocation, introspection, device authorization and registration
Section titled “Revocation, introspection, device authorization and registration”| Endpoint | HTTP | error | Cause |
|---|---|---|---|
/oauth/revoke | 401 | invalid_client | A wrong client secret, or a client_id without a secret that isn’t a usable PUBLIC client. A PUBLIC client may revoke with its client_id alone. An unknown or already-invalid token is not an error: it gets 200. Nor is a token_type_hint other than access_token or refresh_token — it’s ignored, here and on /oauth/introspect. |
/oauth/introspect | 401 | invalid_client | Missing or wrong client credentials. Introspection reads them from the Authorization header only. |
/oauth/device/authorize | 401 | invalid_client | The client didn’t authenticate. It authenticates exactly as at /oauth/token — HTTP Basic, or client_id + client_secret in the body — so this is a missing or unknown client_id, an app OneBooks has suspended or that’s been deactivated, a CONFIDENTIAL client without its secret, or a wrong secret (whatever the client type). Repeated failures lock the caller out of this endpoint for a minute — a lockout of its own, apart from /oauth/token’s. |
/oauth/device/authorize | 400 | invalid_scope | You asked for a scope your app didn’t register in the console. error_description names it. |
/oauth/device/authorize | 400 | invalid_target | The RFC 8707 resource parameter isn’t a single absolute URI on this API’s origin — the same rule as on /oauth/token. |
/oauth/register | 400 | invalid_redirect_uri | redirect_uris isn’t a list of 1 to 5 URIs of at most 512 characters, or one of them isn’t https (loopback http excepted) or has a wildcard, a query string or a fragment. |
/oauth/register | 400 | invalid_client_metadata | Other registration metadata is missing or malformed. |
| Any | 400 | invalid_request | A required parameter is missing or malformed, or the request can’t be read — for example, a malformed or oversized JSON body. On /oauth/register, a registration field that fails validation is invalid_redirect_uri or invalid_client_metadata instead (above), but a request it can’t read is invalid_request there too. |
On the consent screen
Section titled “On the consent screen”Authorization itself runs on OneBooks’ own consent page, not on an endpoint
your app calls. When the request can’t go ahead, the page shows the user
what’s wrong and doesn’t redirect to your app — only a denial comes back, as
?error=access_denied on your redirect URI.
| The page reports | Cause |
|---|---|
Invalid client_id | Unknown client_id, or an app OneBooks has suspended or that’s been deactivated. |
redirect_uri does not match any registered URI | Register the exact URI in the console first. |
Requested scopes not permitted for this client: … | You asked for a scope your app didn’t register. |
PKCE is required for public clients | A PUBLIC client started authorization without a code_challenge. |
invalid_target / invalid_target: resource must be one absolute URI of at most 512 characters | The RFC 8707 resource on your authorization request isn’t one absolute URI on this API’s origin (https://api.getonebooks.com, any path) — the same rule as on /oauth/token. The longer form is for a resource sent more than once or longer than 512 characters. |
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. |
invalid_request: authorization request not found / …expired | The consent step outlived its 5-minute window — restart authorization. |
App platform errors
Section titled “App platform errors”Branch on the code column; the message column is the current English text,
shown so you can recognize it in logs.
| HTTP | code | message | Cause |
|---|---|---|---|
| 429 | APP_RATE_LIMITED | Rate limit exceeded for this app. Retry after the number of seconds in the Retry-After header. | Your app exceeded one of its rate limits. Honor Retry-After. |
| 400 | UNSUPPORTED_API_VERSION | This OneBooks-Version is not supported. See the API versioning guide for the supported versions. | The resolved API version (header, pinned, or current) doesn’t exist or is past its sunset date. |
| 400 | UNKNOWN_APP_DATA_FIELD | Unknown app data field | The key doesn’t match a field your app declared for that resourceType. Declare it on the console’s App data tab first. |
| 400 | INVALID_APP_DATA_VALUE | Invalid app data value | A value broke its field’s type, length, range or choice rules. The response adds details: [{ resourceType?, resourceId?, key, reason }] naming the item that failed and why — read details, not message. |
| 400 | PROVIDE_RESOURCEID_RESOURCEIDS | Provide resourceId or resourceIds | GET /app-data without a resourceId or resourceIds. |
| 400 | RESOURCEIDS_ACCEPTS_MOST_50_IDS | resourceIds accepts at most 50 ids | GET /app-data with more than 50 ids. |
| 403 | APP_CANNOT_ACCESS_TYPE_RECORD | This app cannot access that type of record | Your installation doesn’t hold the read scope App data requires for that resourceType. |
| 403 | APP_DATA_API_AVAILABLE_CONNECTED_APPS_ONLY | The app data API is available to connected apps only | /app-data was called with a OneBooks session cookie instead of an OAuth access token. |
| 404 | RECORD_NOT_FOUND | Record not found | The record you’re writing app data on doesn’t exist in your token’s business. |
| 404 | APP_DATA_VALUE_NOT_FOUND | App data value not found | DELETE /app-data/:id for a value id your app doesn’t have in this business. |
| 400 | UNKNOWN_CURSOR | Unknown cursor | Events API after isn’t an event id in your token’s business, or it aged out of the 30-day window. |
| 400 | UNKNOWN_EVENT_TYPE_TYPES | Unknown event type in “types” | Events API types names an event that isn’t in the catalog. |
| 403 | EVENT_LOG_AVAILABLE_CONNECTED_APPS_ONLY | The event log is available to connected apps only | /events was called with a session cookie instead of an OAuth access token. |
| 404 | EVENT_NOT_FOUND | Event not found | GET /events/:id for an event that doesn’t exist or isn’t visible to your app. |
Like every other endpoint on this API, these carry the standard
{ statusCode, message, code, params } envelope described at the top of this
page. A request body that fails class-validator checks (a values array longer
than 25, say) is the usual array-of-messages 400 with no code.
A token whose app OneBooks has suspended
or deactivated stops working immediately: API calls get 401, every grant on
/oauth/token — refresh and token exchange included — answers
invalid_client, and /oauth/introspect reports the token
{ "active": false }.
Developer console errors
Section titled “Developer console errors”The developer console (/developer/*, used by the console and the CLI) is a
separate, English-only surface with its own codes. The ones you’re most likely
to meet, and where each is explained:
| HTTP | code | Cause |
|---|---|---|
| 409 | LISTING_EXISTS | Two first saves of the same listing raced; the other one created it. Reload the listing and save again. See Publishing your app. |
| 400 | SLUG_TAKEN | Another app already uses that listing slug — including one that claimed it a moment before your save. |
| 400 | ASSET_IN_USE | You tried to delete an icon or screenshot that the live listing or an open revision (draft, submitted, in review, changes requested or rejected) still references. See Assets. |
| 400 | EXTENSION_SCOPE_REQUIRED | An extension on a record needs your app to request that record type’s read scope. See UI extensions. |
| 400 | EXTENSION_PATH_INVALID | The extension path breaks a path rule — for example, it contains a backslash, space or control character, or doesn’t resolve to a page on your App URL’s origin. |
| 400 | INVALID_CHOICES | A CHOICE field’s options aren’t 1–50 distinct, non-empty strings of at most 60 characters — checked after trimming. That includes choices: null when editing a CHOICE field, which would leave it with none. See App data. |
| 400 | FUNCTION_SOURCE_NO_HANDLER | A hosted-function upload isn’t an ES module exporting a handler. See Hosted functions. |
Listing submission has its own set, listed on Publishing your app.
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 on POST /invoices naming a plan limit | The business has reached its plan’s invoice ceiling — the Free plan’s yearly invoice count or turnover cap, or a paid plan’s yearly invoice limit. | Only new invoices are blocked; every other route keeps working. Surface the message so the merchant can pick a plan; don’t retry. API access itself is never plan-gated. See Scopes. |
403 (no insufficient_scope prefix) | The consenting user lacks the permission this operation requires, independently of scope. | Scope and permission are two separate gates. Some endpoints (e.g. journal posting, chart-of-accounts changes, payment deletion) require a permission that, by default, only owner and admin roles hold — so a correctly-scoped token from a member-role user still gets this 403. Permissions are per-business data: a business can widen or narrow what each role holds, so the same call can succeed for one merchant and 403 for another. There’s no token fix — the business must perform the action as a permitted user, or grant the connecting user the permission, and 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.