Skip to content

Webhooks

When something interesting happens on a tenant — a donation lands, a recurring schedule pauses, a shipment goes out — the platform can POST a JSON event to an HTTPS endpoint you operate. This page describes the delivery model end-to-end. The full event catalog lives at Webhooks → Events.

The tenant needs the webhooks:enabled feature flag turned on. Without it, the delivery service skips the tenant entirely — registered endpoints still exist in the configuration, but no events fire.

The API key that registers and manages endpoints needs the webhooks:manage scope.

Terminal window
curl -sS -X POST \
-H "X-Api-Key: $AURA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-replication",
"url": "https://hooks.example.com/aura",
"eventTypes": ["contact.created","order.created","order_payment.created"],
"description": "Replicates contact + donation events into our warehouse."
}' \
"https://api.auradonors.com/api/tenants/$TENANT_ID/webhooks"

The response is 201 Created and carries the plaintext signing secret exactly once:

{
"id": "8a4f1c7b-3d6e-4f12-9a01-8b8d2c3e4f56",
"name": "production-replication",
"url": "https://hooks.example.com/aura",
"eventTypes": ["contact.created", "order.created", "order_payment.created"],
"secret": "whsec_..."
}

Store secret in your secret manager immediately. The configuration GET endpoints will not return it again. To rotate the secret, register a new endpoint and revoke the old one — the existing endpoint’s secret is not re-issuable in place.

A background service ticks every 30 seconds. On each tick, for each active endpoint:

  1. Enqueue. Up to 500 new audit-log events since the endpoint’s last processed timestamp are matched against the endpoint’s eventTypes filter and turned into pending webhook_deliveries rows.
  2. Deliver. Up to 100 pending deliveries (across all endpoints) are POSTed with a 10-second timeout. Successful deliveries are marked success = true; failures are flagged for retry.

Every delivery POST carries:

HeaderMeaning
Content-TypeAlways application/json.
X-Webhook-EventThe event type (e.g., order.created).
X-Webhook-Delivery-IdUnique Guid per attempt. Use this for idempotent receivers.
X-Webhook-TimestampUnix epoch seconds when the request was signed. Used in the signature — required to recompute.
X-Webhook-Signaturesha256={hex-hmac} — see Signature verification.

Body shape:

{
"id": "9c4a7b2d-...",
"event": "order.created",
"timestamp": "2026-05-06T13:42:01.1234567Z",
"data": {
"entity_type": "Order",
"entity_id": "00000000-0000-0000-0000-000000000123",
"action": "Created",
"changes": null,
"user_id": "api-key:7c..."
}
}

The body’s data block is the audit-log row that produced the event. The changes field is non-null on updated events and contains a JSON description of which columns changed.

To resolve the entity itself, follow up with the matching read endpoint (GET /api/tenants/{tid}/orders/{entity_id} for an Order, etc.). The delivery does not embed the full entity — that keeps the wire payload small and means receivers always see a fresh view.

A delivery is retried up to 5 attempts. The retry delay is exponential on the attempt number:

Attempt #Delay before next tryCumulative wait at next attempt
1 → 21 minute~1 minute
2 → 32 minutes~3 minutes
3 → 44 minutes~7 minutes
4 → 58 minutes~15 minutes

Anything 2xx is treated as success. Anything else (including 4xx — the delivery is a server-driven push, so 4xx isn’t terminal) plus network errors and timeouts are treated as failures and queued for retry. After the fifth failed attempt the delivery is abandoned. There is no manual re-delivery API today — fix the receiver and the next event flows; for a backfill, contact support with the affected entity_ids.

You can inspect every delivery (success or fail) for an endpoint:

Terminal window
curl -sS \
-H "X-Api-Key: $AURA_API_KEY" \
"https://api.auradonors.com/api/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/deliveries?page=1&pageSize=20"

Each row carries the attempted timestamp, the HTTP status code, the attempt number, and a sanitized error message when the call failed.

Every delivery is signed with the endpoint’s secret using HMAC-SHA256. The signed message is {timestamp}.{body} (not just the body) — this prevents replay of an old payload against a newer timestamp.

Recompute and compare:

public static bool IsValidSignature(string body, string timestamp, string headerValue, string secret)
{
// headerValue arrives as "sha256={hex}"
var expected = headerValue.StartsWith("sha256=") ? headerValue["sha256=".Length..] : headerValue;
var message = $"{timestamp}.{body}";
var keyBytes = Encoding.UTF8.GetBytes(secret);
var msgBytes = Encoding.UTF8.GetBytes(message);
var hash = HMACSHA256.HashData(keyBytes, msgBytes);
var actual = Convert.ToHexStringLower(hash);
// Constant-time compare to prevent timing attacks.
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(actual),
Encoding.UTF8.GetBytes(expected));
}
Terminal window
# Recreate the signature with bash + openssl as a smoke test.
TIMESTAMP="$(curl -sI ... | grep -i x-webhook-timestamp | awk '{print $2}' | tr -d '\r')"
BODY="$(cat received-body.json)"
SECRET="whsec_..."
EXPECTED=$(printf '%s.%s' "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -hex \
| awk '{print $2}')
echo "$EXPECTED" # compare to the X-Webhook-Signature header value (after the sha256= prefix)

Reject any delivery where:

  • The X-Webhook-Signature header is missing.
  • The recomputed HMAC doesn’t match (constant-time compare).
  • The X-Webhook-Timestamp is more than a few minutes off your wall clock (replay protection — the platform doesn’t enforce this on its side, but you should).

Best-practice handler shape:

  1. Read the body as raw bytes / raw string before any framework JSON middleware mutates it. The signature is over the exact bytes sent.
  2. Verify the signature.
  3. Check X-Webhook-Delivery-Id against your “already processed” cache; if seen, return 200 immediately without re-running side effects.
  4. Run your side effect.
  5. Return 200 OK within ten seconds (the delivery service’s per-attempt timeout). If you can’t, queue the work to a background processor and return 200 immediately — long-running handlers risk timing out and getting retried.

To verify connectivity without waiting for a real event:

Terminal window
curl -sS -X POST \
-H "X-Api-Key: $AURA_API_KEY" \
"https://api.auradonors.com/api/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/test"

The platform fires a synthetic event against the endpoint’s URL and returns the delivery row so you can inspect status code and timing.

The platform also receives webhooks from payment processors at /api/webhooks/{provider}/{tenantSlug} (Stripe, Authorize.Net). Those are platform-inbound and aren’t part of the integrator-facing direction described above — they’re documented for completeness in the Live API explorer but you don’t subscribe to them.

  • No per-event payload customization. The body shape is fixed.
  • No filtering beyond event type. You can’t subscribe to “only orders over $1000.”
  • No restored event delivery. The catalog enumerates created, updated, and deleted only.
  • No manual re-delivery API. Five attempts is the budget. Fix the receiver first; coordinate a backfill with support if you need historical events.
  • No webhook secret rotation in place. Rotate by creating a new endpoint and revoking the old.
  • No batched delivery. One event per HTTP POST.
  • No HTTP/3 or websocket transport. HTTPS POST only.