Authentication & tokens
This guide explains how your plugin obtains, stores, and uses Orbit tokens, and how to find the right store context for every request.
Two token types
Orbit issues two kinds of tokens. They are not interchangeable: one is short-lived and lives only in the iframe, the other is long-lived and belongs in your own backend.
| Session token | Access token | |
|---|---|---|
| Lifetime | ~1 hour | ~90 days |
| Where it lives | In memory, inside the iframe | Encrypted, in your own database |
| Used for | Frontend (iframe) calls | Backend / background calls |
| Refreshed by | The store dashboard, automatically | Your backend, via the paired refresh token |
| Delivered via | postMessage from the dashboard | Obtained by exchanging a session token |
JWT type | plugin_session | plugin_access |
Both are JWTs that carry the same claim shape (see below). The difference is lifetime and where you are responsible for holding them.
The iframe sign-in flow
Your plugin UI mounts as an <iframe> inside the Orbit store dashboard. You never see a login screen. Instead:
- The merchant installs your plugin and opens its page in the dashboard.
- The dashboard mints a short-lived session token for that store and your plugin.
- The dashboard delivers the token and the store context to your iframe over
window.postMessage. Tokens are never put in the URL. - The SDK listens for that message. You wait for it with
orbit.ready().
import { OrbitClient } from '@orbitcommerce/sdk'
const orbit = new OrbitClient()
await orbit.ready() // resolves once token + storeId arrive via postMessage
const storeId = orbit.getStoreId()
const token = orbit.getToken() // session JWT; its `scopes` claim is the granted set
ready() accepts an optional timeout in milliseconds (default 10000). When it resolves, the token and store context are available and you can start making calls.
The JWT payload
Every token decodes to the same claim shape:
{
"sub": "<StorePlugin id>",
"storeId": "<store uuid>",
"pluginId": "<your plugin id>",
"scopes": ["product:read", "order:read"],
"type": "plugin_session",
"iat": 1700000000,
"exp": 1700003600
}
subis the StorePlugin record id (this specific install).storeIdbinds the token to one merchant's store.scopesis the set the merchant consented to at install. It is enforced per endpoint — see Scopes.typeisplugin_sessionorplugin_access.
Where does the store ID come from?
This is the single most common source of bugs, so read it carefully: storeId is dynamic, per merchant install. It is never a hard-coded or global environment variable.
Your plugin can be installed on many stores. Each request carries the context of one specific store, and each token is bound to that store. You must resolve the correct store for every request rather than assuming one.
In the iframe. The store ID arrives over postMessage together with the session token. Read it after ready():
const orbit = new OrbitClient()
await orbit.ready()
const storeId = orbit.getStoreId()
On your server / in background jobs. There is no postMessage. When the merchant installed your plugin, you persisted that install's record — { storeId, accessToken, refreshToken }. Load the right record for the store you are acting on, and use that. The token itself is bound to a store, so OrbitClient.verifyToken will also return the storeId it belongs to.
// Pseudocode: pick the install, then act with its stored context
const install = await db.installs.findByStore(storeId)
const orbit = OrbitClient.fromRefreshToken({
token: install.accessToken,
refreshToken: install.refreshToken,
storeId: install.storeId,
onTokenRefreshed: ({ accessToken, refreshToken }) =>
db.installs.update(install.id, { accessToken, refreshToken }),
})
A static ORBIT_STORE_ID environment variable is acceptable only for single-store local development, where you are testing against one store. Never ship it as the way you resolve store context in production.
Authenticating in the iframe
For frontend code, construction plus ready() is the whole story. The dashboard supplies the token and store ID, and refreshes the session token for you. The SDK also retries once on an HTTP 401 by refreshing.
import { OrbitClient } from '@orbitcommerce/sdk'
const orbit = new OrbitClient()
await orbit.ready()
// Now you can query, mutate, and use the typed resource clients
const products = await orbit.products.list({ limit: 20 })
Call orbit.destroy() when your component unmounts to remove the postMessage listener.
Authenticating on your server or background jobs
Background work (syncs, webhook handlers, scheduled jobs) runs outside the iframe, so it needs a long-lived access token. You get one by exchanging a session token, then store the result encrypted.
1. Exchange a session token for an access + refresh pair
When the merchant first installs and opens your plugin, take the session token from the iframe and send it to your backend, which exchanges it:
import { OrbitClient } from '@orbitcommerce/sdk'
const { accessToken, refreshToken } = await OrbitClient.exchangeToken(sessionToken)
This POSTs to /oauth/token/exchange with Authorization: Bearer <sessionToken> and returns a long-lived access token plus a refresh token.
2. Store the pair encrypted, keyed by store
Persist { storeId, accessToken, refreshToken } for that install. Encrypt the tokens at rest. This is the record you load later, keyed by store, whenever you do background work.
3. Make calls with auto-refresh
Construct a client from the stored refresh token. It refreshes the access token before it expires and calls onTokenRefreshed so you can persist the rotated pair:
const orbit = OrbitClient.fromRefreshToken({
token: accessToken,
refreshToken,
storeId,
onTokenRefreshed: ({ accessToken, refreshToken }) =>
persist(storeId, { accessToken, refreshToken }),
})
If you only have a token and store ID and do not need auto-refresh, new OrbitClient({ token, storeId }) also works.
4. Refresh manually if you need to
You can also rotate the pair yourself:
const { accessToken, refreshToken } = await OrbitClient.refreshToken(oldRefreshToken)
This POSTs to /oauth/token/refresh with body { refreshToken }. Refresh rotates the pair — the old refresh token is invalidated, so always persist the new pair and discard the old one.
Verifying tokens on your own backend
When your backend receives a request that carries an Orbit token (for example, a call from your own iframe code to your own API), verify it before trusting it for anything sensitive.
import { OrbitClient } from '@orbitcommerce/sdk'
const { storeId, pluginId, scopes, type } = await OrbitClient.verifyToken(token)
This POSTs to /oauth/verify-token and returns the verified { storeId, pluginId, scopes, type }. The result is cached for five minutes. This is the recommended path for any sensitive operation, because it confirms the token has not been revoked and gives you the authoritative store and scope set.
For non-sensitive reads where you only need to inspect a claim, you may decode the JWT locally instead — but decoding does not prove the token is valid or unrevoked. Use verifyToken whenever the operation matters.
The OAuth endpoints
All three are unversioned and live at the API host root (https://api.myorbitcommerce.net). The SDK calls them for you; they are listed here for reference.
| Endpoint | Method | Auth / body | Returns |
|---|---|---|---|
/oauth/token/exchange | POST | Authorization: Bearer <sessionToken> | { accessToken, refreshToken } |
/oauth/token/refresh | POST | body { refreshToken } | { accessToken, refreshToken } (rotated) |
/oauth/verify-token | POST | body { token } | { storeId, pluginId, scopes, type } |
See the API reference for full request and response details.
Security best practices
- Never put tokens in URLs. They are delivered over
postMessageand belong in memory or in encrypted storage, never in query strings, paths, or redirects. - Never log tokens. Redact them from logs, error reports, and analytics.
- Encrypt access and refresh tokens at rest in your database.
- Rotate on every refresh. A refresh invalidates the old refresh token; persist the new pair and drop the old one immediately.
- Validate server-side for sensitive operations. Use
OrbitClient.verifyTokenrather than trusting a decoded JWT before performing writes or anything that matters. - Resolve store context per request. Load the install record bound to the relevant
storeId; never assume a single global store.