Use cases
Auto-issue keys from a store
Wire a store or Merchant-of-Record to Keyright so a paid order mints a license key and emails it to the buyer automatically — with HMAC-verified events, idempotent retries, and refund-driven revocation.
Goal. A customer pays on your store (or a Merchant-of-Record that sells on your behalf), and a Keyright license is minted and emailed to them with no manual step — and a refund or cancellation revokes it just as automatically.
When to use it
- Use this if you sell through a checkout — your own store, a payment provider, or a Merchant-of-Record (MoR) that handles tax and billing — and you want fulfillment to be hands-off.
- Use this if you want refunds and cancellations to cut off access without someone remembering to revoke.
- Not this if you issue keys by hand to a handful of enterprise accounts (just use Licenses → Issue license or
POST /admin/licenses), or if your billing system already calls the admin API itself — the webhook is a convenience, not the only path.
This is primarily a Cloud convenience — the hosted service is already reachable from the public internet, so a store can POST to it directly. It works self-hosted too, as long as your instance is reachable by the store (a public URL or a tunnel the provider can hit); everything below is identical bar the base URL.
What you do (vendor side)
1. Get your webhook URL and secret
Every tenant has a per-tenant fulfillment webhook at POST /webhooks/fulfill/{slug} (where {slug} is your tenant) and a secret used to HMAC-sign events. The tenant is taken from the route and forced onto the event, so a signature valid for one tenant can never mint a license in another.
Find both in the dashboard’s integration/developer settings, or over the API: GET /admin/account returns an integration block with the base URL, signing public key and webhook URL. Rotate the secret any time (owner role):
curl -X POST $BASE/admin/webhook-secret/rotate -H "X-Admin-Token: $TOKEN"
# -> { "webhookSecret": "…", "webhookUrl": "https://keyright.delta1labs.com/webhooks/fulfill/acme" }
2. Point your store at it
In your store / payment provider / MoR, add a webhook that fires on the order lifecycle and set:
- URL — your
/webhooks/fulfill/{slug}. - Signing secret — the
webhookSecretabove. The provider HMAC-signs the raw request body and sends it in theX-Keyright-Signatureheader; Keyright verifies it with your secret and rejects anything that doesn’t match (401).
Most stores emit their own event shape. Keyright expects a normalized event, so if your provider’s payload differs, put a thin adapter in front (a small function that translates the provider’s webhook into the normalized body, signs it, and forwards it). The normalized body:
{
"type": "paid",
"licensee": "Acme Inc.",
"product": "acme-app",
"tier": "pro",
"seats": 3,
"expiryUtc": "2027-10-12T00:00:00Z",
"orderRef": "order_8421",
"email": "buyer@acme.com"
}
typedrives the action: a paid event (paid/order.paid/issue) mints a license; a refund / cancel / revoke event revokes it.productandtierare how you map the store’s product/variant to a Keyright product + edition. Your store sells a SKU; you decide which Keyrightproductslug and whichtier(edition) that SKU corresponds to, and set those fields in the event (directly, or in your adapter’s mapping table). The edition’s seats, entitlements and any coverage policy are applied automatically at issue, exactly as for a hand-issued license — so a store-fulfilled Pro key is indistinguishable from one issued in the dashboard.seats/expiryUtcoverride the edition defaults when the SKU needs it (e.g. a 5-seat pack, or an annual term).orderRefis the idempotency anchor (next section) — always send your store’s order id.emailis where the key is sent.
A tenant field in the body is ignored on the per-tenant route — the slug in the URL always wins.
3. What the customer receives
On a paid event Keyright mints the license and emails the key to email (the instance needs SMTP configured for the mail to go out). The buyer pastes that key into your app and activates it like any other license — see Getting started. Nothing about activation, seats or entitlements differs because the key came from a store.
Idempotency & retries
Stores retry webhooks — on a timeout, a 5xx, or a provider hiccup you will get the same event more than once. Keyright makes paid idempotent per orderRef: the first delivery mints the license, and every later delivery with the same orderRef returns the existing license instead of issuing a second key. That is why orderRef must be stable (your store’s order id, not a per-attempt id). The endpoint answers 200 for every handled outcome — issued, already exists, revoked, or ignored — so a well-behaved store sees success and stops retrying.
Refund/cancel events revoke the license that order minted; the customer’s app drops to Free on its next lease refresh (see Revoke, renew & handle expiry). A refund for an order that was never fulfilled is a no-op (ignored, still 200).
In your code
There is no app-side SDK work for fulfillment — it is a server-to-server flow between your store and Keyright, and the customer’s app only ever sees an ordinary license key. The only code you might write is the optional adapter that normalizes your provider’s webhook. Sketch:
// Minimal adapter: translate a provider event into Keyright's normalized, HMAC-signed body.
var evt = new {
type = providerEvent.IsPaid ? "paid" : "refund",
licensee = providerEvent.CustomerName,
product = MapToKeyrightProduct(providerEvent.Sku), // your SKU → Keyright product slug
tier = MapToKeyrightTier(providerEvent.Sku), // your SKU → edition
seats = providerEvent.Quantity,
orderRef = providerEvent.OrderId, // the idempotency anchor
email = providerEvent.CustomerEmail,
};
var body = JsonSerializer.SerializeToUtf8Bytes(evt);
var sig = Convert.ToBase64String(new HMACSHA256(secretBytes).ComputeHash(body)); // over the RAW bytes
using var http = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post, "https://keyright.delta1labs.com/webhooks/fulfill/acme")
{ Content = new ByteArrayContent(body) };
req.Content.Headers.ContentType = new("application/json");
req.Headers.Add("X-Keyright-Signature", sig);
await http.SendAsync(req);
If your provider can post a custom JSON body and a custom signing scheme directly (some can), you may not need an adapter at all — point it straight at the webhook with the normalized shape above.
Cloud vs Self-hosted
- Reachability is the one real difference: Cloud is public, so a store reaches
https://keyright.delta1labs.com/webhooks/fulfill/{slug}out of the box; a self-hosted instance must be reachable by the store at a URL it can POST to. - The legacy single-tenant route. A single-tenant self-hosted instance may instead use the deployment-wide
POST /webhooks/fulfillwith aKEYRIGHT_WEBHOOK_SECRET. Prefer the per-tenant/webhooks/fulfill/{slug}route whenever the instance has more than one tenant. $BASE/$TOKEN/ SMTP follow the usual split; the emailed-key step needs mail configured on whichever instance mints the license.
Verify
- Rotate the webhook secret and note it; map one store SKU to
product=acme-app,tier=pro. - Send a test paid event with a real
emailand a freshorderRef, signed with the secret. Expect200, a new license in Licenses, and the key in the inbox. - Idempotency: re-send the exact same event. Expect
200and no second license — the same key comes back. - Bad signature: send the same body with a wrong signature. Expect
401and no license. - Refund: send a refund event for that
orderRef. The license flips to revoked; the customer’s app drops to Free on the next refresh.
See also
- Getting started — the vendor setup and what the buyer does with the key.
- Revoke, renew & handle expiry — what a refund-driven revoke does to a running app.
- Gate features & editions — the entitlements a store-issued edition carries.
- HTTP API reference — the webhook and admin endpoints in full.