Skip to content

Getting started

Build, embed, and ship your first Orbit plugin: an external web app that runs inside the Orbit store dashboard and talks to the Orbit API with scoped OAuth tokens.

This guide walks you from an empty directory to a plugin page rendering inside the dashboard iframe. By the end you will have a Next.js app, the SDK installed, a plugin.json, an embed page that receives a live store context, and the headers required for iframe embedding.

What an Orbit plugin is

An Orbit plugin is an external, standalone web app that you build and host yourself (Next.js, React, or any stack that serves HTTPS). Orbit does not run your code. Instead, the store dashboard embeds your app as an <iframe> and brokers a secure connection to it.

Three properties define a plugin:

  • Embedded, not installed into Orbit. Your UI mounts inside the dashboard through an iframe. Communication between the dashboard and your app happens over window.postMessage, which the SDK wraps for you.
  • OAuth-scoped. Your plugin requests a set of scopes in its plugin.json. The merchant consents to them at install time, and every API call your plugin makes is checked against the granted scopes.
  • Owns its own data. Your plugin stores everything in its own database. It cannot read or write Orbit's database directly; all Orbit data flows through the public API.

A single plugin is installed on many stores. Each store grants its own consent and carries its own context, so you never hard-code a store identity.

Architecture overview

There are two communication paths to understand.

                postMessage (token + storeId)
  ┌─────────────────────────┐   <iframe>   ┌──────────────────────────┐
  │  Orbit store dashboard  │ ───────────▶ │  Your plugin app (HTTPS) │
  │admin.myorbitcommerce.net│ ◀─────────── │  Next.js / React / ...   │
  └─────────────────────────┘  UI helpers  └────────────┬─────────────┘
                                                         │ Bearer token (store-scoped)
                                                         ▼
                                            ┌──────────────────────────┐
                                            │  Orbit API               │
                                            │  api.myorbitcommerce.net  │
                                            │  /v1/*  /graphql  /oauth/*│
                                            └──────────────────────────┘
  • Dashboard to your app, and back (postMessage). When the dashboard loads your iframe, it sends the current session token and storeId over postMessage. The SDK listens for these. Your app can also post UI requests back (toasts, navigation, modals, resize) through the same channel.
  • Your app to the Orbit API. With the token and store context in hand, the SDK calls the Orbit API on https://api.myorbitcommerce.net, attaching Authorization: Bearer <token> to each request. The public surfaces are REST at /v1/*, GraphQL at /graphql, and the OAuth token endpoints at /oauth/*. On /v1/* the store is derived from the token itself; the x-store-id header the SDK also sends is only meaningful for /graphql. Every REST response arrives wrapped in the standard envelope { statusCode, message, data, timestamp } (the SDK unwraps data for you; see Authentication).

The store and token are provided dynamically by the dashboard at runtime. Never bake a storeId into an environment variable or constant: the same deployed plugin serves every store that installs it.

Quick start

1. Create a Next.js app

Shell
npx create-next-app@latest my-orbit-plugin --typescript
cd my-orbit-plugin

2. Install the SDK

Shell
npm install @orbitcommerce/sdk

The SDK exposes the OrbitClient class, which handles the postMessage handshake, token refresh, and typed API calls.

3. Add a plugin.json

plugin.json is your plugin's one file: it holds the configuration a release snapshots (where the plugin is served from, where its UI mounts, the scopes and webhooks it needs) and, in an optional listing block, the copy the plugin store shows. Its format is a JSON Schema the API serves at https://api.myorbitcommerce.net/v1/plugin-schema.json, public and free to fetch. Point $schema at it and your editor checks and completes the file as you type; the partner dashboard's Import from plugin.json and every release you cut are validated against the same schema.

Scopes follow a strict <resource>:<action> convention where the resource is singular and kebab-case: product:read, order:read, not products:read. Request only what you need; the merchant sees and consents to this exact list.

JSON
{
  "$schema": "https://api.myorbitcommerce.net/v1/plugin-schema.json",
  "pluginId": "order-sync",
  "version": "1.0.0",
  "baseUrl": "https://my-orbit-plugin.example.com",
  "extensionPoints": [
    {
      "target": "dashboard.main.embed",
      "url": "/embed",
      "label": "Order Sync"
    }
  ],
  "oauth": {
    "scopes": ["product:read", "order:read", "order:list"],
    "webhooks": [
      { "topic": "order.created", "path": "/webhooks/orbit" }
    ]
  },
  "ui": {
    "menu": { "label": "Order Sync", "icon": "puzzle" }
  },
  "listing": {
    "name": "Order Sync",
    "shortDescription": "Sync orders to your fulfilment system",
    "description": "Order Sync mirrors new and updated orders from your Orbit store into an external fulfilment system, keeping statuses in step without manual data entry.",
    "category": "sales",
    "tags": ["orders", "fulfilment"]
  }
}

A few things to note:

  • baseUrl is where your app is served, and every url and path below it is a path joined onto it. Change baseUrl and the whole plugin moves; that is how a development release points a store at your machine.
  • extensionPoints[].target names a slot in the dashboard. dashboard.main.embed is your main plugin page, the one merchants open from the sidebar; the partner dashboard's Technical tab lists the others. The sidebar item itself is ui.menu, which needs a label and an icon.
  • oauth.webhooks declares the topics your plugin wants delivered and where; Orbit creates the subscriptions on every store that installs it. Each topic needs its scope in oauth.scopes. See Webhooks.
  • listing is plugin store copy, never configuration. Releases ignore it; creating a plugin from the file uses it to fill the listing fields.

See the full list of available scopes in the Scope Catalog, and the Scopes guide for how requesting and enforcement work. Publishing covers the rest of the file.

4. Build the embed page

Create the page that the dashboard.main.embed extension point's url points to. It constructs an OrbitClient, waits for the dashboard handshake with ready(), then reads the live store context.

TSX
// app/embed/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { OrbitClient } from '@orbitcommerce/sdk';

export default function EmbedPage() {
  const [storeId, setStoreId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const orbit = new OrbitClient();

    orbit
      .ready() // resolves once the token + storeId arrive via postMessage
      .then(() => {
        setStoreId(orbit.getStoreId());
      })
      .catch((e: unknown) => {
        setError(e instanceof Error ? e.message : 'Failed to initialise');
      });

    // remove the postMessage listener on unmount
    return () => orbit.destroy();
  }, []);

  if (error) return <p>{error}</p>;
  if (!storeId) return <p>Connecting to Orbit...</p>;

  return <p>Connected to store {storeId}</p>;
}

A few things to note:

  • new OrbitClient() with no arguments is the correct iframe constructor. The token, storeId, and API URL all arrive over postMessage; you do not pass them in.
  • ready() accepts an optional timeout in milliseconds (default 10000). It rejects if the handshake does not complete in time, so always handle the rejection.
  • Read context only after ready() resolves: orbit.getStoreId() and orbit.getToken(). Calling them before the handshake returns nothing useful. Your granted scopes live in the token claims; see Scopes.
  • Call orbit.destroy() when the component unmounts to detach the listener.

Once connected, you can make API calls, for example orbit.query(...) / orbit.mutate(...) for GraphQL, or typed REST clients like orbit.orders and orbit.products. See the SDK guide for the full surface.

5. Configure CSP and HTTPS for iframe embedding

The dashboard can only embed your app if it is served over HTTPS and sends a Content-Security-Policy header whose frame-ancestors directive allows Orbit's domains. Without this header, the browser refuses to render your page inside the dashboard iframe.

Add the header in next.config.ts:

TypeScript
// next.config.ts
import type { NextConfig } from 'next';

const FRAME_ANCESTORS =
  "frame-ancestors 'self' https://*.orbitcommerce.net https://*.myorbitcommerce.net";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/embed/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: FRAME_ANCESTORS,
          },
        ],
      },
    ];
  },
};

export default nextConfig;

The same header does for development and production, because in both cases the page embedding you is an Orbit dashboard on an Orbit domain. What changes between the two is where your app is served from, and that is settled by the release, not by this header: the release validator accepts https URLs only and refuses localhost, so a local build is reached through an https tunnel (next step) and a production build through your hosted HTTPS origin.

6. Run it on a development store

You test on a development store: a free store of your own, created from the partner dashboard, that can never take live payments or become a merchant's store. Create one, create your plugin in the partner dashboard (Plugins, Create plugin, or Import from plugin.json), and use the plugin page's Test on a development store panel to install it there.

To run the plugin from your machine, expose your local port through an https tunnel (Cloudflare quick tunnels and ngrok both work) and cut a development release whose baseUrl is the tunnel URL. The store dashboard then loads the iframe from your machine, the handshake fires, ready() resolves, and you see the connected store id render. Testing your plugin has every step and the API route.

Next steps

  • Authentication: session vs access tokens, the postMessage handshake, and refreshing tokens for backend or background work.
  • SDK reference: the full OrbitClient surface: GraphQL queries, typed REST clients, billing, and the iframe UI helpers.
  • Scopes: how to choose scopes, the singular naming convention, consent, and 401/403 enforcement. Browse the complete Scope Catalog.
  • Webhooks: subscribe to topics like order.created, verify delivery signatures, send test webhooks, and handle retries.
  • Development stores: the free store you build and test on, with sample data and test payments.
  • Testing your plugin: install it on a development store and run it from your machine, before it is reviewed or seen by any merchant.
  • Publishing: releases, review, and getting listed on the marketplace or made available to merchants.
  • API reference: the complete REST and GraphQL surface.