New: Offline-first licensing with cryptographic validation. Learn more

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

  1. Extract the timestamp and signature from the header
  2. Check that the timestamp is within 5 minutes of current time
  3. Compute the expected signature: HMAC-SHA256(timestamp + "." + payload, secret)
  4. 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:

  1. Go to Settings > Webhooks
  2. Click on your webhook endpoint
  3. Click "Send Test Event"
  4. Select an event type and review the payload
  5. 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