Session tokens
A session token proves which OneBooks user is looking at your embedded app right now. Your frontend gets one from App Bridge and sends it to your backend with each request. Your backend verifies it on every request (to learn who’s asking), and the first time it sees a given business and user it also exchanges it for API tokens it then keeps.
Getting one
Section titled “Getting one”Inside your embedded page:
const app = createApp();const token = await app.sessionToken();The host mints it by calling POST /apps/session-token on your page’s behalf
(session-cookie authenticated — the merchant is already signed in to OneBooks,
and your app must be installed and active in their business). The SDK reuses a
token until about 10 seconds before it expires and then fetches a new one
transparently, so in practice you rarely call sessionToken() at all:
app.fetch() attaches the
current token to every request to your own backend.
Never persist a session token yourself — no localStorage, cookie or
database. It’s worthless a minute later, and holding onto one only widens what
a leak could expose.
Header: { "alg": "ES256", "kid": "<key id>", "typ": "JWT" }. OneBooks signs
with ES256 (asymmetric) rather than an HMAC of your client secret, because
client secrets are hashed at rest and never recoverable server-side — an HMAC
scheme would be impossible to verify.
| Claim | Meaning |
|---|---|
iss | The OneBooks API origin, e.g. https://api.getonebooks.com |
aud | Your app’s public client_id |
sub | The id of the user currently viewing your app |
bid | The id of the business they’re in |
role | That user’s role in the business (owner, admin, member, viewer, …) |
locale | The merchant’s current UI language |
jti | Unique token id — single-use at token exchange |
iat | Issued-at (Unix seconds) |
nbf | iat − 5 seconds |
exp | iat + 60 seconds — the token is dead one minute after issue |
typ | Always the literal string "onebooks.app_session" |
Verifying it
Section titled “Verifying it”Fetch the JWKS once and cache it (respect standard HTTP caching; keys rotate
infrequently but do rotate — always match by kid, never hardcode a single
public key):
curl -s https://api.getonebooks.com/.well-known/jwks.jsonimport { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet( new URL('https://api.getonebooks.com/.well-known/jwks.json'),);
async function verifySessionToken(token, clientId) { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://api.getonebooks.com', audience: clientId, algorithms: ['ES256'], }); if (payload.typ !== 'onebooks.app_session') { throw new Error('not a session token'); } return payload; // { sub, bid, role, locale, jti, iat, nbf, exp, ... }}- Parse the JWT header; read
kid. - Fetch (or use your cached copy of) the JWKS; find the key with matching
kid. - Verify the signature is a valid ES256 signature over the header+payload, using that key.
- Check
issequals the OneBooks API origin,audequals yourclient_id,typequalsonebooks.app_session, and the current time is within[nbf, exp](allow a few seconds of clock skew). - Only now trust
sub,bid,role,locale.
Pitfalls
Section titled “Pitfalls”- Never trust unverified claims. Base64-decoding the payload without checking the signature lets anyone forge a session claiming to be any user in any business. Always verify against the JWKS.
- 60 seconds is real. By the time a token reaches your backend over a slow connection, it may already be close to expiry — exchange or verify it immediately on receipt rather than queuing it for later.
- Clock skew. OneBooks itself allows 30 seconds of skew on
exp/nbfat token exchange; if you verify session tokens yourself for other purposes, allow a small (a few seconds) tolerance too — but don’t stretch it far, since it directly widens the token’s usable window. - Keys rotate. The JWKS can list more than one key (an outgoing one kept
around briefly for tokens already in flight). Always select by
kid; never assume there’s exactly one key in the set. jtiis single-use — but only at token exchange. Verifying a session token yourself (to readsub/role) never consumes it; an exchange attempt does, even one that then fails. And App Bridge sends the same token with every request for up to ~50 seconds. So verify on every request, but exchange only the first time you see a business and user, let a page’s parallel requests share that one exchange, and keep the tokens you get — see Token exchange.
Where next
Section titled “Where next”Token exchange — turn a session token into a real API access token.