Skip to content

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.

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.

ClaimMeaning
issThe OneBooks API origin, e.g. https://api.getonebooks.com
audYour app’s public client_id
subThe id of the user currently viewing your app
bidThe id of the business they’re in
roleThat user’s role in the business (owner, admin, member, viewer, …)
localeThe merchant’s current UI language
jtiUnique token id — single-use at token exchange
iatIssued-at (Unix seconds)
nbfiat − 5 seconds
expiat + 60 seconds — the token is dead one minute after issue
typAlways the literal string "onebooks.app_session"

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):

Terminal window
curl -s https://api.getonebooks.com/.well-known/jwks.json
import { 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, ... }
}
  • 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/nbf at 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.
  • jti is single-use — but only at token exchange. Verifying a session token yourself (to read sub/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.

Token exchange — turn a session token into a real API access token.