Webhooks
LicenChain notifies your backend about lifecycle milestones by sending signed POST requests to an
HTTPS endpoint you register during Seller onboarding.
Events
Section titled “Events”| Event | Meaning | Your action |
|---|---|---|
onboarding.completed |
The Buyer accepted the applicable terms version and completed onboarding. | Mark the order as accepted and record termsVersion. Do not grant licensed access yet. |
licence.issued |
The one corporate license for the licensing request has been issued. | Store data.licenceKey with the order, together with the validity period, seatLimit and accepted termsVersion. |
onboarding.completed
Section titled “onboarding.completed”{ "schemaVersion": "1.0", "eventId": "evt_5N1R8T2W6Y0B", "type": "onboarding.completed", "occurredAt": "2026-11-12T14:05:00Z", "environment": "sandbox", "externalOrderId": "SELLER-PO-EXAMPLE-001", "requestId": "lreq_7Q2M9K4X8P3D", "data": { "onboardingStatus": "completed", "sellerId": "sel_3J6K9M2P5R8T", "productRef": "ai-specialist-standard", "buyerOrganizationId": "org_2B5D8F1H4K7M", "termsVersion": "2026.2", "termsAcceptedAt": "2026-11-12T14:04:51Z", "transactions": [ { "transactionId": "txn_8F4D2K9Q1M7C" } ] }}licence.issued
Section titled “licence.issued”{ "schemaVersion": "1.0", "eventId": "evt_9C3V7X1Z5A2D", "type": "licence.issued", "occurredAt": "2026-11-12T14:20:00Z", "environment": "sandbox", "externalOrderId": "SELLER-PO-EXAMPLE-001", "requestId": "lreq_7Q2M9K4X8P3D", "data": { "licenceKey": "11155111:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:42", "statusAtIssuance": "not_yet_valid", "validFrom": "2027-01-01T00:00:00Z", "validUntil": "2028-01-01T00:00:00Z", "seatLimit": 25, "sellerId": "sel_3J6K9M2P5R8T", "productRef": "ai-specialist-standard", "buyerOrganizationId": "org_2B5D8F1H4K7M", "licenceHolderId": "org_2B5D8F1H4K7M", "termsVersion": "2026.2", "termsAcceptedAt": "2026-11-12T14:04:51Z", "onboardingEventId": "evt_5N1R8T2W6Y0B", "transactionId": "txn_8F4D2K9Q1M7C" }}| Field | Description |
|---|---|
externalOrderId, requestId (envelope) |
Link the license to your order and to the licensing request that produced it. |
data.licenceKey |
Canonical license identifier from the actual issuance. Store it exactly as received; it is the same key you send to Verify license. |
data.statusAtIssuance |
Initial state: active, or not_yet_valid when issued before validFrom. A snapshot only — the current state comes from verification. |
data.validFrom, data.validUntil |
License period in UTC (validFrom inclusive, validUntil exclusive). |
data.seatLimit |
Seat or unit allowance recorded on this one license (the request quantity), or null when the product has none. Your delivery system enforces it. |
data.sellerId |
Your LicenChain Seller account ID. Informational; your endpoint and signing secret already identify you. |
data.productRef |
The registered product that was licensed. |
data.buyerOrganizationId |
LicenChain ID of the Buyer organization. |
data.licenceHolderId |
LicenChain ID of the organization assigned the rights in this license record. It may differ from the Buyer; never assume they are equal. |
data.termsVersion, data.termsAcceptedAt |
The product terms version the Buyer accepted and when. Matches onboarding.completed. |
data.onboardingEventId |
eventId of the onboarding.completed event that recorded the acceptance. |
data.transactionId |
LicenChain support reference. Not a license identifier. |
Payloads contain identifiers only — never Buyer names, email addresses, onboarding links, API keys or
other secrets. Look up names in your own order records by externalOrderId.
Authentication
Section titled “Authentication”Webhooks are sent by LicenChain to you, so they are not authenticated with your Seller API key
or any Authorization header. Each delivery is authenticated by an HMAC signature computed with the
webhook signing secret issued for your endpoint during Seller onboarding. In the
OpenAPI target document this is the webhookSignature security scheme, set on
both webhook operations.
Each delivery carries these headers:
| Header | Description |
|---|---|
LC-Timestamp |
Unix time in seconds when the delivery was signed. |
LC-Event-Id |
Same value as eventId in the body. Use it to de-duplicate. |
LC-Signature |
v1= followed by the lowercase hex HMAC-SHA256 of <LC-Timestamp>.<raw request body>, keyed with your webhook signing secret. |
To verify a delivery:
- Read the raw request body bytes before parsing JSON. Re-serialized JSON will not match.
- Compute
HMAC-SHA256(signingSecret, "<LC-Timestamp>.<raw body>")as lowercase hex and prefix it withv1=. - Compare it with
LC-Signaturein constant time. - Reject the delivery with
401if the signature does not match orLC-Timestampis more than five minutes away from your clock.
import { createHmac, timingSafeEqual } from 'node:crypto';
const signingSecret = process.env.LICENCHAIN_WEBHOOK_SECRET ?? '';const MAX_AGE_SECONDS = 300;
// rawBody must be the exact bytes received, before any JSON parsing.export function isAuthenticLicenChainEvent(rawBody: string, headers: Headers): boolean { const timestamp = headers.get('LC-Timestamp') ?? ''; const signature = headers.get('LC-Signature') ?? ''; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!/^\d{10}$/.test(timestamp) || age > MAX_AGE_SECONDS) return false;
const expected = 'v1=' + createHmac('sha256', signingSecret).update(`${timestamp}.${rawBody}`).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b);}import hashlibimport hmacimport osimport time
SIGNING_SECRET = os.environ["LICENCHAIN_WEBHOOK_SECRET"].encode()MAX_AGE_SECONDS = 300
def is_authentic_licenchain_event(raw_body: bytes, headers) -> bool: """raw_body must be the exact bytes received, before any JSON parsing.""" timestamp = headers.get("LC-Timestamp", "") signature = headers.get("LC-Signature", "") if not (timestamp.isdigit() and len(timestamp) == 10): return False if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS: return False signed = timestamp.encode() + b"." + raw_body expected = "v1=" + hmac.new(SIGNING_SECRET, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature)Delivery and retries
Section titled “Delivery and retries”- Respond with any
2xxstatus once you have durably recorded the event. Do the real work asynchronously. - Deliveries are at least once. De-duplicate by
eventIdand return2xxfor duplicates. - Events can arrive out of order. Use
type,occurredAtand your stored state, not arrival order. - Non-
2xxresponses, timeouts and network errors are retried with exponential backoff for a bounded period. Signature or validation failures on your side should return4xxand be investigated.
Missed licence.issued deliveries
Section titled “Missed licence.issued deliveries”Verification cannot recover a missed issuance. Verify license requires the
canonical licenceKey, and in this contract version licence.issued is the only way that key is
delivered to you. If every retry of that event fails — for example because your endpoint was down for
longer than the retry period — you do not have the key, and you must not grant licensed access for that
order.
Detect the gap yourself: an order that received onboarding.completed but no licence.issued within
your expected time is a reconciliation case. Never derive, guess or reconstruct a licenceKey, and do
not substitute requestId, externalOrderId or transactionId for it.
Support-assisted recovery — target procedure, not yet operational
Section titled “Support-assisted recovery — target procedure, not yet operational”The steps below describe the intended recovery process, not a currently implemented or
guaranteed support service. Secure manual event redelivery and its operating procedures still need
implementation and acceptance. Until a verified recovery is available, deny licensed access for
orders without a canonical licenceKey:
- Fix and test your webhook endpoint and signature verification first.
- Contact LicenChain support through your onboarding channel and quote the
requestIdandexternalOrderId(andtransactionId, if you receivedonboarding.completed). Never send your API key, signing secret or onboarding link. - Ask LicenChain support to investigate issuance status and identify an authenticated recovery method. Do not assume signed event redelivery can currently be performed.
- If LicenChain has implemented and validated secure redelivery, process the recovered
licence.issuedevent like the original (verify the signature, de-duplicate byeventId, store the canonicallicenceKey), then verify the license before granting access. Otherwise continue to deny access until recovery is available.
Self-service reconciliation (separate future capability)
Section titled “Self-service reconciliation (separate future capability)”A Seller-scoped licensing-request status lookup by requestId or externalOrderId — returning the
request status and, once issued, the canonical licenceKey — is the intended long-term reconciliation
mechanism. It is a separate future capability: it is not part of this contract version, not in the
OpenAPI target document, and must not be relied on. This delivery focuses on the
two primary APIs: Submit a licensing request and
Verify license.