Receive real-time HTTP callbacks when important license events occur in your ExisOne account.
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.
First, create an endpoint in your application to receive webhook payloads. This should be a publicly accessible HTTPS URL that accepts POST requests.
Use the Test button to send a test webhook and verify your endpoint is receiving and processing requests correctly.
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 |
{
"licenseId": 123,
"activationKey": "XXXX-XXXX-XXXX-XXXX",
"email": "customer@example.com",
"productCode": "MyProduct",
"productName": "MyProduct",
"expirationDate": "2026-01-01T23:59:59Z"
}
{
"licenseId": 123,
"activationKey": "XXXX-XXXX-XXXX-XXXX",
"hardwareId": "ABC123DEF456...",
"deviceName": null,
"ipAddress": "203.0.113.45",
"email": "customer@example.com"
}
{
"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"
}
{
"licenseId": 123,
"activationKey": "XXXX-XXXX-XXXX-XXXX",
"revokedBy": "user",
"reason": "License deactivated by user",
"hardwareId": "ABC123DEF456..."
}
{
"licenseId": 123,
"deviceId": 456,
"hardwareId": "ABC123DEF456...",
"activationKey": "XXXX-XXXX-XXXX-XXXX",
"isCorporate": true
}
All webhooks are signed using HMAC-SHA256 to ensure authenticity. You should verify the signature before processing any webhook.
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 sentX-Webhook-Event – The event type (e.g., license.created)X-Webhook-Timestamp header{timestamp}.{payload}X-Webhook-Signature headerusing 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
If your endpoint returns a non-2xx status code, ExisOne will retry the delivery with the following schedule:
After 5 consecutive failures, the webhook endpoint will be automatically disabled. You can re-enable it from the dashboard after fixing the issue.
// 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
}
});
});
The ExisOne dashboard includes a "Test" button for each webhook endpoint. This sends a test payload to verify your endpoint is working correctly.
During development, you can use webhook.site to inspect webhook payloads:
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
This happens after 5 consecutive failures. To fix:
Common causes:
Webhooks have a 30-second timeout. If your processing takes longer:
Check the following:
If you're still having issues, contact our support team with: