Register a Webhook
Webhooks are one transport of Azotte's EventDelivery subsystem. EventDelivery emits events from Azotte to your tenant server whenever entity properties marked [EventDelivery] change. Two transports are supported:
- HTTPS webhook — Azotte POSTs the event envelope to an endpoint you register.
- Kafka stream — Azotte publishes events to a Kafka topic your consumer reads.
Events flow one-way: Azotte → tenant. Tenants never push events back through this channel.
This page covers webhook transport: register an endpoint, pick event groups, verify signatures, handle retries, and develop locally. For Kafka transport, see the Kafka stream guide.
When to Use
- Sync subscription lifecycle state into your own database.
- Trigger entitlement provisioning when a customer subscribes or renews.
- Send transactional emails on payment failure or cancellation.
- Forward Azotte events into your data warehouse, analytics, or CRM.
Register an Endpoint in the Portal
- Sign in to the portal as a Tenant Admin.
- Open Settings → Developers → Webhooks.
- Click Add Endpoint.
- Enter the endpoint URL (must be HTTPS, publicly reachable).
- Select the event groups to subscribe to (see the event groups below).
- Pick the environment: Sandbox or Live.
- Click Save.
- Copy the signing secret shown after creation. Store it in your secret manager.
Event Groups
EventDelivery classifies every event into one of five groups (EnProductGroupType):
| Group | Code | Fires for |
|---|---|---|
RecurringSubscriptionEvent | 0 | Subscription lifecycle: create, renew, pause, cancel, grace period. |
OneTimeTransactionalProductEvent | 10 | One-time purchase lifecycle. |
PaymentEvent | 20 | Payment capture, failure, refund. |
CustomerInfoEvent | 30 | Customer profile or entitlement changes. |
SecurityEvent | 40 | Auth, key, permission, or compliance changes. |
Subscribe per group when registering the endpoint.
Payload Shape
EventDelivery payloads carry only properties marked [EventDelivery] on the entity. Field names use the entity's [Alias] codes (3-letter), not full property names, to keep payloads compact.
Example — CustomerEntitlement (only Quota, Unit, EntitlementIdentifier, MetaData, Flag are [EventDelivery]; LineItems[*] ships Quota, EntitlementName, Flag):
{
"id": "evt_01HX9Y...",
"group": "CustomerInfoEvent",
"model": "ce",
"tenantId": "tn_acme",
"createdAt": "2026-05-26T10:14:21.503Z",
"data": {
"QTA": 100,
"UNT": "GB",
"EID": "ent_data_quota",
"MDT": { "tier": "premium" },
"FLG": "active",
"CIL": [
{ "QTA": 50, "ENM": "Bonus Quota", "FLG": "promo" }
]
}
}
model matches the entity ModelIdentifier (e.g. ce = CustomerEntitlement). Resolve aliases via the entity reference in the C2A docs.
Respond with HTTP 2xx within 10 seconds to acknowledge the event.
Verify the Signature
Every request includes an Azotte-Signature header containing a timestamp and an HMAC-SHA256 signature computed over timestamp + "." + rawBody using your signing secret.
Azotte-Signature: t=1748246061,v1=5f9b...c3e1
Always verify the signature before processing the payload. Compare in constant time and reject requests where the timestamp is older than 5 minutes (replay protection).
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.AZOTTE_WEBHOOK_SECRET;
app.post(
"/webhooks/azotte",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.header("Azotte-Signature") || "";
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const timestamp = parts.t;
const received = parts.v1;
if (!timestamp || !received) return res.status(400).end();
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).end();
}
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${req.body.toString("utf8")}`)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(received, "hex"),
Buffer.from(expected, "hex"),
);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// handle event...
res.status(200).end();
},
);
.NET (ASP.NET Core)
[HttpPost("/webhooks/azotte")]
public async Task<IActionResult> Handle()
{
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
var header = Request.Headers["Azotte-Signature"].ToString();
var parts = header.Split(',')
.Select(p => p.Split('='))
.ToDictionary(p => p[0], p => p[1]);
var timestamp = long.Parse(parts["t"]);
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300)
return BadRequest();
var secret = Environment.GetEnvironmentVariable("AZOTTE_WEBHOOK_SECRET")!;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(
hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"))
).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(parts["v1"])))
return Unauthorized();
// handle event...
return Ok();
}
Retries and Idempotency
Azotte retries failed deliveries (any non-2xx response or timeout) with exponential backoff for up to 24 hours.
- Always treat the
idfield on the event envelope as the idempotency key. Persist processed event IDs and skip duplicates. - Acknowledge quickly. Push slow work onto a queue and return
200immediately. - Order is not guaranteed. Use
createdAtplus your own state machine to detect out-of-order events.
Rotate the Signing Secret
- Open the endpoint in Settings → Developers → Webhooks.
- Click Rotate Secret. Azotte returns a new secret immediately.
- The endpoint accepts signatures from both the old and new secret for 24 hours.
- Deploy the new secret to all consumers within that window.
Local Development
Azotte cannot reach localhost. Use a tunnel for local testing:
ngrok http 4000
# Register the https URL ngrok prints (e.g. https://abc123.ngrok.io/webhooks/azotte)
# as a Sandbox endpoint in the portal.
Use the Send Test Event button in the portal to deliver a synthetic event to your endpoint and confirm signature verification and parsing logic.
Security Rules
- HTTPS only. Plain HTTP endpoints are rejected.
- Verify the signature on every request. Never trust the payload alone.
- Reject requests with a timestamp skew greater than 5 minutes.
- Use a different signing secret per environment.
- Lock the endpoint to Azotte source IPs at the edge (firewall, WAF) if your security policy requires it.
- Never log the signing secret or the raw signature header.
Related
- API Keys - outbound credentials for Azotte API calls.
- Terminology -
Notification,Order,Payment,Subscriptiondefinitions.