Webhooks
Subscribe partner endpoints to real-time events from Aura — event catalog, HMAC signature verification, retry policy, and setup.
Webhooks
Webhooks let your application react to events inside Aura in real time instead
of polling. When a lead is created, a call is booked, or a payment succeeds,
Aura sends a signed HTTPS POST to a URL you specify. Your endpoint verifies
the signature, processes the event, and returns a 2xx response.
Aura is the sender. Your application is the receiver. There is no OAuth handshake, no verification ping, and no inbound traffic from your app to Aura for webhook setup — all you provide is a public HTTPS URL and a stored secret.
Quick start
- In the Aura dashboard, go to Settings → Webhooks and click New subscription. Pick an event type, paste your HTTPS endpoint URL, and create the subscription.
- Aura shows you a signing secret once. Copy it into a secure store on your side (env var, secret manager). You cannot retrieve it again — only rotate it.
- In your endpoint, read the raw request body, then verify the signature using the secret. See verifying signatures below.
- Return a
2xxresponse within 10 seconds. Anything else (non-2xx, timeout, network error) triggers a retry. - Deduplicate on the payload's
data.id+created_at— retries mean the same event can arrive more than once.
Event catalog
| Event | Fires when |
|---|---|
lead.created | A new lead is created in Aura (via booking form, API, or integration). |
lead.updated | A lead's profile fields change (name, email, company, etc.). |
lead.status_changed | A lead's pipeline status transitions (e.g. new → qualified). |
call.booked | A new call is scheduled through a booking link. |
call.updated | A call's metadata changes (notes, assigned closer, outcome tags). |
call.started | A call begins (detected via Nylas meeting state or closer check-in). |
call.completed | A call finishes successfully with a recorded outcome. |
call.canceled | A call is canceled before it takes place. |
call.rescheduled | A call is moved to a new time. |
call.no_show | A call is marked as a no-show after the scheduled start time. |
payment.succeeded | A payment is successfully captured for a lead or call. |
payment.failed | A payment attempt fails (declined card, insufficient funds, etc.). |
payment.refunded | A previously captured payment is fully or partially refunded. |
Payload shape
Every delivery uses the same top-level wrapper:
{
"event": "call.booked",
"created_at": "2026-04-15T17:52:10.000Z",
"organization_id": "org_xxx",
"data": { /* entity-specific — see below */ }
}data carries the full entity with attribution fields (utm_source,
utm_medium, utm_campaign, booking_link_id, referral) so you don't
have to call the API to figure out where a lead came from.
Example: call.booked
{
"event": "call.booked",
"created_at": "2026-04-15T17:52:10.000Z",
"organization_id": "org_xxx",
"data": {
"id": "call_abc123",
"lead_id": "lead_xyz",
"closer_id": "user_456",
"booking_link_id": "bl_789",
"scheduled_at": "2026-04-16T15:00:00.000Z",
"duration": 30,
"status": "scheduled",
"conferencing_url": "https://meet.google.com/...",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-2026",
"utm_term": null,
"utm_content": null,
"referral": null,
"guest_rsvp_status": null,
"guest_rsvp_confirmed_at": null,
"created_at": "2026-04-15T17:52:10.000Z",
"updated_at": "2026-04-15T17:52:10.000Z"
}
}Verifying signatures
Aura signs every request with two headers so you can pick the verification scheme you prefer:
X-Aura-Signature-V1 (recommended)
Stripe-style format:
X-Aura-Signature-V1: t=1744739530123,v1=8f3a1b...t=<unix_ms>— the delivery timestamp. You must reject requests whose timestamp drifts more than 5 minutes from your clock, or an attacker who captures one delivery could replay it indefinitely.v1=<sha256_hex>— HMAC-SHA256 of the string${t}.${rawBody}, hex-encoded.
During the 24-hour secret rotation grace window, the header will carry two
v1= segments — one computed with the current secret, one with the previous
secret. Accept the request if either verifies. That lets you rotate secrets
without dropping in-flight deliveries.
X-Aura-Signature (legacy, deprecated)
Bare hex digest of HMAC-SHA256(rawBody):
X-Aura-Signature: 8f3a1b...During rotation it becomes current,previous — split on , and accept if
either matches. This header is kept for backwards compatibility and offers
no replay protection. New integrations should verify X-Aura-Signature-V1.
Reference — Node.js
import crypto from "node:crypto";
const FRESHNESS_MS = 5 * 60 * 1000;
export function verifyAuraWebhook(
rawBody: string, // MUST be the raw request bytes, not JSON.parse'd
headerValue: string, // X-Aura-Signature-V1
secret: string,
): boolean {
if (!headerValue) return false;
// Parse "t=<ms>,v1=<sig>[,v1=<sig>]"
const pairs = headerValue.split(",").map((s) => s.trim());
let timestamp: number | null = null;
const signatures: string[] = [];
for (const pair of pairs) {
const eq = pair.indexOf("=");
if (eq < 0) continue;
const [key, value] = [pair.slice(0, eq), pair.slice(eq + 1)];
if (key === "t") timestamp = Number.parseInt(value, 10);
else if (key === "v1") signatures.push(value);
}
if (timestamp === null || signatures.length === 0) return false;
// Freshness window — reject replays.
if (Math.abs(Date.now() - timestamp) > FRESHNESS_MS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return signatures.some((sig) => {
if (sig.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}Then in your handler (Next.js / Hono / Express all the same pattern):
// Next.js App Router example
export async function POST(request: Request) {
const rawBody = await request.text(); // critical: NOT request.json()
const header = request.headers.get("x-aura-signature-v1");
const valid = verifyAuraWebhook(rawBody, header ?? "", process.env.AURA_WEBHOOK_SECRET!);
if (!valid) return new Response("Invalid signature", { status: 401 });
const event = JSON.parse(rawBody);
// ... dedupe on event.data.id + event.created_at, then process
return new Response("ok", { status: 200 });
}Reference — Python
import hmac, hashlib, time
FRESHNESS_MS = 5 * 60 * 1000
def verify_aura_webhook(raw_body: bytes, header_value: str, secret: str) -> bool:
if not header_value:
return False
timestamp: int | None = None
signatures: list[str] = []
for pair in header_value.split(","):
if "=" not in pair:
continue
key, value = pair.split("=", 1)
key, value = key.strip(), value.strip()
if key == "t":
try:
timestamp = int(value)
except ValueError:
return False
elif key == "v1":
signatures.append(value)
if timestamp is None or not signatures:
return False
now_ms = int(time.time() * 1000)
if abs(now_ms - timestamp) > FRESHNESS_MS:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return any(hmac.compare_digest(sig, expected) for sig in signatures)Why the raw body?
HMAC must cover the exact bytes Aura signed. If you JSON.parse and then
re-stringify, key ordering or whitespace may drift and your signature will
silently fail to verify. Always capture the raw body first (await request.text()
in modern Node, request.body as a string/buffer elsewhere), verify, then
parse.
Retry policy
If your endpoint doesn't return a 2xx within 10 seconds, Aura retries with exponential backoff.
| Outcome | Retried? |
|---|---|
2xx | No — success |
5xx (server error) | Yes |
408 Request Timeout | Yes |
429 Too Many Requests | Yes |
4xx other (400/401/403/404/...) | No — permanent |
| Network error, DNS failure, TLS error | Yes |
| Timeout after 10s | Yes |
Retries happen at 1s, 5s, 15s after the first failure (3 retries max,
4 attempts total). After the last retry, the delivery is marked failed in
webhook_delivery_logs. There is no automatic re-send — fix your endpoint
and use the Test event button in the dashboard, or call the replay
endpoint, to re-trigger a delivery.
Permanent 4xx responses are NOT retried
Aura treats most 4xx responses as permanent failures and does not retry.
Returning 404 because your endpoint path is wrong, or 401 because your
signature check rejected the request, tells Aura the delivery can never
succeed — retrying just wastes 21 seconds and creates noise in your logs.
If you want a retry, return 500 or 503.
Security model
- HTTPS only. Aura refuses to create subscriptions for non-HTTPS URLs.
- SSRF-guarded target URLs. Aura rejects subscriptions pointing at
localhost, any RFC1918 private range (10/8,172.16/12,192.168/16), link-local (169.254/16— blocks AWS IMDS / GCP / Azure metadata endpoints), carrier-grade NAT, multicast, and broadcast ranges. DNS is re-resolved at delivery time to defend against DNS-rebinding attacks. - HMAC-SHA256. Signing secrets are 32 bytes of
crypto.randomBytes(256 bits of entropy). Compare signatures with a timing-safe comparison (crypto.timingSafeEqualin Node,hmac.compare_digestin Python). - One-time secret reveal. The signing secret is shown once at subscription create time and once at rotation time. If you lose it, rotate to generate a new one.
- Per-host concurrency limit. Aura caps outbound deliveries to a single partner hostname at 5 concurrent requests so a burst of events can't accidentally DoS your endpoint.
- PII-scrubbed error logs. Your endpoint's error response text is
stored in
webhook_delivery_logs.error_messagefor debugging, but Aura scrubs email addresses and phone numbers from the message and truncates to 500 chars before persisting.
Secret rotation
To rotate a secret without downtime:
- In the dashboard, click Rotate secret on the subscription. Aura generates a new secret and shows it to you once.
- For the next 24 hours, Aura sends both the new and old signatures
in
X-Aura-Signature-V1(twov1=segments) andX-Aura-Signature(comma-separated). - Deploy the new secret to your endpoint. Your existing verification code continues to work during the grace window because it accepts either signature.
- After 24 hours, Aura stops sending the old signature. Deliveries signed only with the old secret will fail verification.
Delivery logs
Every delivery attempt — successful or failed — is recorded in
webhook_delivery_logs with its status, HTTP status code, response time,
attempt count, and (sanitized) error message. You can view the last 50
attempts for any subscription in the dashboard's delivery panel, or
fetch them programmatically:
curl -H "Authorization: Bearer aura_pk_live_xxxxx" \
"https://api.aura-app.ai/v1/webhooks/deliveries?subscription_id=sub_xyz"Logs are retained for 30 days.
Webhooks vs. the Partner API
These two surfaces complement each other:
| Webhooks | Partner API | |
|---|---|---|
| Direction | Aura → your app (push) | Your app → Aura (pull) |
| Auth | HMAC signature on each request | Authorization: Bearer aura_pk_live_... |
| Setup | Paste HTTPS URL + store secret | Create API key with scopes |
| Best for | Reacting to events in real time | Queries, backfills, reconciliation, mutations |
Most integrations want both. For example: on lead.created react by
enriching the lead in your CRM; then call
GET /v1/leads/:id on-demand to pull the full
history and attribution chain.
Reference
POST /v1/webhooks— create a subscription programmaticallyGET /v1/webhooks— list subscriptionsPATCH /v1/webhooks/{id}— update target URLDELETE /v1/webhooks/{id}— delete a subscriptionPOST /v1/webhooks/test— send a test event to an endpoint