Webhook Documentation

Receive real-time HTTP callbacks when important license events occur in your ExisOne account.

Introduction

Webhooks provide a way for ExisOne to send real-time notifications to your application when events occur in your licensing system. Instead of polling our API to check for changes, webhooks push data to your server as events happen.

Why Use Webhooks?

  • Real-time updates – Get notified immediately when licenses are created, activated, or validated
  • Efficient integration – No need to continuously poll the API for changes
  • Automated workflows – Trigger actions in your CRM, billing system, or custom applications
  • Security monitoring – Track validation failures and potential abuse in real-time
Use cases: Sync licenses with your CRM, send welcome emails after activation, alert on validation failures, update analytics dashboards, trigger billing workflows.

Getting Started

1. Create a Webhook Endpoint

First, create an endpoint in your application to receive webhook payloads. This should be a publicly accessible HTTPS URL that accepts POST requests.

2. Register the Endpoint in ExisOne

  1. Log in to your ExisOne dashboard
  2. Navigate to Webhooks in the sidebar
  3. Click Add Webhook
  4. Enter your endpoint URL
  5. Select the events you want to subscribe to
  6. Copy and securely store the generated signing secret

3. Verify Your Endpoint

Use the Test button to send a test webhook and verify your endpoint is receiving and processing requests correctly.

Important: Store your webhook signing secret securely. It's used to verify that webhooks are genuinely from ExisOne.

Event Types

ExisOne sends the following webhook events:

Event Description When Triggered
license.created A new license key was generated After calling the generate license API or completing a purchase
license.activated A license was activated on a device When a license is bound to a new hardware ID
license.expired A license has expired When validation is attempted on an expired license
license.validation.failed License validation was rejected Invalid key, hardware mismatch, expired, etc.
license.revoked A license was deactivated When a user or admin deactivates a license
device.deactivated A device was removed from a license When a seat is released from a corporate license

Sample Payloads

license.created

{
  "licenseId": 123,
  "activationKey": "XXXX-XXXX-XXXX-XXXX",
  "email": "customer@example.com",
  "productCode": "MyProduct",
  "productName": "MyProduct",
  "expirationDate": "2026-01-01T23:59:59Z"
}

license.activated

{
  "licenseId": 123,
  "activationKey": "XXXX-XXXX-XXXX-XXXX",
  "hardwareId": "ABC123DEF456...",
  "deviceName": null,
  "ipAddress": "203.0.113.45",
  "email": "customer@example.com"
}

license.validation.failed

{
  "activationKey": "INVALID-KEY",
  "hardwareId": "ABC123DEF456...",
  "ipAddress": "203.0.113.45",
  "reason": "Invalid activation key",
  "status": "invalid_key",
  "productName": "MyProduct",
  "timestamp": "2025-01-01T12:00:00Z"
}

license.revoked

{
  "licenseId": 123,
  "activationKey": "XXXX-XXXX-XXXX-XXXX",
  "revokedBy": "user",
  "reason": "License deactivated by user",
  "hardwareId": "ABC123DEF456..."
}

device.deactivated

{
  "licenseId": 123,
  "deviceId": 456,
  "hardwareId": "ABC123DEF456...",
  "activationKey": "XXXX-XXXX-XXXX-XXXX",
  "isCorporate": true
}

Verifying Signatures

All webhooks are signed using HMAC-SHA256 to ensure authenticity. You should verify the signature before processing any webhook.

Webhook Headers

Each webhook request includes these headers:

  • X-Webhook-Signature – The HMAC-SHA256 signature in format sha256=<hex_signature>
  • X-Webhook-Timestamp – Unix timestamp when the webhook was sent
  • X-Webhook-Event – The event type (e.g., license.created)

Signature Verification Algorithm

  1. Get the timestamp from X-Webhook-Timestamp header
  2. Get the raw request body (JSON payload)
  3. Create the signed message: {timestamp}.{payload}
  4. Compute HMAC-SHA256 using your webhook secret
  5. Compare with the signature in X-Webhook-Signature header

Code Examples

using System.Security.Cryptography;
using System.Text;

public static bool VerifyWebhookSignature(
    string secret, 
    string signature, 
    string timestamp, 
    string payload)
{
    var message = $"{timestamp}.{payload}";
    var keyBytes = Encoding.UTF8.GetBytes(secret);
    var messageBytes = Encoding.UTF8.GetBytes(message);
    
    using var hmac = new HMACSHA256(keyBytes);
    var hash = hmac.ComputeHash(messageBytes);
    var computed = "sha256=" + BitConverter.ToString(hash)
        .Replace("-", "").ToLowerInvariant();
    
    return signature == computed;
}

// In your webhook endpoint:
[HttpPost("webhook")]
public IActionResult HandleWebhook()
{
    var signature = Request.Headers["X-Webhook-Signature"].ToString();
    var timestamp = Request.Headers["X-Webhook-Timestamp"].ToString();
    
    using var reader = new StreamReader(Request.Body);
    var payload = await reader.ReadToEndAsync();
    
    if (!VerifyWebhookSignature(WebhookSecret, signature, timestamp, payload))
    {
        return Unauthorized("Invalid signature");
    }
    
    // Process the webhook...
    return Ok();
}
import hmac
import hashlib

def verify_webhook_signature(secret, signature, timestamp, payload):
    message = f"{timestamp}.{payload}"
    computed = "sha256=" + hmac.new(
        secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, computed)

# Flask example:
from flask import Flask, request

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    payload = request.get_data(as_text=True)
    
    if not verify_webhook_signature(WEBHOOK_SECRET, signature, timestamp, payload):
        return 'Invalid signature', 401
    
    data = request.json
    # Process the webhook...
    return 'OK', 200
const crypto = require('crypto');

function verifyWebhookSignature(secret, signature, timestamp, payload) {
    const message = `${timestamp}.${payload}`;
    const computed = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(message)
        .digest('hex');
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(computed)
    );
}

// Express example:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];
    const payload = req.body.toString();
    
    if (!verifyWebhookSignature(WEBHOOK_SECRET, signature, timestamp, payload)) {
        return res.status(401).send('Invalid signature');
    }
    
    const data = JSON.parse(payload);
    // Process the webhook...
    res.send('OK');
});
<?php
function verifyWebhookSignature($secret, $signature, $timestamp, $payload) {
    $message = $timestamp . '.' . $payload;
    $computed = 'sha256=' . hash_hmac('sha256', $message, $secret);
    return hash_equals($signature, $computed);
}

// Usage:
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$payload = file_get_contents('php://input');

if (!verifyWebhookSignature(WEBHOOK_SECRET, $signature, $timestamp, $payload)) {
    http_response_code(401);
    exit('Invalid signature');
}

$data = json_decode($payload, true);
// Process the webhook...
echo 'OK';
?>
require 'openssl'

def verify_webhook_signature(secret, signature, timestamp, payload)
  message = "#{timestamp}.#{payload}"
  computed = "sha256=" + OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    message
  )
  Rack::Utils.secure_compare(signature, computed)
end

# Sinatra example:
post '/webhook' do
  signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
  timestamp = request.env['HTTP_X_WEBHOOK_TIMESTAMP']
  payload = request.body.read
  
  unless verify_webhook_signature(WEBHOOK_SECRET, signature, timestamp, payload)
    halt 401, 'Invalid signature'
  end
  
  data = JSON.parse(payload)
  # Process the webhook...
  'OK'
end

Handling Webhooks

Best Practices

  1. Respond quickly – Return a 2xx response within 30 seconds. Process heavy work asynchronously.
  2. Be idempotent – Use the delivery ID to deduplicate. The same event may be sent multiple times.
  3. Verify signatures – Always validate the webhook signature before processing.
  4. Handle failures gracefully – If your endpoint returns a non-2xx response, ExisOne will retry.

Retry Behavior

If your endpoint returns a non-2xx status code, ExisOne will retry the delivery with the following schedule:

  • Immediately after first failure
  • 5 minutes later
  • 30 minutes later
  • 2 hours later
  • 24 hours later

After 5 consecutive failures, the webhook endpoint will be automatically disabled. You can re-enable it from the dashboard after fixing the issue.

Example Handler (Node.js)

// Store processed delivery IDs to prevent duplicates
const processedDeliveries = new Set();

app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
    // 1. Verify signature
    if (!verifySignature(req)) {
        return res.status(401).send('Invalid signature');
    }
    
    // 2. Parse payload
    const data = JSON.parse(req.body);
    const event = req.headers['x-webhook-event'];
    
    // 3. Acknowledge receipt immediately
    res.status(200).send('OK');
    
    // 4. Process asynchronously
    setImmediate(async () => {
        switch (event) {
            case 'license.created':
                await handleLicenseCreated(data);
                break;
            case 'license.activated':
                await handleLicenseActivated(data);
                break;
            case 'license.validation.failed':
                await handleValidationFailed(data);
                break;
            // ... handle other events
        }
    });
});

Testing

Using the Test Button

The ExisOne dashboard includes a "Test" button for each webhook endpoint. This sends a test payload to verify your endpoint is working correctly.

Using webhook.site

During development, you can use webhook.site to inspect webhook payloads:

  1. Go to webhook.site
  2. Copy your unique URL
  3. Add it as a webhook endpoint in ExisOne
  4. Trigger events and inspect the payloads

Using ngrok for Local Development

To test webhooks against your local development server:

# Install ngrok
npm install -g ngrok

# Start your local server (e.g., on port 3000)
node server.js

# In another terminal, expose it
ngrok http 3000

# Use the generated URL (e.g., https://abc123.ngrok.io/webhook)
# as your webhook endpoint

Troubleshooting

Common Issues

This happens after 5 consecutive failures. To fix:

  1. Check the delivery history for error messages
  2. Verify your endpoint is accessible and returning 2xx responses
  3. Fix any issues with your endpoint
  4. Edit the webhook and enable it again

Common causes:

  • Wrong secret – make sure you're using the correct webhook secret
  • Modified payload – the raw request body must not be modified before verification
  • Encoding issues – ensure UTF-8 encoding throughout
  • Timestamp mismatch – use the timestamp from the header, not current time

Webhooks have a 30-second timeout. If your processing takes longer:

  • Return 200 immediately and process asynchronously
  • Use a message queue (e.g., Redis, RabbitMQ) to handle processing
  • Optimize your endpoint code

Check the following:

  • The webhook endpoint is active (not paused or disabled)
  • The correct events are selected
  • Your endpoint is publicly accessible (not behind a firewall)
  • Your endpoint accepts POST requests with JSON body
  • Check delivery history for any failed attempts

Need Help?

If you're still having issues, contact our support team with:

  • Your webhook endpoint URL
  • The event type you're trying to receive
  • Any error messages from the delivery history
  • Your endpoint's logs showing the failed request