Skip to content

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.

Store / MoRbuyer paysproduct + variantHMACPOST /webhooks/fulfill/{slug}1. verify X-Keyright-Signature2. map → product + edition3. mint license (idempotentper orderRef)4. email the keyBuyer gets emailed keypastes it into your apprefund / cancel→ revokes same licenseThe tenant is taken from the route and forced onto the event, so a signature valid for one tenant can never mint in another.A duplicate "paid" for the same orderRef returns the existing license instead of issuing a second one.Keyright never processes the payment — it reacts to the normalized event your store (or a thin adapter) sends.
A paid order becomes a signed, emailed license key; a refund revokes it — all from one HMAC-verified webhook.

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 webhookSecret above. The provider HMAC-signs the raw request body and sends it in the X-Keyright-Signature header; 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"
}
  • type drives the action: a paid event (paid / order.paid / issue) mints a license; a refund / cancel / revoke event revokes it.
  • product and tier are how you map the store’s product/variant to a Keyright product + edition. Your store sells a SKU; you decide which Keyright product slug and which tier (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 / expiryUtc override the edition defaults when the SKU needs it (e.g. a 5-seat pack, or an annual term).
  • orderRef is the idempotency anchor (next section) — always send your store’s order id.
  • email is 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/fulfill with a KEYRIGHT_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

  1. Rotate the webhook secret and note it; map one store SKU to product=acme-app, tier=pro.
  2. Send a test paid event with a real email and a fresh orderRef, signed with the secret. Expect 200, a new license in Licenses, and the key in the inbox.
  3. Idempotency: re-send the exact same event. Expect 200 and no second license — the same key comes back.
  4. Bad signature: send the same body with a wrong signature. Expect 401 and no license.
  5. 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