Outbound Webhooks
Receive real-time HTTP event notifications for balance alerts, payment events, and product user spending caps.
Zorveus Outbound Webhooks send real-time HTTP event notifications to your server whenever state changes occur within your organization workspace.
Instead of polling Zorveus REST endpoints, your server registers a webhook URL to receive event payloads automatically.
Architecture and Delivery Guarantees
Zorveus uses the Transactional Outbox Pattern to guarantee at-least-once event delivery without adding network latency to core API calls.
1. Atomic Event Persistence
Core API transactions write events directly to a PostgreSQL outbox table in the same database transaction.
2. Outbox Worker Processing
An asynchronous worker polls pending outbox events, checks active organization subscriptions, and computes an HMAC SHA-256 signature.
3. HTTP POST Delivery
Zorveus sends an HTTP POST request containing the event payload to your registered webhook URL.
4. Retry Policy
If your endpoint returns a 5xx status or times out, Zorveus retries delivery up to 10 times using exponential backoff.
Webhook Event Catalog
Zorveus categorizes webhook events by product area:
| Feature Area | Event Type | Description |
|---|---|---|
| Wallet | wallet.low_balance | Available organization wallet balance drops below alert threshold. |
| Wallet | wallet.payment_credited | Wallet top-up purchase or credit succeeds. |
| Wallet | wallet.payment_failed | Wallet top-up transaction fails. |
| Product User | product_user.cap_reached | An attributed product user reaches a direct or connection cap for the configured period. |
| Product User | product_user.credits_granted | Promotional or admin credits are granted to a product user. |
| App Connection | app_connection.cap_reached | API key app connection reaches monthly spend cap. |
| Provider Credential | provider_credential.quota_exhausted | An upstream provider credential reports exhausted quota. |
Retry Rules and Status Codes
Zorveus evaluates response HTTP status codes returned by your receiver endpoint:
2xx(200-299): Success. Event marked as delivered.5xx(Server Error): Retryable. Zorveus schedules retries with exponential backoff up to 10 attempts.429(Rate Limited): Retryable. Zorveus backs off before retrying.- Timeouts and Connection Refused: Retryable.
4xx(Client Error except 429): Non-retryable. Requests returning400,401, or404are marked as failed immediately.- Disabled Webhooks (
enabled: false): Non-retryable. When an organization sets"enabled": false, the outbox worker skips HTTP delivery cleanly (reason: "webhook_disabled_or_not_configured").
Subscription Management
Manage webhook subscriptions through the Zorveus dashboard in your settings.
Complete Event Payload Schemas
Zorveus sends event payloads in standard JSON format with an X-Zorveus-Signature header.
1. wallet.low_balance
{
"id": "outbox_evt_101",
"event": "wallet.low_balance",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"available_balance": "4.50",
"threshold": "10.00",
"currency": "USD"
}
}2. wallet.payment_credited
{
"id": "outbox_evt_102",
"event": "wallet.payment_credited",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"amount": "50.00",
"new_balance": "54.50",
"currency": "USD",
"payment_provider": "stripe",
"reference": "ch_3Mv123"
}
}3. wallet.payment_failed
{
"id": "outbox_evt_103",
"event": "wallet.payment_failed",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"amount": "50.00",
"currency": "USD",
"payment_provider": "stripe",
"reason": "card_declined"
}
}4. product_user.cap_reached
{
"id": "outbox_evt_104",
"event": "product_user.cap_reached",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"app_id": "app_123",
"product_end_user_id": "peu_987",
"external_user_id": "customer_42",
"app_connection_id": "conn_456",
"target_type": "product_end_user",
"target_id": "peu_987",
"cap_target_type": "app_connection",
"cap_target_id": "conn_456",
"cap_rule_id": "cap_123",
"model": "openai/gpt-4.1-mini",
"cap_amount": "100.00",
"spent_this_period": "99.99",
"requested_amount": "0.02",
"projected_spend": "100.01",
"currency": "USD",
"period_start": "2026-08-01T00:00:00Z",
"period_end": "2026-09-01T00:00:00Z"
}
}5. product_user.credits_granted
{
"id": "outbox_evt_105",
"event": "product_user.credits_granted",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"app_id": "app_6b541d674ddc40df99485589985b22a9",
"product_end_user_id": "peu_99e504f9565f454fbde4a8abec010cd1",
"external_user_id": "sarah.jenkins@example.com",
"credit_grant_id": "pucg_246b817030cb4a0fbf4ec2811736ed49",
"granted_amount": "10.00",
"currency": "USD",
"reason": "Candidate Career Grant",
"expires_at": null
}
}6. app_connection.cap_reached
{
"id": "outbox_evt_106",
"event": "app_connection.cap_reached",
"org_id": "org_7k9xq2fm",
"timestamp": "2026-08-28T06:34:56.110229Z",
"data": {
"org_id": "org_7k9xq2fm",
"app_connection_id": "conn_456",
"target_type": "app_connection",
"target_id": "conn_456",
"cap_rule_id": "cap_456",
"model": "openai/gpt-4.1-mini",
"cap_amount": "500.00",
"spent_this_period": "499.99",
"requested_amount": "0.02",
"projected_spend": "500.01",
"currency": "USD",
"period_start": "2026-08-01T00:00:00Z",
"period_end": "2026-09-01T00:00:00Z"
}
}Signature Verification
Reject malformed timestamps and signatures. Reject timestamps outside a five-minute tolerance before processing the body. Compute the signature over the original body bytes, not parsed or reserialized JSON. The digest is a 64-character SHA-256 hexadecimal string.
Every request includes an X-Zorveus-Signature header containing a UNIX timestamp (t) and an HMAC SHA-256 digest (v1):
X-Zorveus-Signature: t=1756249200,v1=5d41402abc4b2a76b9719d911017c59205d41402abc4b2a76b9719d911017c5920To verify the signature:
- Extract
tandv1from the header. - Concatenate the timestamp string, a dot, and the raw HTTP request body bytes (
timestamp_bytes + b"." + raw_http_body). - Compute
HMAC-SHA256(webhook_secret, signed_payload)as a hex string. - Compare expected and received signatures using a constant-time comparison function.
const crypto = require('crypto');
function verifyZorveusSignature(req, secret) {
const sigHeader = req.headers['x-zorveus-signature'];
if (!sigHeader) return false;
const parts = {};
for (const item of sigHeader.split(',')) {
const idx = item.indexOf('=');
if (idx !== -1) {
parts[item.slice(0, idx).trim()] = item.slice(idx + 1).trim();
}
}
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
if (!/^\d{1,12}$/.test(timestamp) || !/^[a-f0-9]{64}$/i.test(signature)) return false;
const signedAt = Number(timestamp);
if (!Number.isSafeInteger(signedAt) || Math.abs(Date.now() / 1000 - signedAt) > 300) return false;
const rawBody = req.rawBody;
if (!Buffer.isBuffer(rawBody)) return false;
const signedPayload = Buffer.concat([
Buffer.from(`${timestamp}.`, 'utf-8'),
rawBody
]);
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
const sigBuf = Buffer.from(signature.toLowerCase(), 'utf-8');
const expBuf = Buffer.from(expectedSignature, 'utf-8');
// Prevent length mismatch errors before timing-safe check
if (sigBuf.length !== expBuf.length) {
return false;
}
return crypto.timingSafeEqual(sigBuf, expBuf);
}Receiver Best Practices
For product-user events, resolve your customer through data.external_user_id and verify data.app_id and the envelope's organization. Do not use an internal Zorveus ID as your application's customer ID. An attributed request that reaches a connection cap can emit product_user.cap_reached; inspect cap_target_type to identify that cap.
Use one allowance service for webhook events and inference errors. Recheck available allowance when access may have recovered. A credit grant alone does not guarantee the next request will fit.
provider_credential.quota_exhausted contains org_id, provider_credential_id, provider, credential_name, and exhausted_at in data. Treat it as an upstream credential issue, not a customer allowance failure.
- Acknowledge quickly. Return HTTP
200or204within 3 seconds. Queue long-running tasks asynchronously. - Prevent replay attacks. Reject payloads with timestamps older than 5 minutes.
- Deduplicate using event IDs. Zorveus guarantees at-least-once delivery. Use the
idproperty in the JSON payload to skip duplicate processing.