API Authentication Guide

API authentication OAuth 2.0 authorization code developer 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 code and state
  • From your server, exchange code for an access token and refresh token with POST 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_typeYesOnly code is supported.
client_idYesYour app's client ID.
redirect_uriYesMust be character-for-character identical to the URI registered for the app.
scopeYesSpace-separated scopes. Only scopes registered for the app can be granted.
stateRecommendedA random string against CSRF. It comes back unchanged on the callback.
code_challengeRequired for public clientsThe PKCE challenge. With S256 it is the Base64URL-encoded SHA-256 of your code_verifier, without trailing =.
code_challenge_methodOptionalUse S256. If omitted, plain is used. Any value other than S256 or plain, including a different case such as s256, returns invalid_request.
promptOptionallogin discards the current session and makes the user sign in again.
identity_providerOptionalSends 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/me and /api/v1/me/organizations return 403. You may omit organizationId on API calls; naming a different business account returns 403.
  • Off: no business account is picked. The token belongs to the user, and you pass organizationId on 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_typeresponse_type is not code
invalid_clientUnknown client_id, or the app is disabled
invalid_redirect_uriredirect_uri does not match the registered value, or the app has no redirect URI
invalid_scopescope is missing, or an app with binding on mixed user.* and other scopes in one request
invalid_requestcode_challenge_method is neither S256 nor plain
app_not_approvedThe app requested user.* scopes before its review was approved, whatever its binding setting
no_organizationAn 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_uri must be the same value as in the authorize request.
  • Put client_secret in the body. HTTP Basic authentication in the Authorization header is not read. Public clients send no client_secret.
  • If the authorize request carried a code_challenge, code_verifier is 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
400invalid_requestThe body cannot be read, or code / refresh_token is missing
400unsupported_grant_typegrant_type is not authorization_code or refresh_token (client_credentials is not supported)
401invalid_clientUnknown client ID, or wrong client secret
401invalid_grantCode 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
Storestore.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
Advertiseradvertiser.campaigns.read advertiser.campaigns.write advertiser.reports.read
Mediamedia.channels.read media.channels.write
CRMcrm.profiles.read crm.segments.read
SNSsns.content.read sns.content.write sns.audience.read
Analyticsanalytics.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_secret on your server only. For apps you distribute, such as mobile apps, turn on "Public client" and use PKCE instead of a secret.
  • Generate state for every request and check it on the callback.
  • Use S256 for PKCE.
  • Do not put access tokens in logs or URLs.
  • Request only the scopes your app actually uses.

Related guides

Published: 2026-03-27 Updated: 2026-10-26