Every webhook delivery from KeyPort includes a signed JSON payload. You should verify the signature before trusting or processing the payload.
Always verify the X-KeyPort-Signature header before processing a webhook payload. Skipping verification exposes your endpoint to spoofed requests from unauthorized sources.
KeyPort includes the following HTTP headers with every delivery:
Content-Type: application/json
X-KeyPort-Event: license.created
X-KeyPort-Timestamp: 1710000000
X-KeyPort-Signature: hmac-sha256=abc123...
| Header | Description |
|---|
X-KeyPort-Event | The event type that triggered this delivery. |
X-KeyPort-Timestamp | Unix timestamp of when the event occurred. |
X-KeyPort-Signature | HMAC-SHA256 signature used to verify the payload. |
Payload structure
The request body is a JSON object with the following shape:
{
"event": "license.created",
"timestamp": "2026-04-07T00:00:00.000Z",
"data": {
"organization_id": "org_uuid",
"product_id": "product_uuid"
}
}
| Field | Description |
|---|
event | The event type that fired. Matches the X-KeyPort-Event header. |
timestamp | ISO 8601 timestamp of when the event occurred. |
data.organization_id | The UUID of your KeyPort organization. |
data.product_id | The UUID of the product associated with the event. |
Verifying the signature
The X-KeyPort-Signature header contains an HMAC-SHA256 signature. To verify it, concatenate the X-KeyPort-Timestamp value, a literal ., and the raw request body, then compute the HMAC using your whsec_... signing secret.
import crypto from 'crypto';
function verifyWebhook(
rawBody: string,
timestamp: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return `hmac-sha256=${expected}` === signature;
}
Pass the raw request body string — not a parsed JSON object — to the function. Parsing and re-serializing the body can change whitespace or key order and cause the signature check to fail.