The SDK
The @orbitcommerce/sdk package wraps the Orbit Commerce APIs — OAuth-scoped REST, GraphQL, billing, and the dashboard postMessage bridge — behind one typed OrbitClient.
The SDK is versioned independently of the API. A new SDK release does not imply a new API version, and the REST surface it calls remains /v1/*. See Authentication for how tokens are issued and the API reference for the full endpoint and schema catalog.
Install
npm install @orbitcommerce/sdk
import { OrbitClient } from '@orbitcommerce/sdk';
Initialisation
There are two ways to construct a client, depending on where your code runs.
Iframe (frontend)
Inside the store dashboard your plugin renders in an <iframe>. The dashboard delivers the session token and storeId over postMessage — never in a URL. Construct the client with no arguments and wait for ready().
const orbit = new OrbitClient();
await orbit.ready(); // default timeout 10000ms
const storeId = orbit.getStoreId();
ready(timeoutMs = 10000) resolves once the token and storeId have arrived from the dashboard. Until it resolves the client is not configured, so do not issue calls before awaiting it.
Server / background
For backend jobs, webhook handlers, and cron work there is no dashboard to talk to. Use the access token and refresh token you persisted when the merchant installed the plugin.
const orbit = OrbitClient.fromRefreshToken({
token: install.accessToken,
refreshToken: install.refreshToken,
storeId: install.storeId,
onTokenRefreshed: async ({ accessToken, refreshToken }) => {
await saveInstallTokens(install.storeId, { accessToken, refreshToken });
},
});
fromRefreshToken auto-refreshes the access token before it expires and calls onTokenRefreshed with the rotated pair so you can persist it. Refresh rotates both tokens — the old refresh token is invalidated, so you must store the new one.
If you already hold a valid access token and do not need auto-refresh, you can construct directly:
const orbit = new OrbitClient({ token: install.accessToken, storeId: install.storeId });
storeIdis dynamic per merchant install — never a global constant. A plugin is installed on many stores, and each carries its own context. Load the correct{ storeId, accessToken, refreshToken }record per store before constructing a server-side client.
apiUrl resolution
apiUrl accepts either a base URL or a full /graphql URL; it is normalised automatically. The default is https://api.myorbitcommerce.net/graphql. You rarely need to set it explicitly — resolution differs by environment.
| Environment | Resolution order |
|---|---|
| Server | ORBIT_API_URL → NEXT_PUBLIC_ORBIT_API_URL → default |
| Client | ?apiUrl= query param → NEXT_PUBLIC_ORBIT_API_URL → postMessage → default |
# server-side override
ORBIT_API_URL=https://api.myorbitcommerce.net/graphql
# exposed to the browser bundle
NEXT_PUBLIC_ORBIT_API_URL=https://api.myorbitcommerce.net/graphql
REST resource clients
The client exposes typed resource helpers. Every request sends Authorization: Bearer <token> and x-store-id: <storeId>; calls require the matching scope (see Scopes).
Products
// List a page of products
const { items } = await orbit.products.list({ limit: 50 });
// Create a product
const created = await orbit.products.create({
// product fields — see the API reference
});
// Paginate the full catalog for an initial sync
let page = 1;
for (;;) {
const batch = await orbit.products.listForSync({ page, limit: 100 });
if (batch.items.length === 0) break;
await indexProducts(batch.items);
page += 1;
}
listForSync is the paginated cursor you want for backfilling an external system; list is the general-purpose query.
Orders
const order = await orbit.orders.create({
email: 'buyer@example.com',
currencyId: store.currencyId,
externalId: 'mp-1029',
source: 'my-plugin',
items: [{ name: 'Widget', quantity: 2, price: 1999 }],
});
Settings
settings reads and writes the values defined by your manifest's settingsSchema.
// Full settings object plus the declared schema
const { settings, schema } = await orbit.settings.get();
// Read a single value
const apiMode = await orbit.settings.getValue('api_mode');
// Persist changes
await orbit.settings.update({ api_mode: 'live', sync_interval: 15 });
The billing resource is documented in its own section below.
GraphQL
For data not covered by a resource client, call GraphQL directly. Both methods POST to {base}/graphql with the same Authorization and x-store-id headers.
const data = await orbit.query<{ product: { id: string; title: string } }>(
`query Product($id: ID!) { product(id: $id) { id title } }`,
{ id: productId },
);
await orbit.mutate(
`mutation Update($id: ID!, $input: ProductInput!) {
updateProduct(id: $id, input: $input) { id }
}`,
{ id: productId, input },
);
On HTTP 401 the SDK refreshes the token once and retries the request automatically. If the GraphQL response contains errors, the SDK throws Error(result.errors[0].message). Field names and input types live in the API reference.
Billing
The billing module lets a plugin offer paid plans and read the merchant's subscription state.
const plans = await orbit.billing.getPlans();
const status = await orbit.billing.getStatus();
if (!status.active) {
// Opens the dashboard payment modal (iframe context)
await orbit.billing.requestPurchase({ planId: plans[0].id });
}
| Method | Purpose |
|---|---|
getPlans() | List the plans you have configured. |
getPaymentMethods() | The merchant's available payment methods. |
getStatus() | Current subscription state for this store. |
requestPurchase({ planId, priceId? }) | Open the dashboard payment modal for the merchant to pay. |
subscribeToPlan({ planId, priceId?, paymentMethodId }) | Subscribe using a known payment method. |
cancelSubscription({ reason? }) | Cancel the active subscription. |
restoreSubscription() | Restore a previously cancelled subscription. |
In an iframe, prefer requestPurchase — it hands the payment flow to the dashboard. Use subscribeToPlan only when you already have a paymentMethodId.
Iframe UI helpers
These post to the parent dashboard, so they only work in the iframe context after ready().
orbit.toast({ message: 'Sync complete', type: 'success' });
orbit.toast('Saved'); // string shorthand
orbit.navigate('/products'); // navigate the dashboard
orbit.setRoute('/plugin/settings'); // sync your internal route into the URL
const initial = orbit.getInitialRoute(); // string | null — restore deep link on load
orbit.resize({ height: 720 }); // fit the iframe to your content
orbit.openModal({ title: 'Connect account', content: '<div>…</div>', width: 480 });
toast takes either a string or { message, type?: 'success' | 'error' | 'info' | 'warning', duration? }.
When your plugin is mounted as a focused action, use the action lifecycle helpers:
const ctx = orbit.getResourceContext(); // { resourceType, resourceId, storeId } | null
if (ctx?.resourceType === 'product') {
// act on ctx.resourceId
}
orbit.completeAction({ ok: true }); // finish and return a result to the dashboard
orbit.closeAction(); // dismiss without a result
Context getters
Read the current token and store context synchronously after the client is ready.
orbit.getStoreId(); // string
orbit.getToken(); // current access/session token (a JWT)
orbit.getApiUrl(); // resolved GraphQL URL
orbit.getBaseUrl(); // resolved base URL
orbit.isConfigured(); // token + storeId present
orbit.getTokenExpiresAt(); // expiry timestamp (or null)
orbit.isTokenExpiring(); // true when close to expiry
The granted scopes and the plugin id are not exposed as getters — they live in the token's claims. Read them from a verified token server-side, or by decoding orbit.getToken() in the iframe:
// Server-side (recommended): authoritative + checks revocation
const { scopes, pluginId } = await OrbitClient.verifyToken(token);
// In the iframe: decode the session JWT payload
const [, payload] = orbit.getToken().split('.');
const claims = JSON.parse(atob(payload)) as { scopes: string[]; pluginId: string };
if (claims.scopes.includes('order:read')) {
await loadOrders();
}
See Scopes for the convention and the Scope Catalog for the full list.
Static auth helpers
These run server-side and do not require a constructed client. Use them when validating an incoming token or completing an OAuth exchange. See Authentication for the full flow.
// Validate a token (POSTs /oauth/verify-token; result cached ~5 min)
const claims = await OrbitClient.verifyToken(token);
// -> { storeId, pluginId, scopes, type }
// Exchange a short-lived session token for an access + refresh pair
const { accessToken, refreshToken } = await OrbitClient.exchangeToken(sessionToken);
// Refresh an access token (rotates the pair — old refresh token is invalidated)
const rotated = await OrbitClient.refreshToken(refreshToken);
Always validate tokens server-side for sensitive operations, and verify the returned storeId matches the install you are acting on.
Error handling
The SDK throws on failure rather than returning error objects:
- REST and GraphQL calls throw
Erroron any non-2xx HTTP response. - GraphQL responses containing an
errorsarray throwError(result.errors[0].message). - A
401is handled transparently first — the SDK refreshes the token once and retries; only a persistent failure surfaces.
Wrap calls accordingly:
try {
await orbit.orders.create(payload);
} catch (err) {
orbit.toast({ message: (err as Error).message, type: 'error' });
}
A 401 after retry means the token could not be refreshed; re-run the OAuth flow. A 403 means the token is valid but lacks the required scope — request it in your manifest and have the merchant reinstall or re-consent.
Cleanup
In the iframe the client installs a postMessage listener. Remove it when your component unmounts to avoid leaks and duplicate handlers.
useEffect(() => {
const orbit = new OrbitClient();
orbit.ready().then(() => {/* … */});
return () => orbit.destroy();
}, []);
destroy() is only relevant for the iframe context; server-side clients hold no listener.
Related
- Authentication — token types, OAuth exchange, and refresh.
- Scopes and the Scope Catalog — what each call requires.
- Webhooks — receive events instead of polling.
- API reference — endpoint paths, GraphQL schema, and field-level detail.