Aura Logo
AuraAPI Docs

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

  1. 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.
  2. 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.
  3. In your endpoint, read the raw request body, then verify the signature using the secret. See verifying signatures below.
  4. Return a 2xx response within 10 seconds. Anything else (non-2xx, timeout, network error) triggers a retry.
  5. Deduplicate on the payload's data.id + created_at — retries mean the same event can arrive more than once.

Event catalog

EventFires when
lead.createdA new lead is created in Aura (via booking form, API, or integration).
lead.updatedA lead's profile fields change (name, email, company, etc.).
lead.status_changedA lead's pipeline status transitions (e.g. newqualified).
call.bookedA new call is scheduled through a booking link.
call.updatedA call's metadata changes (notes, assigned closer, outcome tags).
call.startedA call begins (detected via Nylas meeting state or closer check-in).
call.completedA call finishes successfully with a recorded outcome.
call.canceledA call is canceled before it takes place.
call.rescheduledA call is moved to a new time.
call.no_showA call is marked as a no-show after the scheduled start time.
payment.succeededA payment is successfully captured for a lead or call.
payment.failedA payment attempt fails (declined card, insufficient funds, etc.).
payment.refundedA 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:

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.

OutcomeRetried?
2xxNo — success
5xx (server error)Yes
408 Request TimeoutYes
429 Too Many RequestsYes
4xx other (400/401/403/404/...)No — permanent
Network error, DNS failure, TLS errorYes
Timeout after 10sYes

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.timingSafeEqual in Node, hmac.compare_digest in 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_message for 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:

  1. In the dashboard, click Rotate secret on the subscription. Aura generates a new secret and shows it to you once.
  2. For the next 24 hours, Aura sends both the new and old signatures in X-Aura-Signature-V1 (two v1= segments) and X-Aura-Signature (comma-separated).
  3. Deploy the new secret to your endpoint. Your existing verification code continues to work during the grace window because it accepts either signature.
  4. 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:

WebhooksPartner API
DirectionAura → your app (push)Your app → Aura (pull)
AuthHMAC signature on each requestAuthorization: Bearer aura_pk_live_...
SetupPaste HTTPS URL + store secretCreate API key with scopes
Best forReacting to events in real timeQueries, 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

On this page