Orbit Commerce
Plugin guides

Webhooks

Subscribe your plugin to events in a merchant's store and receive them as signed HTTP requests at your own HTTPS endpoint.

What outbound webhooks are

Outbound webhooks let Orbit push events to your plugin in near real time. When something happens in a store a product is updated, an order is created, a payment succeeds Orbit sends an HTTP POST to a URL you control. Your plugin does not poll; it reacts.

Webhooks are the right tool for background work that should happen without a merchant sitting in the iframe: syncing inventory to a marketplace, emailing a receipt, reconciling fulfilments. They complement the SDK and the REST/GraphQL surfaces documented in the API Reference.

Every subscription and delivery is scoped to a single store. The store_id in each payload tells you which merchant install the event belongs to.

Subscribing

Create a subscription with POST /v1/webhooks. Authenticate with a plugin access token.

POST /v1/webhooks HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <access-token>
x-store-id: <store-id>
Content-Type: application/json

{
  "topic": "order.created",
  "webhookUrl": "https://your-plugin.example.com/webhooks/orbit"
}

Subscribing requires two scopes:

  • webhook:create always.
  • The topic's own scope. For example, to subscribe to any order.* topic you also need order:read. The mapping is in the table below, and the full list is on the Scopes guide and the Scope Catalog.
StatusMeaning
201 CreatedSubscription created.
400 Bad RequestInvalid topic or webhookUrl.
403 ForbiddenToken is missing webhook:create or the topic's required scope.
409 ConflictA subscription for this topic and URL already exists.

The webhookUrl must be HTTPS.

Managing subscriptions

Method and pathRequired scopePurpose
GET /v1/webhookswebhook:listList your store's subscriptions.
DELETE /v1/webhooks/{id}webhook:deleteRemove a subscription.
GET /v1/webhooks/{id}/deliverieswebhook:readInspect recent delivery attempts for a subscription.

Use GET /v1/webhooks/{id}/deliveries when debugging: it shows what Orbit attempted to send and how your endpoint responded.

Topic catalog

Each topic requires the listed scope at subscribe time, in addition to webhook:create.

TopicRequired scope
product.createdproduct:read
product.updatedproduct:read
product.deletedproduct:read
order.createdorder:read
order.updatedorder:read
order.canceledorder:read
order.confirmedorder:read
fulfillment.createdorder:read
fulfillment.updatedorder:read
payment.succeededorder:read
payment.failedorder:read
payment.awaiting_manualorder:read
payment.refundedorder:read
payment.partial_refundorder:read
payment.canceledorder:read
plugin.installednone (lifecycle event)
plugin.activatednone (lifecycle event)
plugin.deactivatednone (lifecycle event)
plugin.uninstallednone (lifecycle event)

The plugin.* lifecycle topics require no resource scope only webhook:create to subscribe. They are useful for provisioning and cleanup when a merchant installs or removes your plugin.

The delivery request

Orbit delivers each event as an HTTP POST to your webhookUrl:

  • Content-Type: application/json
  • User-Agent: OrbitCommerce-Webhook/1.0
  • A 10 second timeout. If your endpoint does not respond in time, the attempt counts as failed.
  • Retries up to roughly 4 attempts on any non-2xx response (or timeout). Return a 2xx status to acknowledge.

Each delivery carries these headers:

HeaderDescription
X-Orbit-Webhook-IdUnique delivery and event id. Use it as an idempotency key.
X-Orbit-Webhook-TopicThe topic, for example order.created.
X-Orbit-Webhook-SignatureHMAC-SHA256 signature, formatted sha256=<hex>.
X-Orbit-Webhook-TimestampISO 8601 time the delivery was sent.

The envelope

The request body is a JSON envelope with the same outer shape for every topic:

{
  "id": "0f3c8b2a-1d4e-4a6b-9c2f-7e5a1b2c3d4e",
  "topic": "order.created",
  "created_at": "2026-05-30T12:34:56.000Z",
  "store_id": "8a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "data": {}
}
  • id the unique event id; matches X-Orbit-Webhook-Id. Treat it as your idempotency key.
  • topic the event topic; matches X-Orbit-Webhook-Topic.
  • created_at ISO 8601 timestamp.
  • store_id the store this event belongs to. Use it to load the right merchant install record.
  • data the event-specific payload. Its fields depend on the topic and are documented in the API Reference.

Verify the signature

Always verify the signature before trusting or processing a delivery. Recompute the HMAC over the raw request body using your plugin's webhook signing secret, prefix it with sha256=, and compare against the X-Orbit-Webhook-Signature header using a timing-safe comparison.

You must verify against the exact bytes Orbit sent. Capture the raw body before any JSON parsing reorders or reformats it.

import crypto from 'node:crypto';

const WEBHOOK_SECRET = process.env.ORBIT_WEBHOOK_SECRET!;

export function verifyOrbitSignature(rawBody: Buffer, signatureHeader: string): boolean {
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? '');

  // Lengths must match for timingSafeEqual; an early length check is itself safe.
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

In Express, preserve the raw body so the bytes are not mutated before you hash them:

import express from 'express';

const app = express();

// Capture the raw body for this route only.
app.post(
  '/webhooks/orbit',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-Orbit-Webhook-Signature') ?? '';
    if (!verifyOrbitSignature(req.body, signature)) {
      return res.status(401).send('invalid signature');
    }

    const event = JSON.parse(req.body.toString('utf8'));
    // event.id, event.topic, event.store_id, event.data
    res.status(200).send('ok');

    // Do the real work after responding (queue, worker, etc.).
  },
);

Idempotency

Retries mean your endpoint can receive the same event more than once. Use the event id (also sent as X-Orbit-Webhook-Id) as an idempotency key: record processed ids and skip any you have already handled. Because deliveries can arrive out of order, prefer comparing the resource's current state in data over assuming a strict sequence.

Best practices

  • Respond 2xx fast. Acknowledge within the 10 second timeout, then do slow work asynchronously (a queue or background worker). Holding the connection open to finish processing risks a timeout and an unnecessary retry.
  • Verify before processing. Reject any request whose signature does not match before you read data. Never act on an unverified payload.
  • HTTPS only. Your webhookUrl must be HTTPS, and tokens or secrets must never appear in logs.
  • Be idempotent. Deduplicate on the event id; the same event may be delivered more than once.
  • Subscribe to lifecycle topics. Use plugin.installed and plugin.uninstalled to provision and clean up per-store state.
  • Request only the scopes you need. Each topic requires its resource scope at subscribe time; see the Scopes guide.

When a delivery looks wrong, inspect attempts with GET /v1/webhooks/{id}/deliveries, and consult the API Reference for the per-topic data shapes.