Webhooks
Webhooks allow your application to receive real-time notifications when events occur in Licenz. Use webhooks to sync license data, trigger workflows, or update your systems automatically.
Overview
When you configure a webhook endpoint, Licenz will send HTTP POST requests to your URL whenever specified events occur. Each request contains a JSON payload with event details.
Configure Webhooks
Set up webhook endpoints in the Dashboard Settings. You can configure multiple endpoints and filter by event type.
Event Types
Subscribe to the events relevant to your integration:
| Event | Description |
|---|---|
license.created | A new license was generated |
license.revoked | A license was revoked |
license.expired | A license has expired |
license.expiring_soon | A license will expire within 7 days |
license.activated | A license was activated (first use) |
license.sync | A license sync event was received |
product.created | A new product was created |
product.updated | A product was updated |
product.deleted | A product was deleted |
Payload Format
All webhook payloads follow a consistent structure:
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier |
type | string | Event type (e.g., license.created) |
created_at | datetime | When the event occurred (ISO 8601) |
data | object | Event-specific data |
Example Payloads
license.created
Sent when a new license is generated.
{
"id": "evt_abc123def456",
"type": "license.created",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"product_id": "550e8400-e29b-41d4-a716-446655440000",
"product_name": "My Application Pro",
"product_code": "myapp-pro",
"serial": "LIC-MYAPP-A1B2C3D4",
"customer_id": "cust_12345",
"customer_email": "customer@example.com",
"status": "active",
"valid_from": "2024-01-15T10:30:00Z",
"valid_until": "2025-01-15T10:30:00Z",
"features": ["pro", "analytics"],
"max_seats": 5,
"created_at": "2024-01-15T10:30:00Z"
}
} license.revoked
Sent when a license is revoked.
{
"id": "evt_xyz789ghi012",
"type": "license.revoked",
"created_at": "2024-02-01T14:22:00Z",
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"product_id": "550e8400-e29b-41d4-a716-446655440000",
"product_name": "My Application Pro",
"product_code": "myapp-pro",
"serial": "LIC-MYAPP-A1B2C3D4",
"customer_id": "cust_12345",
"customer_email": "customer@example.com",
"status": "revoked",
"revoked_at": "2024-02-01T14:22:00Z",
"created_at": "2024-01-15T10:30:00Z"
}
} license.expired
Sent when a license expires.
{
"id": "evt_mno345pqr678",
"type": "license.expired",
"created_at": "2025-01-15T10:30:00Z",
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"product_id": "550e8400-e29b-41d4-a716-446655440000",
"product_name": "My Application Pro",
"product_code": "myapp-pro",
"serial": "LIC-MYAPP-A1B2C3D4",
"customer_id": "cust_12345",
"customer_email": "customer@example.com",
"status": "expired",
"valid_until": "2025-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:00Z"
}
} product.created
Sent when a new product is created.
{
"id": "evt_stu901vwx234",
"type": "product.created",
"created_at": "2024-01-10T08:00:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Application Pro",
"code": "myapp-pro",
"description": "Professional edition",
"default_features": ["pro", "analytics"],
"default_valid_days": 365,
"created_at": "2024-01-10T08:00:00Z"
}
} Webhook Signature Verification
All webhook requests include a signature header that you should verify to ensure the request came from Licenz and wasn't tampered with.
Signature Header
Each request includes the X-Licenz-Signature header with the format:
X-Licenz-Signature: t=1705319400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd | Component | Description |
|---|---|
t | Unix timestamp when the signature was created |
v1 | HMAC-SHA256 signature of the payload |
Verification Steps
- Extract the timestamp and signature from the header
- Check that the timestamp is within 5 minutes of current time
- Compute the expected signature:
HMAC-SHA256(timestamp + "." + payload, secret) - Compare signatures using constant-time comparison
Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const timestamp = signature.split(',')[0].split('=')[1];
const receivedSig = signature.split(',')[1].split('=')[1];
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Timestamp too old');
}
// Compute expected signature
const signedPayload = timestamp + '.' + payload;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
)) {
throw new Error('Invalid signature');
}
return JSON.parse(payload);
}
// Express.js example
app.post('/webhooks/licenz', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-licenz-signature'];
const secret = process.env.WEBHOOK_SECRET;
try {
const event = verifyWebhookSignature(req.body.toString(), signature, secret);
switch (event.type) {
case 'license.created':
handleLicenseCreated(event.data);
break;
case 'license.revoked':
handleLicenseRevoked(event.data);
break;
// ... handle other events
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(400).send('Invalid signature');
}
}); Rust Example
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn verify_webhook(
payload: &str,
signature_header: &str,
secret: &str,
) -> Result<serde_json::Value, &'static str> {
// Parse signature header: t=timestamp,v1=signature
let parts: Vec<&str> = signature_header.split(',').collect();
let timestamp = parts[0].strip_prefix("t=").ok_or("Invalid timestamp")?;
let received_sig = parts[1].strip_prefix("v1=").ok_or("Invalid signature")?;
// Check timestamp (within 5 minutes)
let ts: i64 = timestamp.parse().map_err(|_| "Invalid timestamp")?;
let now = chrono::Utc::now().timestamp();
if (now - ts).abs() > 300 {
return Err("Timestamp too old");
}
// Compute expected signature
let signed_payload = format!("{}.{}", timestamp, payload);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(signed_payload.as_bytes());
let expected_sig = hex::encode(mac.finalize().into_bytes());
// Constant-time comparison
if !constant_time_eq(received_sig.as_bytes(), expected_sig.as_bytes()) {
return Err("Invalid signature");
}
serde_json::from_str(payload).map_err(|_| "Invalid JSON")
} Always verify signatures
Never process webhook payloads without verifying the signature. This prevents attackers from sending fake events to your endpoint.
Retry Policy
Licenz will retry failed webhook deliveries with exponential backoff. Your endpoint should respond with a 2xx status code to acknowledge receipt.
Retry Schedule
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the webhook will be marked as failed and no further retries will be attempted for that event.
Timeout
Your endpoint must respond within 30 seconds. If processing takes longer, acknowledge the webhook immediately and process asynchronously.
Best Practices
1. Respond Quickly
Return a 2xx status immediately. Process the event asynchronously if needed.
app.post('/webhooks/licenz', async (req, res) => {
// Verify signature first
const event = verifyWebhookSignature(req.body, req.headers['x-licenz-signature'], secret);
// Acknowledge immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookEvent(event).catch(console.error);
}); 2. Handle Duplicates
Webhooks may be delivered more than once. Use the event id to deduplicate.
async function processWebhookEvent(event) {
// Check if already processed
const processed = await db.webhookEvents.findOne({ eventId: event.id });
if (processed) {
return; // Already handled
}
// Process the event
await handleEvent(event);
// Mark as processed
await db.webhookEvents.insertOne({ eventId: event.id, processedAt: new Date() });
} 3. Use HTTPS
Always use HTTPS endpoints. Licenz will not send webhooks to HTTP URLs in production.
4. Monitor Failures
Set up monitoring for your webhook endpoint. Check the webhook delivery logs in the Dashboard for failed attempts.
Testing Webhooks
Use the Dashboard to send test webhook events to your endpoint:
- Go to Settings > Webhooks
- Click on your webhook endpoint
- Click "Send Test Event"
- Select an event type and review the payload
- Click "Send" to deliver the test webhook
Local Development
Use tools like ngrok or localtunnel to expose your local development server and receive webhooks.
Next Steps
- Licenses API - Generate and manage licenses
- Products API - Manage your products
- Rust SDK - Integrate license validation