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>; calls require the matching scope (see Scopes). On /v1 endpoints the store is derived from the token — the x-store-id header the SDK also attaches is ignored there (it matters only for GraphQL).

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: 'GBP', // ISO 4217 code enabled on the store, or the currency UUID
  shippingAmount: 0, // required — use 0 for free shipping
  externalId: 'mp-1029',
  source: 'my-plugin',
  items: [{ name: 'Widget', sku: 'WIDGET-2', quantity: 2, price: 1999 }],
});

currencyId accepts the Orbit currency UUID or an ISO 4217 code — codes are resolved against the currencies enabled on the store, and an un-enabled code fails with a 400 naming the currency. Line-item sku is persisted: on catalog-matched lines a provided sku wins over the catalog snapshot, and on custom lines it is the only sku the order item can carry.

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.getSubscriptionStatus();

if (!status.hasSubscription) {
  // Opens the dashboard payment modal (iframe context)
  await orbit.billing.requestPurchase({
    planId: plans[0].id,
    priceId: plans[0].prices?.[0].id
  });
}
MethodPurpose
getPlans()List the plans you have configured.
getPaymentMethods()The merchant's available payment methods.
getSubscriptionStatus()Current subscription state for this store.
hasActiveSubscription()Shorthand boolean for the above.
requestPurchase({ planId, priceId? })Open the dashboard payment modal for the merchant to pay.
subscribeToPlan({ planId, priceId?, paymentMethodId })Subscribe using a known payment method.
changePlan({ planId, priceId? })Move an existing subscription to another plan.
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.

getSubscriptionStatus() returns { hasSubscription, subscription? }, and the detail — status, planName, currentPeriodEnd, cancelAtPeriodEnd — is nested under subscription. A status of trialing is still an entitlement: the merchant agreed to the plan and expects the features.

Where plans come from. The pricing block in plugin.json is a display summary for the store listing; it does not create anything sellable. Plans and their prices are configured on your plugin's detail page in the partner dashboard, and that is what getPlans() returns. A manifest advertising a subscription with no plans configured shows a price and sells nothing.

Check entitlement on your server too. Requires the billing:read scope — without it these calls return 401, and code that treats the error as "not subscribed" will show a free tier to paying customers. Treat an error as unknown rather than as a negative answer: an outage must not silently downgrade someone. The page should check as well, so the UI is right, but the page runs on the merchant's machine and cannot be what decides whether they paid.

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 either the token could not be refreshed (re-run the OAuth flow) or it is valid but lacks a required scope — the error body reads Missing required scopes: …, so request it in your manifest and have the merchant reinstall or re-consent. (A 403 is specific to subscribing to a webhook topic without the topic's scope.)

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.