# Testing your plugin

Install your plugin on a development store of your own and exercise it end to end, before it is reviewed, listed or seen by any merchant.

A development store is a real store in your partner organisation, flagged as a development store. Your plugin installs on it through the same path a merchant install takes, so scopes, tenant isolation and auditing behave exactly as they will in production. What you test is what merchants will get. The store itself is described in [Development stores](/guides/development-stores); this guide is the testing loop that runs on one.

## What you need

| | Where | What it gives you |
|---|---|---|
| **Partner account** | [partner.myorbitcommerce.net](https://partner.myorbitcommerce.net) | Your plugin: identity, scopes, releases, the webhook signing secret |
| **A development store** | Partner dashboard, **Development stores** | A store you own, with sample data and test payments, to install the plugin on |

Development stores are a partner feature that Orbit switches on for your account. Your email address must be verified before you can create one, and you can keep a small number open at a time. If the Development stores page says they are not available yet, they have not been switched on for you.

## 1. Create a development store

In the partner dashboard open **Development stores** and choose **Create development store**. Give it a name (it becomes the store's web address), an optional private label, and leave **Add sample products, customers and orders** ticked unless you want an empty store.

The store is ready in a few seconds. Sample data is added in the background; the card shows **Sample data: adding** and then **Sample data: added**.

**Open store** signs you in to the store dashboard as the store's owner. The storefront address is on the card.

Renaming, closing, restoring, sample data top-ups and the API are all in [Development stores](/guides/development-stores).

## 2. Install your plugin on it

Open your plugin in the partner dashboard. The Overview tab has a **Test on a development store** panel listing your open development stores, each with its state: **Not installed**, **Installed, release N**, or **Development release N**.

Choose **Install**. The plugin is installed and activated on that store on its current production release, with every scope the release requests granted. You own both sides, so there is no consent screen. A draft that has never been reviewed installs here just the same, because the store is yours. For an unlisted or private draft, **Install on a development store** is the primary action on the page.

**Open in store** signs you in to the store dashboard and lands on the plugin's page there.

The panel calls:

```http
POST /api/plugins/mine/{pluginId}/dev-stores/{storeId}/install
Authorization: Bearer <partner JWT>
```

Add `?channel=development` to install on the plugin's latest development release instead (step 3). The response is `{ storeId, storePluginId, installed, channel, releaseNumber, openUrl }`. `installed` is `false` when the plugin was already there. `openUrl` is the plugin's page in the store dashboard as a path; pass it as `next` to the store's login token route to sign in and land on it. `DELETE` on the same path uninstalls.

The plugin routes on this page (installing on a store, development releases, test webhooks and the signing secret) need the `partner:plugin:dev-release` permission, which organisation owners, organisation admins and partner admins hold.

## 3. Point the store at your local app

Your plugin is your own app, and the store dashboard embeds it from the URL in your release. While you build, that URL should be your machine.

Orbit reaches your app from the outside, so it needs a public https address. Run a tunnel to your local port (Cloudflare quick tunnels and ngrok both work) and use the https URL it gives you as your plugin's `baseUrl`.

`http://localhost:3000` is refused, and not by the browser: the **release validator** refuses it. Every release cut against the production API is validated as a production release, so every URL must use https and must not point at `localhost` or `127.0.0.1`. A development release additionally refuses private and loopback addresses, because Orbit could never deliver a webhook to one. The error names the URL and the rule it broke.

### Cut a development release

A development release is a release of your plugin that runs only on your development stores. Production releases never see it, merchants never see it, and it can never be activated.

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

| Field | Type | Effect |
|---|---|---|
| `manifest` | object | Your `plugin.json` configuration with `baseUrl` set to the tunnel URL. Paths stay relative |
| `devStoreIds` | uuid[] | Development stores (their `storeId`) to install the plugin on if needed and run this release on. Optional |
| `notes` | string | Up to 500 characters, shown in the Releases list. Optional |
| `env` | `dev`, `staging` or `prod` | Optional. Floored to the environment the API runs in, so against production it is always `prod` |

```bash
curl -X POST \
  'https://api.myorbitcommerce.net/api/plugins/mine/order-sync/dev-releases' \
  --header 'Authorization: Bearer <partner JWT>' \
  --header 'Content-Type: application/json' \
  --data '{
    "manifest": {
      "version": "1.0.0",
      "baseUrl": "https://your-tunnel.trycloudflare.com",
      "extensionPoints": [
        { "target": "dashboard.main.embed", "url": "/embed", "label": "Order Sync" }
      ],
      "oauth": {
        "scopes": ["order:read", "order:list"],
        "webhooks": [{ "topic": "order.created", "path": "/webhooks/orbit" }]
      },
      "ui": { "menu": { "label": "Order Sync", "icon": "puzzle" } }
    },
    "devStoreIds": ["<store uuid>"]
  }'
```

The response:

```json
{
  "releaseNumber": 4,
  "channel": "development",
  "deduplicated": false,
  "appliedTo": [{ "storeId": "<store uuid>", "storePluginId": "<install uuid>" }],
  "openUrl": "/stores/<store slug>/plugins/order-sync"
}
```

From that moment the store dashboard loads your plugin from the tunnel, and the plugin's webhook subscriptions on that store point at the tunnel too. Sending the same manifest again returns the existing release with `deduplicated: true`. A `400` means the manifest failed validation; the body lists `errors: [{ path, code, message }]`.

Every tunnel restart gives you a new URL, so cut a new development release each time. Your plugin keeps the newest 20 development releases that no store is running; older ones are cleaned up.

### Stop using it

```http
DELETE /api/plugins/mine/{pluginId}/dev-releases/current?storeId=<store uuid>
```

takes the store back to the plugin's production release and re-points its webhooks. Leave `storeId` out to do it on every development store at once. It returns `{ cleared: [{ storeId, storePluginId, releaseNumber }] }` and is safe to repeat.

In the partner dashboard, the Technical tab's **Releases** card lists every release with a **Development** or **Production** badge, and a **Development releases in use** list with **Stop using development release** for each store.

## 4. Exercise it

Choose **Open in store**. Your plugin loads in the store dashboard, the iframe handshake fires, the SDK receives the session token and store id, and your API calls are scoped to that store.

From here the loop is fast: edit locally, your tunnel serves the change, refresh the iframe. You only cut a new development release when `plugin.json` changes or the tunnel URL does.

### Place a test order

Open the storefront (the address is on the store's card), add a product to the basket and check out. Development stores offer one payment method, **Test payment**: no money is taken and the order is marked paid straight away. The order lands as paid, so `order.created` fires and then `payment.succeeded`, the same events a real paid order produces. Order emails, stock movements and everything downstream behave as they do for a paid order.

Card entry, wallets and 3-D Secure are not part of a development store, so a plugin whose job is payment processing cannot be tested end to end on one.

### Send a test webhook

In the **Test on a development store** panel, choose **Test webhooks** on a store the plugin is installed on, pick a topic and choose **Send test webhook**. The topics offered are the ones the release running on that store declares in `oauth.webhooks`. The result tells you whether your endpoint took it: **Delivered (200)**, the status your endpoint answered, or that Orbit could not reach the URL.

Test webhooks go only to your own development stores, never to a merchant's store. The delivery is signed with your plugin's signing secret and carries the same headers as a real one plus `X-Orbit-Webhook-Test: true`. The verification recipe, the API route and where the secret lives are in [Webhooks](/guides/webhooks#test-webhooks).

### Calling the API outside the iframe

For Postman, curl, or the [API reference](/api-reference), mint a session token for the store:

```http
POST /api/plugins/{pluginId}/session-token
Authorization: Bearer <admin JWT for the store>
x-store-id: <store uuid>
```

That token is valid for **one hour**. For longer runs, exchange it:

```http
POST /oauth/token/exchange
Authorization: Bearer <session token>
```

which returns an access token (**one hour**) plus a refresh token (**90 days**). Refresh via `POST /oauth/token/refresh`. There is no long-lived access token; see [Authentication](/guides/authentication).

## Install links are for merchants

An install link installs a live unlisted or private plugin on a merchant's store. It is a distribution channel, not a test channel: links only work once the plugin is live, and until then a merchant who opens one is told the link is not valid. To test your own plugin, use a development store. When you are ready for merchants, see [Publishing](/guides/publishing#make-it-available-to-merchants).

## Troubleshooting

| Symptom | Cause |
|---|---|
| The Development stores page says they are not available yet | Development stores have not been switched on for your partner account |
| `403` creating a store | Your email address is not verified, your partner account is not active, or you are at your open-store allowance. Close one to create another |
| `429` creating a store | The daily limit for new stores has been reached, for your account or for the platform. Try again later |
| `400` cutting a development release | The manifest failed validation. `ENV_BOUND_URL` means http or localhost; `PRIVATE_HOST` means a private or loopback address Orbit cannot reach; `PLATFORM_ORIGIN` means an Orbit host |
| `403` installing on a store | You are not a member of that store; the store id is wrong or belongs to someone else |
| `404` from a development store route | The store is not one of your development stores. Another partner's store id and an unknown id are the same `404` |
| `409` installing or releasing | The store is closed (restore it first), or you asked for `?channel=development` and the plugin has no development release yet |
| `404` sending a test webhook | The plugin is not installed on that store, or the release running there does not declare the topic. The message names the topic |
| Test webhook not delivered | Your endpoint did not answer 2xx within 10 seconds, or the tunnel is down. The response carries `responseStatus` and the `url` Orbit tried |
| Iframe stays blank | The tunnel is down, or your app is missing the `frame-ancestors` header from [Getting started](/guides/getting-started#5-configure-csp-and-https-for-iframe-embedding) |
| `401` from `/v1/*` | Bad or expired token, or the install is missing that scope. Both return `401`; check the granted scopes before assuming the token is at fault |

## Next steps

- [Development stores](/guides/development-stores): the store itself, sample data, test payments and the trash
- [Webhooks](/guides/webhooks): the signing secret, verifying a delivery and test webhooks
- [Publishing your plugin](/guides/publishing): releases, review and making the plugin available to merchants
- [Scopes](/guides/scopes) and the [Scope Catalog](/scope-catalog): pick the right permissions
- [Authentication](/guides/authentication): the token model in full
