Skip to content

Publishing your plugin

This guide takes you from a built plugin that works on your development store to one merchants can install: listed on the plugin store after Orbit's review, or made available through install links without a listing.

Overview

Publishing happens in the partner dashboard at https://partner.myorbitcommerce.net. You create the plugin (by name, or by importing your plugin.json), fill in its listing, keep its configuration in releases, optionally add pricing plans, and choose how it is distributed. There are two ways to go live:

                                 Submit for Review          approved
  draft ──▶ release ──▶ public ─────────────────▶ in review ─────────▶ listed in the plugin store
                   │
                   └──▶ unlisted or private ──▶ Make available to merchants ──▶ live via install links
                                                                                    │
                                                                     Submit for public listing ──▶ listed
                                                                     Unpublish ──▶ inactive

Before you start, make sure your plugin works end to end on a development store: see Testing your plugin. Getting started covers the architecture, Scopes the permission model, and the SDK guide how your app talks to Orbit at runtime.

Register or sign in

  1. Go to https://partner.myorbitcommerce.net.
  2. Register a partner account, or sign in if you already have one, and verify your email address.
  3. From the partner dashboard you can create plugins, manage their releases, configure pricing, test them on development stores and submit them for review.

A single partner account can own multiple plugins.

Create the plugin

Under Plugins, choose New Plugin. Give it a name and choose Create plugin; listing details, technical configuration and media come after, on the plugin's own page.

If you already have a plugin.json, choose Import from plugin.json instead. The file is checked against the public schema and the plugin is created from it: configuration from the top level, listing copy from the listing block. Older field names (displayName, embedUrl, a top-level webhooks list, oauth.redirectUrl, author, permissions, ui.pages) are still accepted, and the import ends with a report headed What Orbit did with your plugin.json that lists every field that was changed to fit Orbit or left out, so nothing changes silently. A file that uses relative URLs needs a baseUrl to join them to.

The plugin page has six tabs:

TabWhat lives there
OverviewThe submission readiness checklist, and the Test on a development store panel
ListingName, descriptions, category, tags, features, support and documentation links, screenshots and icon
TechnicalExtension points, scopes, menu and settings schema, the Releases card and the Webhook signing secret card
PlansPricing plans, if you charge
DistributionVisibility, install links, and Make available to merchants
ReviewsMerchant reviews and your replies

Listing

Marketplace-facing detail. The form enforces the limits.

FieldRules
nameDisplay name, max 30 characters
pluginIdStable identifier, kebab-case, max 50 characters
shortDescriptionOne-line summary shown on listing cards
descriptionFull description for the plugin store page
versionSemantic version, for example 1.0.0
categoryOne of: marketing, sales, analytics, shipping, payments, inventory, customer-service, design, productivity, other
tagsFree-form tags
features[]Highlighted capabilities shown on the listing
supportUrlWhere merchants get help
documentationUrlYour plugin's docs

The pluginId is your plugin's permanent handle. Choose it carefully, since it identifies your plugin across installs and releases. supportUrl and documentationUrl are optional, but real, working links improve both review and merchant trust. In plugin.json all of this is the listing block, except pluginId and version, which are configuration.

Technical configuration

How your plugin mounts inside the store dashboard, what it can access, and how it is configured. In plugin.json these are the top-level fields; the Technical tab edits the same things.

  • baseUrl: where your hosted app is served. Every path below is joined onto it.
  • extensionPoints[]: where your plugin UI appears. Each entry is { target, url, label? }, where url is a path under baseUrl (or an absolute HTTPS URL) that the store dashboard loads in an iframe. dashboard.main.embed is the main plugin page; the Technical tab lists the other targets, such as the home widget and the product page block and action.
  • oauth.scopes[]: the permissions your plugin requests, in the singular <resource>:<action> form (product:read, order:list).
  • oauth.webhooks[]: { topic, path } subscriptions Orbit creates on every store that installs the plugin. Each topic needs its scope in oauth.scopes. See Webhooks.
  • oauth.redirectUrls[]: OAuth callback paths or URLs, if you use them.
  • ui.menu: { label, icon, position?, parent? } for your entry in the store dashboard navigation. label and icon are required.
  • staffPermissions[]: the permissions a store hands its own staff for your plugin's screens. Each entry is { key, label, default? }; see Staff permissions below.
  • settingsSchema: the merchant-facing settings form, an object keyed by field key (lower_snake). Each field is { type, title, description?, required?, options? }, where type is one of string, number, boolean or select, and options ({ label, value }) applies to select.
JSON
{
  "baseUrl": "https://app.example.com",
  "extensionPoints": [
    {
      "target": "dashboard.main.embed",
      "url": "/orbit",
      "label": "My Plugin"
    }
  ],
  "oauth": {
    "scopes": ["product:list", "product:read", "order:read"],
    "webhooks": [{ "topic": "order.created", "path": "/webhooks/orbit" }],
    "redirectUrls": ["/oauth/callback"]
  },
  "ui": {
    "menu": { "label": "My Plugin", "icon": "puzzle", "position": 50 }
  },
  "staffPermissions": [
    { "key": "orders:sync", "label": "Run a sync by hand", "default": "staff" },
    { "key": "settings:edit", "label": "Change the connection settings" }
  ],
  "settingsSchema": {
    "api_mode": {
      "type": "select",
      "title": "API mode",
      "description": "Which environment to sync with",
      "required": true,
      "options": [
        { "label": "Live", "value": "live" },
        { "label": "Sandbox", "value": "sandbox" }
      ]
    }
  }
}

Every URL in a release must be HTTPS, and localhost is refused by the release validator. Your hosted app must also send the frame-ancestors header from Getting started so the dashboard can embed it.

Request only the scopes you need

The scopes you list in oauth.scopes are what the merchant is asked to consent to at install. The granted set becomes your token's scopes claim and is enforced on every request: a token missing a required scope is rejected with HTTP 401 (the error body names the missing scope). Webhook-topic scope failures are the exception; those return HTTP 403.

Request the minimum that makes your plugin work. Over-requesting raises friction at the consent step and during review. If you later need more, ship a new release. See the Scopes guide and the full Scope Catalog.

Staff permissions

Scopes say what your plugin may do in a store. Staff permissions say what each member of the store's team may do inside your plugin, and the store decides, not you. Declare them in staffPermissions[]:

FieldPurpose
keyresource:action in lower case, two or three segments (deals:manage, automations:rules:edit), each starting with a letter and then letters, digits or hyphens; at most 64 characters and unique within your plugin
labelWhat the store's role editor shows beside the switch, at most 80 characters
defaultThe lowest store role that holds it without being granted it: owner, admin or staff. Defaults to admin

A team member who holds any of your permissions can open your plugin's page, even without the settings:read:store permission every plugin page otherwise takes: a store hands a custom role one of your permissions so that role can use your plugin. A store owner always holds every permission you declare. Admins hold the admin and staff ones, staff and the premade specialist roles hold the staff ones, and a team member with only a custom role holds whatever that role or a one-off grant gives them. When your release goes live, each entry becomes a row in the store's role editor under your plugin's name, so a store can widen or narrow access per role exactly as it does for Orbit's own permissions. A key you later remove from the manifest disappears from every role.

Your plugin learns what the current user holds from the session token: POST /oauth/verify-token answers permissions, the keys this user holds, next to userId and userName (see Verifying tokens). Check it before showing a control or acting on a request made from your embedded page, and treat a missing list as holding nothing. The list is worked out when the session token is minted, so a role change reaches your plugin with the user's next session token. Background work runs on the access token, which names no user and carries no permissions.

At most 50 entries. Staff permissions are configuration: changing them is a new release, like changing a scope.

Releases

A release is an immutable snapshot of your configuration. Merchants' stores run your plugin's current release; Orbit reviews a specific release; a development release runs only on your development stores. Listing copy is never part of a release, so you can edit it at any time without releasing.

Your first save of the Technical tab creates release 1 and makes it current. From then on, configuration changes are made by creating a release:

HTTP
POST /api/plugins/mine/{pluginId}/releases
Authorization: Bearer <partner JWT>
Content-Type: application/json

{ "manifest": { ...your plugin.json configuration... }, "env": "prod", "notes": "Adds the order.created webhook" }

The same configuration under the same version returns the existing release with deduplicated: true (200); anything else is a new release (201). A manifest that fails validation is a 400 with errors: [{ path, code, message }]. Add "activate": true to make the release current straight away, or activate it later:

HTTP
POST /api/plugins/mine/{pluginId}/releases/{releaseNumber}/activate

Activating a release rolls it out to every store running the plugin, so treat it as a deploy. Development releases cannot be activated; the API refuses with the reason. The Technical tab's Releases card lists every release, newest first, with a Production or Development badge and Current on the live one.

HTTP
GET /api/plugins/mine/{pluginId}/releases                    # newest first, as { items, meta }
GET /api/plugins/mine/{pluginId}/releases?channel=production
GET /api/plugins/mine/{pluginId}/releases?page=2&limit=50    # 50 a page by default, 100 at most

Versioning

Use semantic versioning (MAJOR.MINOR.PATCH, for example 1.0.0) for the version field. It is a label on the release, not part of what makes two releases different.

  • PATCH (1.0.1): backwards-compatible fixes.
  • MINOR (1.1.0): backwards-compatible additions.
  • MAJOR (2.0.0): breaking changes, including changes to the scopes you require.

If a new release requests additional scopes, merchants consent to the expanded set, so treat scope expansion as a significant change and document it in your release notes.

Configure pricing (if monetising)

If you charge for your plugin, configure pricing plans on the Plans tab. Plans can be:

  • free: no charge
  • one_time: a single purchase
  • subscription: recurring billing

Your plugin reads and acts on the merchant's billing state at runtime through the SDK's billing client, for example orbit.billing.getSubscriptionStatus() to check entitlement, and orbit.billing.requestPurchase({ planId, priceId }) to open the dashboard payment modal. Checking entitlement requires the billing:read scope, and it should also happen on your server: a page running on the merchant's machine cannot be what decides whether they paid. See the SDK guide for the full billing surface. On a development store a paid plugin runs on a trial that never bills, so you can test the paid path without a card.

Choose how it is distributed

On the Distribution tab, pick a visibility:

VisibilityWho can installHow they installReview
PublicAny merchantThe plugin store listingOrbit reviews it before it goes live
UnlistedAnyone with a linkAn install linkNone
PrivateOne named store, or one useAn install link bound to that store or single useNone

Public is the plugin store. Switching a plugin to Public directly is refused; going public happens through Submit for Review, and approval switches it. Unlisted and private plugins never appear in the plugin store, so they need no review to go live.

Make it available to merchants

For an unlisted or private plugin, the Distribution tab has Make available to merchants. It makes the plugin live at once, with no review, and it stays out of the plugin store; merchants install it through your install links. You can still submit it for a public listing later. The button is enabled once the readiness checklist on the Overview tab is complete.

Install links start working once the plugin is live. Until then a merchant who opens one is told the link is not valid, so wait before you share it. Install links are for merchants: to test your own plugin, use a development store.

Generate a link on the Distribution tab, or with the API:

HTTP
POST /api/plugins/mine/{pluginId}/install-links
Authorization: Bearer <partner JWT>
Content-Type: application/json

Every field is optional. An empty body gives you a reusable link that never expires.

FieldTypeEffect
expiresAtISO 8601Link stops working after this instant. Must be in the future
maxUsesinteger, at least 1Cap on installs through this link. 1 makes it single-use
targetStoreIduuidBinds the link to one store; only that store can resolve or install through it

The response contains the raw token and the shareable URL. This is the only time either is returned: only a SHA-256 hash of the token is stored, so a database leak never exposes a usable link. If you lose it, revoke the link and generate another. Creating a link for a public plugin returns 400; public plugins are installed from the listing.

A merchant who opens the link while signed in to their store sees the consent screen: your plugin's identity and the exact scopes its current release asks for. Accepting installs and activates it, and the granted scopes become your plugin's token scopes for that store.

HTTP
GET    /api/plugins/mine/{pluginId}/install-links            # list, with a usable flag
DELETE /api/plugins/mine/{pluginId}/install-links/{linkId}   # revoke

usable is true only when the link would install right now: the plugin is live and the link is not revoked, expired or used up. Revoking is idempotent. Revoke a link the moment it has served its purpose; anyone holding it can install your plugin on a store they control until it expires or hits its use cap. Unknown, revoked, expired and exhausted links, and links whose plugin is not live, all return the same 404, so a link that stops working gives no signal about which of those it was. A link bound to a different store returns 403.

Unpublish

A live unlisted or private plugin has an Unpublish action. Merchants who already installed it keep using it; new installs through your links stop until you make it available again, and a pending listing review is withdrawn. Public plugins are listed in the store and cannot be unpublished this way.

Submit for review

Review is what puts a plugin in the plugin store. Two things gate it:

  • The readiness checklist on the Overview tab: a name, plugin ID, short description, description, version and category, enough screenshots, and a chosen distribution. The same checklist gates Make available to merchants.
  • A release. Submitting records your current release as the submitted release, so Orbit reviews exactly that. A plugin with no release is refused with "Create a release before submitting your plugin for review."

Then:

  • A draft, inactive or rejected plugin with Public visibility: choose Submit for Review. It moves to In review.
  • A live unlisted or private plugin: choose Submit for public listing. It stays live through your install links while Orbit reviews it, and approval switches it to Public.

Orbit staff install the submitted release on a review store of their own, open it as a merchant would, place a test order and check the manifest, the scopes you requested and that your embedded app loads and behaves. You can keep releasing while the review is open; the reviewer sees that the plugin has changed since submission but tests the release you submitted. Approval lists the plugin in the plugin store. Rejection shows Changes requested with the reviewer's notes; address them and submit again (a live plugin stays live through its install links in the meantime).

To keep review smooth:

  • Make sure your extension point URLs load over HTTPS and embed cleanly in an iframe.
  • Request only the scopes your plugin actually uses.
  • Provide a working support URL and documentation URL.
  • Test the whole flow on a development store before submitting: install, open, place a test order, and send a test webhook for every topic you handle. See Testing your plugin.

Pre-submission checklist

Before you submit, confirm:

  • Your app is served over HTTPS.
  • Your app sends the iframe CSP header: Content-Security-Policy: frame-ancestors 'self' https://*.orbitcommerce.net https://*.myorbitcommerce.net.
  • oauth.scopes lists only the scopes you actually use (singular <resource>:<action> form).
  • baseUrl and every extensionPoints[].url point at your hosted app and load in an iframe.
  • supportUrl and documentationUrl are real, working links.
  • You have tested a full install on a development store (how): the plugin mounts, orbit.ready() resolves, your core flows work, and your webhooks verify.
  • Your current release is the one you want reviewed, and its version is a valid semver string.
  • Listing fields are within their length limits.

Next steps