← Documentation overview

Webhook Key Delivery

Deliver license keys from your own server or SellAuth storefront.

Copy this reference as markdown for an AI: perfect for implementation assistance or clarifying questions.

Webhook key delivery

Generate System Locker license keys after a purchase using a Default connection from your server or SellAuth Dynamic Delivery. Both require Lifetime or an active subscription. Connections become inactive when access expires or an account is on hold.

Default key delivery

This is the simplest option if you have a custom shop site or selling solution.

Set up your connection

  1. Open Account → Webhook connections → Default, create a signing key, and copy it before closing the window. Store it on your server; future visits show only its prefix. Rotating it immediately stops requests signed with the previous key.
  2. Open Systems, select your system, and expand Webhook mappings.
  3. Select Default, enter your product ID and keys per unit, and choose the expiration and any optional key settings. Default uses no variant ID. Each system/product can have one Default mapping, including disabled mappings. If you also select SellAuth, the variant applies only to SellAuth.
  4. After your server confirms payment, send one request per purchased order item to POST https://systemlocker.net/webhook/v1.

System Locker trusts your signed order.item.paid event. Your server is responsible for confirming payment before sending it.

Request body

Send Content-Type: application/json. Serialize the body once and save its exact bytes with your order item and idempotency key before sending:

{
  "event": "order.item.paid",
  "system_id": "my-system",
  "source": "my-store",
  "order_id": "order-1042",
  "item_id": "line-1",
  "product_id": 123,
  "quantity": 2
}
Field Required value
event Exactly order.item.paid.
system_id The selected system's ID as a string, 1–75 characters.
source A stable storefront identifier as a string, 1–128 characters. Use different sources for storefronts with overlapping order IDs.
order_id Your order ID as a string, 1–191 characters.
item_id A stable line-item ID within that order as a string, 1–191 characters. Different lines require different IDs even when they contain the same product.
product_id A JSON integer matching the Default mapping, from 1 to 9,007,199,254,740,991.
quantity Purchased units as a JSON integer from 1 to 100.

String identifiers use printable ASCII without spaces or control characters. All fields are required. Additional fields, including key-setting overrides, variants, and payment status, are rejected. The body cannot exceed 16 KiB. Send failed or unpaid orders only after they have become paid; do not send them as order.item.paid.

The number of generated keys is quantity × keys per unit, with a maximum of 100 keys per delivery. The entire batch must fit your current plan's key capacity. Keys are regular licenses. Their expiration, notes, format, and reseller assignment come from the mapping; expiration begins on redemption.

Sign each attempt

Supply these headers:

Idempotency-Key: store-order-1042-line-1
X-Timestamp: <current Unix timestamp in seconds>
X-Signature: v1=<lowercase HMAC-SHA256 hex digest>

Idempotency-Key is a stable identifier for this delivery: 1–255 printable ASCII characters without spaces or control characters. Keep it unchanged across retries and unique across your developer account's Default deliveries.

Compute the signature over the exact UTF-8 bytes of:

v1.<Idempotency-Key>.<X-Timestamp>.<raw JSON body>

The dots are literal separators. Use the complete Default key, including its slw_ prefix, as the UTF-8 HMAC key; do not strip the prefix or decode its hexadecimal characters. Prefix the resulting lowercase hexadecimal digest with v1= for the header.

The receiver uses a constant-time signature comparison. The timestamp must be within five minutes of server time, including future timestamps. Keep your server clock synchronized. A fresh timestamp and signature are required for later retries; the body and idempotency key remain unchanged.

Successful response

The endpoint responds synchronously with HTTP 200 and Content-Type: application/json:

{
  "delivery_id": "delivery_abc123",
  "keys": ["SYSTEM-AAAA-BBBB-CCCC", "SYSTEM-DDDD-EEEE-FFFF"]
}

Treat delivery_id as an opaque string. Store the result with your order item before delivering the keys to the customer. The response is marked Cache-Control: private, no-store.

All keys and their receipt are committed together. If issuance fails, no partial batch is issued. An identical retry returns the original response bytes and original keys, including after mapping changes, mapping deletion, or license deletion. The connection must still have active access, and retries must be signed with its current key.

System Locker deduplicates both the idempotency key and the signed source + order_id + item_id identity across your account's Default deliveries. Changing the idempotency key does not create a second delivery for the same order item. Reusing either identity with a different body returns 409, including changing the system, product, or quantity. Keep the exact JSON body unchanged, including whitespace and field order.

Errors and retries

Errors use JSON:

{"error":{"code":"MAPPING_UNAVAILABLE","message":"An enabled Default mapping is required for this product."}}
HTTP status Meaning / action
400 Invalid JSON or idempotency key. Correct the request.
401 Invalid signature or timestamp. Check the current secret, signing bytes, and clock.
403 Connection or account access is inactive. Restore access.
404 The system became unavailable during processing. Review the target system.
405 Use POST.
409 An identifier conflicts with an existing delivery. Reconcile the order; do not generate a new identity to bypass the conflict.
413 Body exceeds 16 KiB.
415 Send application/json.
422 Invalid fields, missing/disabled mapping, unsupported quantity, invalid key settings, or insufficient key capacity. Correct the cause before retrying.
429 Too many new deliveries. Retry with backoff and honor Retry-After: 5.
503 Temporary delivery failure. Retry with backoff.

Unknown systems or missing Default connections fail signature verification with 401. Successful retries bypass the new-delivery rate limit of 120 requests per minute per developer. Neither errors nor retries generate additional keys.

Retry network failures, timeouts, 429, and 503 with increasing delays and jitter. A timeout may occur after keys were committed, so always retry the saved body and idempotency key. Other errors require attention before retrying. Retain unresolved requests for later reconciliation if your retry budget runs out.

Node.js signing and retry example

The following helper needs Node.js 20 or later and uses the complete secret from SYSTEM_LOCKER_WEBHOOK_SECRET. Persist body and idempotencyKey with the paid order item first, then call:

const delivery = await deliverPaidItem({ body, idempotencyKey });
// Persist delivery.delivery_id and delivery.keys with the order item.

The helper below keeps the body and identifier stable, signs each attempt with a fresh timestamp, retries transient failures, and honors Retry-After:

import { createHmac } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';

export async function deliverPaidItem({ body, idempotencyKey, secret = process.env.SYSTEM_LOCKER_WEBHOOK_SECRET }) {
    if (typeof body !== 'string' || typeof secret !== 'string' || ! secret || typeof idempotencyKey !== 'string'
        || idempotencyKey.length < 1 || idempotencyKey.length > 255 || /[^\x21-\x7e]/.test(idempotencyKey)) {
        throw new Error('Supply the saved JSON body, idempotency key, and Default secret.');
    }
    let lastFailure;
    for (let attempt = 0; attempt < 8; attempt++) {
        const timestamp = String(Math.floor(Date.now() / 1000));
        const signature = createHmac('sha256', secret)
            .update(`v1.${idempotencyKey}.${timestamp}.${body}`, 'utf8').digest('hex');
        let response;
        let responseBody;
        try {
            response = await fetch('https://systemlocker.net/webhook/v1', {
                method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10000),
                headers: {
                    'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey,
                    'X-Timestamp': timestamp, 'X-Signature': `v1=${signature}`,
                },
                body,
            });
            responseBody = await response.text();
        } catch (error) {
            lastFailure = error;
        }
        if (responseBody !== undefined) {
            if (response.status === 200) {
                const delivery = JSON.parse(responseBody);
                if (typeof delivery.delivery_id !== 'string' || ! Array.isArray(delivery.keys)
                    || ! delivery.keys.length || ! delivery.keys.every(key => typeof key === 'string')) {
                    throw new Error('Unexpected delivery response. Keep the saved request for reconciliation.');
                }
                return delivery;
            }
            let error;
            try { error = JSON.parse(responseBody).error; } catch { /* An upstream error may not be JSON. */ }
            lastFailure = new Error(error?.message || `Delivery returned HTTP ${response.status}.`);
            if (response.status !== 429 && response.status !== 503) throw lastFailure;
        }
        if (attempt === 7) break;
        const retryAfter = Number(response?.headers.get('Retry-After') || 0);
        const backoff = Math.min(30, 2 ** attempt) + Math.random();
        await delay(Math.max(backoff, Number.isFinite(retryAfter) ? Math.max(0, retryAfter) : 0) * 1000);
    }
    throw lastFailure;
}

SellAuth key delivery

Connect SellAuth Dynamic Delivery to generate System Locker license keys when a customer purchases your product. Webhook connections require Lifetime or an active subscription. Saved connections and mappings become inactive when access expires or an account is on hold.

Connect your storefront

  1. Find your webhook secret in SellAuth under Storefront → Configure → Miscellaneous.
  2. Open Account → Webhook connections in System Locker, enter the secret in the SellAuth section, and save it. System Locker displays only its prefix on future visits. To change it, save the new secret here after updating your SellAuth storefront.
  3. Open Systems, select the system to license, and expand Webhook mappings.
  4. Start a new mapping and select SellAuth as its webhook connection. Enter your SellAuth product ID and variant ID. Leave the variant blank only for a product without a variant. Choose your keys per unit, expiration, notes, and any optional key settings.
  5. Copy the system's SellAuth delivery URL into the product's Dynamic Delivery settings in SellAuth. Use the URL shown in System Locker, which includes the selected system's ID.

Every product and variant pair has one mapping per system. You can edit, disable, or delete a mapping. Disabling or deleting it stops new deliveries through that mapping; previously issued keys and delivery receipts are retained.

Generated keys

The mapping's key count is multiplied by the purchased item's quantity. For example, two keys per unit and an item quantity of three creates six keys. Each delivery can create up to 100 keys and must fit your System Locker plan's key capacity.

Expiration begins when a key is redeemed. Webhook mappings create regular license keys and support the same fixed and custom durations as the portal's Key Creator Tool, optional notes, custom formats where your plan permits them, and reseller assignment on qualifying plans.

Each successful delivery returns HTTP 200 with one key per line. SellAuth shows those keys to the customer. Recent deliveries on the Systems page shows the latest 25 successful deliveries, their idempotency keys, and the keys issued.

Verification and retries

SellAuth sends POST /webhook/sellauth/{system} with Content-Type: application/json, Idempotency-Key, and X-Signature. System Locker verifies the hexadecimal HMAC-SHA256 signature against the raw request body using your saved secret. X-Timestamp is informational and does not authorize a delivery.

Only the INVOICE.ITEM.DELIVER-DYNAMIC event is accepted. The invoice must be completed or partially_completed. Failed invoices and failed items never issue keys. The item must identify its invoice, product, optional variant, status, and a positive quantity.

System Locker records both the idempotency key and the signed shop, invoice, and item IDs. A repeat of the same delivery returns its original keys, even if you have since changed the mapping or deleted those licenses. A conflicting request or a replay to another system is rejected. Keep each retry's body unchanged.

Temporary failures return 503; excessive new delivery requests return 429 with Retry-After: 5. These responses let SellAuth retry. Invalid signatures, failed orders, missing or disabled mappings, and unsupported quantities return an error without issuing keys. These errors need seller attention. Successful retries do not create additional keys or consume the new-delivery rate limit.

See SellAuth's Dynamic Delivery documentation for storefront configuration and delivery behavior.