API Authentication Guide
The ReceiptRoller API uses the OAuth 2.0 authorization code flow. A developer registers an app, the owner of a business account approves it on the consent screen, and the app exchanges the authorization code for an access token and calls the API. This article covers the authorize URL and its parameters, how the consent screen behaves, exchanging and revoking tokens, the errors you can get, and the scopes you can request. To try it end to end first, start with Your first API call.
The flow at a glance
- Register a developer app and receive a client ID and client secret
- Send the user's browser to
https://receiptroller.io/oauth/authorize - When the user presses Approve, the browser returns to your registered redirect URI with
codeandstate - From your server, exchange
codefor an access token and refresh token withPOST https://receiptroller.io/api/v1/auth/token - Call the API with
Authorization: Bearer {access token} - Refresh with the refresh token before the access token runs out
Register an app
Apps are registered by the owner of a business account, and the business account's plan must be Starter or above. For the steps and fields, see Creating a new app. An app has exactly one redirect URI.
Send the user to the authorize URL
GET https://receiptroller.io/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
&scope=store.products.read%20store.orders.read
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Only code is supported. |
client_id | Yes | Your app's client ID. |
redirect_uri | Yes | Must be character-for-character identical to the URI registered for the app. |
scope | Yes | Space-separated scopes. Only scopes registered for the app can be granted. |
state | Recommended | A random string against CSRF. It comes back unchanged on the callback. |
code_challenge | Required for public clients | The PKCE challenge. With S256 it is the Base64URL-encoded SHA-256 of your code_verifier, without trailing =. |
code_challenge_method | Optional | Use S256. If omitted, plain is used. Any value other than S256 or plain, including a different case such as s256, returns invalid_request. |
prompt | Optional | login discards the current session and makes the user sign in again. |
identity_provider | Optional | Sends a signed-out user straight to one sign-in method, skipping the sign-in page (google, microsoft, apple, line, smaregi). acr_values=idp:google works too. |
A signed-out user is sent to the ReceiptRoller sign-in page first and returns to the consent screen afterwards.
The consent screen and choosing a business account
The consent screen depends on the app's "Bind the access token to one business account at authorize time" setting.
- On (the default): the user picks one business account on the consent screen. Only business accounts the signed-in user owns are offered. The token is bound to that business account and carries no person, so
/api/v1/meand/api/v1/me/organizationsreturn 403. You may omitorganizationIdon API calls; naming a different business account returns 403. - Off: no business account is picked. The token belongs to the user, and you pass
organizationIdon every API call. The server checks on each call that the user is a member of that business account. This is for mobile apps that switch between business accounts (Guide for native mobile apps).
The requested scopes are listed with their boxes ticked, and the user can untick any of them. A business account on a plan below Starter is marked as needing a plan change and cannot be approved. If the user has already approved the same app for the same scopes, the browser is redirected straight back without showing the consent screen.
Receive the callback
On approval, the browser returns to your registered redirect URI:
https://example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE
If the user denies, or unticks every scope, it returns like this:
https://example.com/callback?error=access_denied&state=RANDOM_STATE
https://example.com/callback?error=access_denied&error_description=no_scopes_approved&state=RANDOM_STATE
An authorization code is valid for 10 minutes and can be used once. Always check that state matches the value you sent.
Errors on the authorize URL
These errors do not redirect. The browser shows an HTTP 400 JSON body with error (and sometimes error_description).
| error | Cause |
|---|---|
unsupported_response_type | response_type is not code |
invalid_client | Unknown client_id, or the app is disabled |
invalid_redirect_uri | redirect_uri does not match the registered value, or the app has no redirect URI |
invalid_scope | scope is missing, or an app with binding on mixed user.* and other scopes in one request |
invalid_request | code_challenge_method is neither S256 nor plain |
app_not_approved | The app requested user.* scopes before its review was approved, whatever its binding setting |
no_organization | An app with binding on, and the signed-in user owns no business account |
Exchange the code for tokens
Send this from your server. A form-encoded body (application/x-www-form-urlencoded) is recommended; JSON is accepted too.
POST https://receiptroller.io/api/v1/auth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&code_verifier=CODE_VERIFIER
redirect_urimust be the same value as in the authorize request.- Put
client_secretin the body. HTTP Basic authentication in theAuthorizationheader is not read. Public clients send noclient_secret. - If the authorize request carried a
code_challenge,code_verifieris required. A public client cannot exchange a code issued without PKCE.
On success you get 200:
{
"access_token": "…",
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 28800,
"scope": "store.products.read store.orders.read",
"accessToken": "…",
"refreshToken": "…",
"tokenType": "Bearer",
"expiresIn": 28800
}
The camelCase keys such as accessToken repeat the same values for older clients. New apps should read the snake_case keys. scope holds the scopes actually granted, so compare it with what you asked for. Lifetimes and refreshing are covered in Obtaining and refreshing an access token.
Token endpoint errors
| HTTP | error | Cause |
|---|---|---|
| 400 | invalid_request | The body cannot be read, or code / refresh_token is missing |
| 400 | unsupported_grant_type | grant_type is not authorization_code or refresh_token (client_credentials is not supported) |
| 401 | invalid_client | Unknown client ID, or wrong client secret |
| 401 | invalid_grant | Code invalid, expired or already used; redirect_uri mismatch; code issued to another app; PKCE failure; refresh token invalid or expired; business account's plan below Starter |
invalid_grant is returned with 401 as well. Read error_description for the reason. For a step-by-step check, see Cannot obtain a token (authentication error).
Call the API
GET https://receiptroller.io/api/v1/products
Authorization: Bearer ACCESS_TOKEN
Apps with binding off add ?organizationId=. A missing, invalid or expired token returns 401; a token without the required scope returns 403.
Check a token
POST https://receiptroller.io/api/v1/auth/verify
Content-Type: application/json
{ "accessToken": "ACCESS_TOKEN" }
A valid token returns 200 with active, clientId, scope, expiresAt and organizationId (null for a token with binding off). An invalid one returns 401.
Revoke a token
POST https://receiptroller.io/api/v1/auth/revoke
Content-Type: application/json
{ "token": "ACCESS_OR_REFRESH_TOKEN", "clientId": "YOUR_CLIENT_ID" }
Passing either the access token or the refresh token revokes the pair. Success returns 200 with {"revoked": true}; a token that does not exist or belongs to another app returns 400 with {"error": "invalid_token"}. The owner of a business account can also disconnect an app from "Connected apps" in the business account's menu, which revokes every token the app holds for that business account.
Scopes
Request what you need from the scopes registered for your app. user.* scopes require the app to pass review (Applying for review of User-scopes).
| Category | Scopes |
|---|---|
| Store | store.products.read store.products.write store.orders.read store.orders.write store.customers.read store.customers.write store.coupons.read store.coupons.write store.coupons.redeem store.inventory.read store.inventory.write store.flyers.read store.flyers.write store.posts.read store.posts.write store.staff.read store.staff.write store.business-hours.read store.business-hours.write store.reservations.read store.reservations.write store.memberships.read store.memberships.write store.passes.read store.passes.write store.terminal.write store.timecards.read store.timecards.write store.info.read store.info.write store.media.read store.media.write |
| Advertiser | advertiser.campaigns.read advertiser.campaigns.write advertiser.reports.read |
| Media | media.channels.read media.channels.write |
| CRM | crm.profiles.read crm.segments.read |
| SNS | sns.content.read sns.content.write sns.audience.read |
| Analytics | analytics.read sales.read |
| User (review required) | user.profile.read user.profile.write user.receipts.read user.receipts.write user.spending.read user.spending.write user.coupons.read user.coupons.write user.favorites.read user.favorites.write |
For what each scope allows, see OAuth Scopes Reference and Availability.
Security points
- Keep
client_secreton your server only. For apps you distribute, such as mobile apps, turn on "Public client" and use PKCE instead of a secret. - Generate
statefor every request and check it on the callback. - Use
S256for PKCE. - Do not put access tokens in logs or URLs.
- Request only the scopes your app actually uses.