# 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.

> **Not building a plugin?** If you are connecting a single store's own systems — an ERP, a warehouse, an in-house service — you do not need any of the machinery on this page. The merchant can mint a [store API key](/guides/api-keys) in their dashboard and your service authenticates with that single header. This guide is for plugins: software many stores install.

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

1. The merchant installs your plugin and opens its page in the dashboard.
2. The dashboard mints a short-lived **session token** for that store and your plugin.
3. The dashboard delivers the token and the store context to your iframe over `window.postMessage`. Tokens are never put in the URL.
4. The SDK listens for that message. You wait for it with `orbit.ready()`.

```ts
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:

```json
{
  "sub": "<StorePlugin id>",
  "storeId": "<store uuid>",
  "pluginId": "<your plugin id>",
  "scopes": ["product:read", "order:read"],
  "type": "plugin_session",
  "iat": 1700000000,
  "exp": 1700003600
}
```

- `sub` is the StorePlugin record id (this specific install).
- `storeId` binds the token to one merchant's store.
- `scopes` is the set the merchant consented to at install. It is enforced per endpoint — see [Scopes](/guides/scopes).
- `type` is `plugin_session` or `plugin_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()`:

```ts
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.

```ts
// 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.

```ts
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:

```ts
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:

```ts
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:

```ts
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.

```ts
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 (in `data`) |
|---|---|---|---|
| `/oauth/token/exchange` | POST | `Authorization: Bearer <sessionToken>` | `{ accessToken, refreshToken, expiresIn, expiresAt, … }` |
| `/oauth/token/refresh` | POST | body `{ refreshToken }` | `{ accessToken, refreshToken, expiresIn, expiresAt, … }` (rotated) |
| `/oauth/verify-token` | POST | `Authorization: Bearer <token>` (no body) | `{ valid, storeId, pluginId, scopes }` |

Like every REST response on the platform, these arrive wrapped in the standard envelope `{ statusCode, message, data, timestamp }` — the payloads above are the `data` field. If you call the endpoints without the SDK, unwrap `data`; reading the top-level body leaves every token field `undefined`. (The SDK unwraps for you, and its `verifyToken()` additionally exposes the token's `type` claim.)

See the [API reference](/api-reference) for full request and response details.

## Security best practices

- **Never put tokens in URLs.** They are delivered over `postMessage` and 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.verifyToken` rather 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.

## Next steps

- Learn how scopes are requested, consented to, and enforced in [Scopes](/guides/scopes).
- See the full client API, resource clients, and iframe helpers in the [SDK guide](/guides/sdk).
