Cannot obtain a token (authentication error)
How to narrow down the cause when the token endpoint POST https://receiptroller.io/api/v1/auth/token returns 400 or 401 and you cannot obtain or refresh an access token. Match the error and error_description you received against the table below. If the error happens earlier, on the authorize URL, see the second table.
Token endpoint errors
| HTTP / error | error_description | Cause and fix |
|---|---|---|
400 invalid_request | none, code is required, refresh_token is required | The body could not be read as JSON, or a required value is missing. Check Content-Type and the body format. |
400 unsupported_grant_type | none | grant_type is empty or is not authorization_code / refresh_token. client_credentials is not supported. |
401 invalid_client | client authentication failed | The client ID does not exist (including when the app was deleted), or the client secret is wrong. After regenerating the secret, use the new value. |
401 invalid_grant | authorization code is invalid, expired, or already used | The code's 10 minutes have passed, it was already exchanged, or the value is wrong. Start authorization again. |
401 invalid_grant | redirect_uri does not match the authorization request | The redirect_uri in the exchange differs from the one in the authorize request. |
401 invalid_grant | authorization code was issued to a different client | You are exchanging with another app's client ID. Check you have not mixed up your development and production apps. |
401 invalid_grant | public clients require PKCE; the code was issued without a code_challenge | A public client authorized without code_challenge. Authorize again with PKCE. |
401 invalid_grant | code_verifier is required / PKCE verification failed | code_verifier is missing or does not match code_challenge. With S256, Base64URL-encode the SHA-256 (+ to -, / to _, no trailing =). |
401 invalid_grant | the business account's plan does not include the developer API; it needs Starter or above | The connected business account is on a plan below Starter. The code is not marked as used, so if the plan changes within the 10 minutes, the same code can still be exchanged. |
401 invalid_grant | refresh token is invalid, expired, or revoked | The refresh token is past its 30 days, was already used for a refresh, or was revoked. Use the most recent refresh token you received; if that fails, authorize again. |
401 invalid_grant | refresh token was issued to a different client | The refresh token belongs to another app. |
invalid_grant is returned with 401, not 400. Decide on error, not on the status code alone.
When it stops at the authorize URL
These errors on /oauth/authorize do not redirect; the browser shows a 400 JSON body.
| error | What to do |
|---|---|
invalid_redirect_uri | Register a redirect URI on the app and make redirect_uri match it exactly (Setting the redirect URL). |
invalid_client | Check client_id. It is also returned when the app is disabled. |
unsupported_response_type | Use response_type=code. |
invalid_scope | Send scope, and do not mix user.* and other scopes in one request. |
invalid_request | Set code_challenge_method to S256 (recommended) or plain, in exactly that case. |
app_not_approved | user.* scopes need the app's review to be approved (Applying for review of User-scopes). |
no_organization | The signed-in user owns no business account. Authorize with the owner's account. |
If the callback comes back with error=access_denied, the user denied, or unticked every scope (error_description=no_scopes_approved).
Checklist
- Have you mixed up the client ID or secret between your development and production apps?
- Was the secret regenerated recently?
- Do you exchange the code within 10 minutes, and only once?
- Is
redirect_urithe same string as in the authorize request (compare the original string, not the encoded one)? - Are
client_idandclient_secretin the body? HTTP Basic authentication in theAuthorizationheader is not read. - Are the parameters in the body rather than the query string?
- For a public client, did you authorize with
code_challengeand sendcode_verifier? - For a refresh, are you using the most recent refresh token?
Body format
A form-encoded body (application/x-www-form-urlencoded, snake_case keys) is recommended. JSON is accepted as well, with snake_case keys such as grant_type or camelCase keys such as grantType. A body that is not valid JSON returns 400 invalid_request.
Related guides
-
403/401 is returned (permissions and scopes)How to isolate the cause when an API call returns 401 or 403. Explains the typical cases: invalid token, insufficient scope, no store access permission, and User-scope review not passed.
-
The developer portal is not shown while on the Free planWhat to check when the ReceiptRoller developer portal is not shown. Explains what to do when your plan is Free, switching business accounts, and the timing of when the display updates.
-
Webhook is not being deliveredWhen a webhook does not arrive: narrow it down with a test send and the delivery log, then check the active flag, subscribed events, connection and signature.
-
app_not_approved is returned for User-scope callsWhat to check when using a User-scope results in an app_not_approved error. Explains development with a sandbox slot, applying for review, and the flow from sandbox to production.