Webhooks
The licensing service can notify your own systems as things happen: it posts a signed JSON event to one HTTPS endpoint of yours when a sale is fulfilled, when a subscription changes, and when a device starts or converts a trial. You set it up in the console under Settings → Webhooks.
Setting it up
- Endpoint URL: a public
https://address of yours. Its host is resolved before every delivery, and an address that resolves to a private or internal network is refused. - Generate secret: creates the signing secret, shown once. Copy it into your endpoint's configuration; it isn't shown again. Rotate secret replaces it at any time, and your endpoint must switch to the new one: deliveries from then on are signed with it.
- Enabled: switch it on, then Save webhook settings. Events are sent only while webhooks are enabled with an endpoint and a secret.
Send test event sends a ping event, to check your endpoint end to end. Recent
deliveries lists every event with its status (pending, delivered or failed), its attempts,
the last response and the next attempt.
The sandbox has no webhooks.
Events
Every event is one JSON object:
{
"id": "6f0c0b2e-6a3d-4c2b-9f39-2b1f3c4d5e6f",
"type": "order.fulfilled",
"created_at": "2026-10-04T12:00:00Z",
"vendor_tenant_id": "sws_…_1234567890",
"data": { }
}
| Type | When | data |
|---|---|---|
order.fulfilled |
An order's licenses (and subscription) exist: a sale through Stripe, PayPal or Shopify, or an order created in the console. | order (id, order_number, date, channel, currency, total, and items, each with sku_id, quantity and the product's metadata), subscription (or null), and licenses, each with its id, sku_id and key. |
subscription.updated |
A subscription changed state or product, or received a payment. | subscription (id, provider, external_id, sku_id, state, current_period_end), change (state, sku or payment), and previous, the values before the change. |
trial.started |
A device began a trial (automatic trials). | license_id (the trial license), sku_id, device_id, started_at, ends_at. |
trial.converted |
A machine in a trial activated a purchased license of the same product. | trial_license_id, license_id (the purchased license), sku_id, device_id, converted_at. |
ping |
Send test event. | message. |
order.fulfilled carries the license keys of the sale, so keep your endpoint private and
verify every delivery. Every fulfilled order sends it, with licenses or without: a product
without license terms (a credit pack, for example) reaches your own records this way.
Verifying a delivery
Each delivery is a POST with Content-Type: application/json and these headers:
| Header | Value |
|---|---|
X-SWS-Event |
The event type. |
X-SWS-Delivery-Id |
The event's id. A retry carries the same id, so you can skip an event you already processed. |
X-SWS-Vendor-Id |
Your account's id, as in vendor_tenant_id. |
X-SWS-Signature |
t=<unix time>,v1=<signature> |
The signature is the hexadecimal HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with your signing secret: the scheme Stripe uses for its webhooks. To verify a delivery:
- Read
tandv1from the header. - Compute the HMAC-SHA256 of
<t>.<raw body>with your secret, and compare it withv1in constant time. - Refuse the delivery when
tis more than five minutes away from your clock.
Use the body exactly as received, before any JSON parsing. In Node.js:
const crypto = require('crypto');
function verifyDelivery(rawBody, signatureHeader, secret) {
const fields = Object.fromEntries(signatureHeader.split(',').map(part => part.trim().split('=')));
const t = Number(fields.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300)
return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const given = Buffer.from(fields.v1 || '', 'utf8');
return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, 'utf8'));
}
Responses and retries
Answer with any 2xx status within five seconds; redirects are not followed. Any other answer, or none, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours: seven attempts over about 35 hours. After the last one the delivery is marked failed and your account's administrators receive an email. Deliveries stay listed for 30 days.
Shopify app billing
If you sell a Shopify app through Shopify's Billing API, the licensing service can issue its licenses. The setup is in the console under Settings → Payments, in the Shopify app billing section:
- Subscribe your app's
app_subscriptions/updateandapp_purchases_one_time/updatewebhook topics to the Webhook endpoint URL shown there, inshopify.app.tomlor the Partner Dashboard. - Paste the app's client secret, from the Partner Dashboard. Shopify signs its deliveries with it, and each one is verified. It is stored write-only and never shown again.
- Keep Enabled on, then Save Shopify settings.
Name your Shopify plans and one-time purchases exactly as your products are named in the console; case and spacing don't matter. The match is then automatic:
- a subscription issues the product's license;
- a one-time purchase creates an order and no license, so give it a product without license terms (a credit pack, for example).
A name that matches no product is acknowledged, and your account's administrators are
notified. A shop has no mailbox, so the license keys reach your app through the
order.fulfilled webhook described above.