Skip to content

Authentication

OneBooks uses OAuth 2.0 authorization code with PKCE. One authorization = one business: the user who consents determines which business’s data your token can see, and the token carries that binding implicitly.

Type Where it runs Secret PKCE
CONFIDENTIAL Your server Yes — authenticate every token request Recommended
PUBLIC Browser SPA, mobile, desktop None (a shipped secret is a leaked secret) Required

Fetch the RFC 8414 metadata document once at startup instead of hard-coding endpoints:

Terminal window
curl -s https://api.getonebooks.com/.well-known/oauth-authorization-server
{
"issuer": "https://api.getonebooks.com",
"authorization_endpoint": "https://app.getonebooks.com/oauth/authorize",
"token_endpoint": "https://api.getonebooks.com/oauth/token",
"revocation_endpoint": "https://api.getonebooks.com/oauth/revoke",
"introspection_endpoint": "https://api.getonebooks.com/oauth/introspect",
"api_base_url": "https://api.getonebooks.com",
"scopes_supported": ["profile:read", "invoices:read", ""],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256", "plain"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"]
}

Note the authorization endpoint is on the app host (it’s a user-facing consent page), while token/revoke/introspect live on the API host. api_base_url is a non-standard extension pointing at the REST API root.

Generate a fresh PKCE verifier, challenge and CSRF state per authorization attempt, then send the user’s browser to the authorization endpoint:

https://app.getonebooks.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI (URL-encoded)
&scope=invoices%3Awrite%20customers%3Aread (space-separated, URL-encoded)
&state=RANDOM_STATE
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
Parameter Required Notes
response_type Yes Always code.
client_id Yes From the developer console.
redirect_uri Yes Must exactly match a registered URI — scheme, host, port, path, trailing slash.
scope Yes Space-separated subset of your app’s registered scopes.
state Recommended Random CSRF token; echoed back on the callback.
code_challenge Public: required BASE64URL(SHA256(verifier)) for S256.
code_challenge_method With challenge S256 (use it) or plain.

The user signs in to OneBooks (if needed), sees your app name and the requested scopes, and approves or denies. The authorization request is held server-side for 5 minutes — if the user walks away longer than that, restart the flow.

On approval:

YOUR_REDIRECT_URI?code=AUTH_CODE&state=RANDOM_STATE

On denial:

YOUR_REDIRECT_URI?error=access_denied&state=RANDOM_STATE

Before continuing:

  1. Verify state equals the value you generated. Reject the callback if not.
  2. Exchange the code promptly — it is single-use and expires in 10 minutes.

POST /oauth/token is form-encoded. Confidential clients authenticate with HTTP Basic (client_secret_basic) or by putting client_secret in the body (client_secret_post); public clients send client_id in the body with no secret.

Terminal window
curl -s https://api.getonebooks.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=https://yourapp.example.com/callback" \
--data-urlencode "code_verifier=$CODE_VERIFIER"

redirect_uri must match step 1 exactly; code_verifier is the original verifier the challenge was derived from.

{
"access_token": "",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "",
"scope": "invoices:write customers:read"
}

Store tokens server-side, encrypted at rest. For SPAs, keep the access token in memory and the refresh token in an HttpOnly cookie scoped to your own backend — never localStorage. Exchange failures are listed in the error matrix.

Terminal window
curl -s https://api.getonebooks.com/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN"
  • Tenant is implicit. The token is bound to the consenting business. Never send a business ID — there is no header for it.
  • Scopes are enforced per endpoint, deny-by-default. A missing scope gets 403; see Scopes.
Credential Lifetime Notes
Authorization request 5 minutes User must complete consent within this window.
Authorization code 10 minutes Single-use.
Access token 3600 s (1 hour) Refresh before expiry, or react to 401.
Refresh token 30 days Rotated on every use.

Refresh before the access token expires (leave ~60 s of slack for clock skew), or eagerly on a 401:

Terminal window
curl -s https://api.getonebooks.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN"

(Public clients: no -u, send client_id in the body.)

The response is a new access token and a new refresh token. Rotation is strict:

  • The old refresh token is marked used and cannot be used again.
  • The old access token is revoked immediately — not left to age out.
  • Persist both new tokens atomically before using either.

If a refresh returns 400 invalid_grant (expired after 30 days, revoked, or family-burned), send the user through authorization again.

Invalidate a token you no longer need (e.g. the user disconnected your app on your side):

Terminal window
curl -s https://api.getonebooks.com/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "token=$REFRESH_TOKEN" \
--data-urlencode "token_type_hint=refresh_token"

Returns 200 even if the token was already invalid (RFC 7009). The business can also revoke your app’s access from inside OneBooks at any time — your next API call returns 401; treat that as “needs re-authorization”.

Debugging aid, not a production hot path — just call the API and handle 401:

Terminal window
curl -s https://api.getonebooks.com/oauth/introspect \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "token=$ACCESS_TOKEN"
{ "active": true, "scope": "invoices:write customers:read", "exp": 1765465600, "client_id": "", "sub": "" }

or { "active": false }.

  • Never embed a client secret in browser, mobile or desktop builds.
  • Generate a fresh code_verifier and state per attempt; never reuse them.
  • Never log raw tokens — log a truncated suffix or hash for correlation.
  • One client_id serves all your customers; each business gets its own token set after its own consent. Don’t conflate your app with their tenant.