Skip to content

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 message as an array of per-field strings and carry no code; treat a missing code as “unmapped”, fall back to statusCode, and never require the field.
  • params appears 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.

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 Request
Content-Type: application/json
Cache-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_description is 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 (and Pragma: no-cache).
  • The body also carries a deprecated message member — 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.
HTTPerrorHeaders
400Every code not listed below—
401invalid_client — client authentication failedWWW-Authenticate: Basic realm="OneBooks" when you authenticated with HTTP Basic
429temporarily_unavailable — a rate limit refused the requestRetry-After — seconds to wait
5xxserver_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.

HTTPerrorCause
400invalid_requestA 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.
401invalid_clientA 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.
400invalid_grantThe code, refresh token, device code or session token can’t be redeemed — see the table below.
400unsupported_grant_typeauthorization_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.
400invalid_targetThe RFC 8707 resource parameter isn’t a single absolute URI on this API’s origin.
400authorization_pendingDevice flow: the user hasn’t approved yet. Keep polling, no faster than the interval you were given.
400slow_downDevice flow: you polled faster than interval. Back off before the next poll.
400access_deniedDevice flow: the user denied the request and the device code is consumed. Stop polling; request a new code to try again.
400expired_tokenDevice 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_descriptionCause
(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 usedAuthorization codes are single-use.
expiredCode older than 10 minutes, or refresh token older than 30 days.
redirect_uri mismatchredirect_uri at exchange didn’t exactly match the authorize step.
PKCE verification failedcode_verifier doesn’t match the challenge sent at authorize.
revokedRefresh token was revoked (by you, the business, or a family burn).
refresh token reuse detectedYou 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”
EndpointHTTPerrorCause
/oauth/revoke401invalid_clientA 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/introspect401invalid_clientMissing or wrong client credentials. Introspection reads them from the Authorization header only.
/oauth/device/authorize401invalid_clientThe 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/authorize400invalid_scopeYou asked for a scope your app didn’t register in the console. error_description names it.
/oauth/device/authorize400invalid_targetThe RFC 8707 resource parameter isn’t a single absolute URI on this API’s origin — the same rule as on /oauth/token.
/oauth/register400invalid_redirect_uriredirect_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/register400invalid_client_metadataOther registration metadata is missing or malformed.
Any400invalid_requestA 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.

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 reportsCause
Invalid client_idUnknown client_id, or an app OneBooks has suspended or that’s been deactivated.
redirect_uri does not match any registered URIRegister 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 clientsA PUBLIC client started authorization without a code_challenge.
invalid_target / invalid_target: resource must be one absolute URI of at most 512 charactersThe 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 businessesUnapproved app authorizing against a non-sandbox business. See Go live.
invalid_request: authorization request not found / …expiredThe consent step outlived its 5-minute window — restart authorization.

Branch on the code column; the message column is the current English text, shown so you can recognize it in logs.

HTTPcodemessageCause
429APP_RATE_LIMITEDRate 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.
400UNSUPPORTED_API_VERSIONThis 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.
400UNKNOWN_APP_DATA_FIELDUnknown app data fieldThe key doesn’t match a field your app declared for that resourceType. Declare it on the console’s App data tab first.
400INVALID_APP_DATA_VALUEInvalid app data valueA 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.
400PROVIDE_RESOURCEID_RESOURCEIDSProvide resourceId or resourceIdsGET /app-data without a resourceId or resourceIds.
400RESOURCEIDS_ACCEPTS_MOST_50_IDSresourceIds accepts at most 50 idsGET /app-data with more than 50 ids.
403APP_CANNOT_ACCESS_TYPE_RECORDThis app cannot access that type of recordYour installation doesn’t hold the read scope App data requires for that resourceType.
403APP_DATA_API_AVAILABLE_CONNECTED_APPS_ONLYThe app data API is available to connected apps only/app-data was called with a OneBooks session cookie instead of an OAuth access token.
404RECORD_NOT_FOUNDRecord not foundThe record you’re writing app data on doesn’t exist in your token’s business.
404APP_DATA_VALUE_NOT_FOUNDApp data value not foundDELETE /app-data/:id for a value id your app doesn’t have in this business.
400UNKNOWN_CURSORUnknown cursorEvents API after isn’t an event id in your token’s business, or it aged out of the 30-day window.
400UNKNOWN_EVENT_TYPE_TYPESUnknown event type in “types”Events API types names an event that isn’t in the catalog.
403EVENT_LOG_AVAILABLE_CONNECTED_APPS_ONLYThe event log is available to connected apps only/events was called with a session cookie instead of an OAuth access token.
404EVENT_NOT_FOUNDEvent not foundGET /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 }.

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:

HTTPcodeCause
409LISTING_EXISTSTwo first saves of the same listing raced; the other one created it. Reload the listing and save again. See Publishing your app.
400SLUG_TAKENAnother app already uses that listing slug — including one that claimed it a moment before your save.
400ASSET_IN_USEYou 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.
400EXTENSION_SCOPE_REQUIREDAn extension on a record needs your app to request that record type’s read scope. See UI extensions.
400EXTENSION_PATH_INVALIDThe 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.
400INVALID_CHOICESA 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.
400FUNCTION_SOURCE_NO_HANDLERA 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.

HTTPMeaningWhat to do
401 UnauthorizedAccess 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 clientsThe 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 limitThe 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 RequestValidation failure — message lists the offending fields.Fix the payload; don’t retry unchanged.
404 Not FoundResource 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 RequestsRate limited.Honor Retry-After, back off with jitter. See Rate limits.
5xxTransient 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.

Most integrations only need this:

  1. 401 on an API call → refresh once → retry once. Refresh failed? → re-authorize.
  2. 403 → configuration problem (scopes or surface), not a retry problem. Fix the app, not the request loop.
  3. 429 / 5xx → back off and retry with the same Idempotency-Key.
  4. 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.