Zorveus LogoZorveusDOCS
Platform & Security

Outbound Webhooks

View Markdown

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 AreaEvent TypeDescription
Walletwallet.low_balanceAvailable organization wallet balance drops below alert threshold.
Walletwallet.payment_creditedWallet top-up purchase or credit succeeds.
Walletwallet.payment_failedWallet top-up transaction fails.
Product Userproduct_user.cap_reachedAn attributed product user reaches a direct or connection cap for the configured period.
Product Userproduct_user.credits_grantedPromotional or admin credits are granted to a product user.
App Connectionapp_connection.cap_reachedAPI key app connection reaches monthly spend cap.
Provider Credentialprovider_credential.quota_exhaustedAn 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 returning 400, 401, or 404 are 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=5d41402abc4b2a76b9719d911017c59205d41402abc4b2a76b9719d911017c5920

To verify the signature:

  1. Extract t and v1 from the header.
  2. Concatenate the timestamp string, a dot, and the raw HTTP request body bytes (timestamp_bytes + b"." + raw_http_body).
  3. Compute HMAC-SHA256(webhook_secret, signed_payload) as a hex string.
  4. Compare expected and received signatures using a constant-time comparison function.
Client SDK / Framework:
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 200 or 204 within 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 id property in the JSON payload to skip duplicate processing.
Was this page helpful?
Edit this page on GitHub

On this page