Skip to main content

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

  1. Sign in to the portal as a Tenant Admin.
  2. Open Settings → Developers → Webhooks.
  3. Click Add Endpoint.
  4. Enter the endpoint URL (must be HTTPS, publicly reachable).
  5. Select the event groups to subscribe to (see the event groups below).
  6. Pick the environment: Sandbox or Live.
  7. Click Save.
  8. Copy the signing secret shown after creation. Store it in your secret manager.

Register webhook endpoint

Event Groups

EventDelivery classifies every event into one of five groups (EnProductGroupType):

GroupCodeFires for
RecurringSubscriptionEvent0Subscription lifecycle: create, renew, pause, cancel, grace period.
OneTimeTransactionalProductEvent10One-time purchase lifecycle.
PaymentEvent20Payment capture, failure, refund.
CustomerInfoEvent30Customer profile or entitlement changes.
SecurityEvent40Auth, 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 id field 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 200 immediately.
  • Order is not guaranteed. Use createdAt plus your own state machine to detect out-of-order events.

Rotate the Signing Secret

  1. Open the endpoint in Settings → Developers → Webhooks.
  2. Click Rotate Secret. Azotte returns a new secret immediately.
  3. The endpoint accepts signatures from both the old and new secret for 24 hours.
  4. 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.
  • API Keys - outbound credentials for Azotte API calls.
  • Terminology - Notification, Order, Payment, Subscription definitions.