Orbit Commerce
Plugin guides

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 });

storeId is 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.

EnvironmentResolution order
ServerORBIT_API_URLNEXT_PUBLIC_ORBIT_API_URL → default
Client?apiUrl= query param → NEXT_PUBLIC_ORBIT_API_URLpostMessage → 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 });
}
MethodPurpose
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 Error on any non-2xx HTTP response.
  • GraphQL responses containing an errors array throw Error(result.errors[0].message).
  • A 401 is 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.