Payload Reference
Field-level reference for every webhook payload
Payload Reference
Every Aura webhook ships in the same envelope. The data object's shape depends on the event type. This page documents each shape exactly as it appears in production.
The interfaces below mirror apps/api/src/lib/webhook-events.ts — the canonical source of truth. Fields here that show string | null may arrive as null if the underlying data is missing.
Envelope
interface WebhookPayload<T> {
event: WebhookEventType; // e.g. "call.booked"
created_at: string; // ISO 8601, UTC
organization_id: string; // The org the event belongs to
data: T; // Resource payload — see below
idempotency_key?: string; // Stable across retries — see below
}| Field | Type | Notes |
|---|---|---|
event | string | One of the 15 event types |
created_at | string | ISO 8601 timestamp of emission, UTC. Recomputed on each retry — do not dedup on this. |
organization_id | string | Your Aura organization ID |
data | object | Shape depends on event |
idempotency_key | string | Stable per-event key, identical across retries of the same event. Use it to enforce exactly-once. |
Exactly-once with idempotency_key
Delivery is at-least-once: if your endpoint commits its side effect and then crashes (or times out) before returning a 2xx, Aura re-POSTs the same event. The retry carries the same idempotency_key — created_at and the signature timestamp are recomputed, but the key is not.
Enforce exactly-once by storing the key and rejecting duplicates:
-- One row per (subscription, event). The conflict drops the redelivery.
CREATE UNIQUE INDEX uq_webhook_dedup
ON processed_webhooks (subscription_id, idempotency_key);// On each delivery, insert-or-ignore before processing.
const inserted = await db.query(
`INSERT INTO processed_webhooks (subscription_id, idempotency_key)
VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING 1`,
[subscriptionId, payload.idempotency_key],
);
if (inserted.rowCount === 0) {
return res.status(200).send('OK'); // already processed — drop silently
}
// ... process the event exactly once ...The key is optional in the type only for forward compatibility — every envelope delivery populates it in practice. If you ever see it missing, fall back to processing the event normally (at-least-once) rather than dropping it.
LeadWebhookData
Emitted on lead.created, lead.updated, lead.status_changed.
| Field | Type | Description |
|---|---|---|
id | string | Lead ID |
name | string | null | Full name, if provided |
email | string | null | Email, if provided |
phone | string | null | Phone, if provided |
company | string | null | Company, if provided |
status | string | The lead's stored status (see status values below). It can lag behind the board: use pipeline_stage to show where the lead stands. |
pipeline_stage | string | null | The lead's column on the Opportunities board when the event was sent, worked out from its calls (see values below). null only if Aura could not read it. |
timezone | string | null | IANA timezone, if known |
source | object | null | Attribution — see below |
consent | array | Every consent box and what the person chose (see below). agreed is true only while they agree, for the lead's phone now. The phone number is sent either way, so check this before you text. [] when no consent is on file. |
consent_by_type | object | One answer for each kind of message: yes, no or not_asked (see below). Every kind reads not_asked when no consent is on file, except Booking texts on some older leads (see below). |
created_at | string | When the lead was created (ISO 8601) |
updated_at | string | When the lead was last updated (ISO 8601) |
source object
| Field | Type | Description |
|---|---|---|
utm_source | string | null | Traffic platform (e.g. google, facebook) |
utm_medium | string | null | Channel type (e.g. cpc, email, social) |
utm_campaign | string | null | Campaign name |
utm_term | string | null | Paid search keywords / targeting criteria |
utm_content | string | null | Ad creative or A/B variant |
referrer | string | null | HTTP referrer — embed host page or traffic source |
consent entries
One entry per consent box, in the order Aura recorded them. This includes your own boxes on the booking form. Aura's box for appointment texts is in the list once the person has seen it, or replied STOP or START.
| Field | Type | Description |
|---|---|---|
id | string | Which box this is, stable when its wording changes: the box's own id for a box your team added, aura_sms for Aura's box for appointment texts, data_consent for the data-processing box. Match on this, not on label |
kind | string | custom (a box your team added), aura_sms (Aura's box for appointment texts) or data_consent (the data-processing box) |
label | string | The words on the box, as the person saw them |
disclosure | string | null | The fine print shown beside the box: for Aura's box for appointment texts, how often texts come, that rates may apply, and how to stop them. null for other boxes |
agreed | boolean | True only while they agree, for the lead's phone now. False if they never ticked the box, took it back, or agreed for a different number |
state | string | granted (ticked), withdrawn (agreed, then took it back by unticking the box or replying STOP) or declined (finished the form without ticking it) |
at | string | When they made the current choice (ISO 8601) |
granted_at | string | null | For a withdrawn box, when they first agreed. null otherwise |
phone | string | null | The phone number they made the choice for. If it is not the lead's phone now, agreed is false. null for a choice made before Aura kept the number |
source | string | null | How the choice reached Aura: booking_form, sms_reply_stop, sms_reply_start, carrier_block, admin or import. null when Aura did not record how |
links | array | The links the box showed, each with text and url |
Before you text anyone, keep these in mind:
- An empty list means no consent is on file. That is not the same as
declined, but it is still not a yes. Treat it as no consent. One exception: for Booking texts, checkconsent_by_type.sms_booking, which can readyesornoon some older leads whose list is empty (seeconsent_by_type). agreedis false when they agreed for a different number.phonesays which number that was.- Consent can change after the webhook is sent. For example, the person
may reply STOP. Read the lead again with
GET /v1/leads/:idright before you text them. - A box they did not tick is only recorded once they finish the form.
Someone who leaves halfway has no
declinedentry yet.
consent_by_type object
One answer for each kind of message, so you can skip people who did not agree
without reading each box's wording. Your team chooses which consent box asks
about each kind, in Settings. Until a box has a kind, only Booking texts can
have an answer, from Aura's own box for appointment texts. Every other kind
reads not_asked.
| Field | Type | Description |
|---|---|---|
sms_booking | object | Booking texts: texts about their booked call, like reminders |
sms_marketing | object | Marketing texts: texts with offers and promotions |
email_marketing | object | Marketing emails: emails with offers and newsletters |
data_processing | object | Data use: agreeing to your privacy policy, in a box your team added |
Each answer has the same four fields:
| Field | Type | Description |
|---|---|---|
state | string | yes (they agreed), no (they left the box unticked or took it back, for example by replying STOP; for texts, also when they agreed for a different phone number) or not_asked (no answer on file) |
at | string | null | When the answer last changed (ISO 8601, UTC). null when not asked |
source | string | null | box (one of your consent boxes) or aura_sms_opt_in (Aura's own box for appointment texts, which a STOP reply turns off). null when not asked |
id | string | null | The box the answer came from, the same id as in consent. null when not asked. On some older leads, Booking texts names Aura's box before consent lists it (see below) |
not_askedis not a yes. It means nothing the person answered asks about that kind of message. Treat it as no consent.- Booking texts can come from Aura's own box. When your booking page
shows Aura's box for appointment texts, its answer comes first, so a STOP
reply reads as
no. - Every surface sends the same answers for the same lead: the REST API,
GraphQL (
Lead.consentByType), the lead webhooks, Zapier and your CRM. - Booking texts also reads Aura's texting settings. Some older leads
have their choice on Aura's box for appointment texts, or their STOP
reply, only in Aura's texting settings. Their Booking texts answer reads
that choice, as your CRM does, with
sourceaura_sms_opt_inandatset to when they chose.consentlists only the answers Aura has stored, so on these leads it can be empty or leave out the box that Booking texts'idnames. Aura adds that box when it next sends the lead to your CRM.lead.deletedalready lists it. - If Aura cannot check your booking page's settings at that moment, for
example during an outage, Booking texts reads Aura's own box first for a
lead who answered it. That way a STOP reply never reads as
yes.
Lead pipeline_stage values
The same columns, in the same order, as the Opportunities board.
| Value | Meaning |
|---|---|
interested | Started booking, no call yet |
disqualified | Filtered out by the booking questions |
qualified | Passed the booking questions, no call yet |
booked | Has a call coming up that they have not confirmed |
confirmed | Has a call coming up and said they will come |
declined | The calendar invite was declined |
cancelled | The call was cancelled |
attended | The call happened, outcome not recorded yet |
unfit | Had the call, not a fit |
no_show | Booked but did not turn up |
follow_up | Had the call, needs a follow-up |
lost | Had the call, did not buy |
won | Bought |
status and pipeline_stage can disagree for a while. When a guest accepts
the invite, pipeline_stage says confirmed at once, while status can still
say booked until Aura's status sync updates it. pipeline_stage is null
only if Aura could not read the lead's calls.
Lead status values
| Value | Meaning |
|---|---|
interested | Contact info captured, no booking yet |
qualified | Form questions answered, fit confirmed |
scheduled | Call booked, in the future |
attended | Call completed |
closed | Deal won |
lost | Deal lost |
canceled | Booking canceled |
disqualified | Marked as not a fit |
CallWebhookData
Emitted on call.booked, call.updated, call.started, call.completed, call.canceled, call.rescheduled, call.no_show.
| Field | Type | Description |
|---|---|---|
id | string | Call ID |
lead_id | string | Associated lead ID |
closer_id | string | null | Assigned host's user ID |
booking_link_id | string | null | Which funnel / product the call came through |
scheduled_at | string | Scheduled meeting time (ISO 8601) |
duration | number | null | Scheduled duration in minutes |
status | string | null | Call status (scheduled, attended, no_show, canceled, etc.) |
conferencing_url | string | null | Video conference URL |
utm_source | string | null | Attribution: traffic platform |
utm_medium | string | null | Attribution: channel type |
utm_campaign | string | null | Attribution: campaign name |
utm_term | string | null | Attribution: paid search keywords |
utm_content | string | null | Attribution: ad creative or A/B variant |
referral | string | null | Partner / affiliate identifier (separate from utm_source) |
guest_rsvp_status | string | null | Guest's calendar RSVP (yes, no, maybe, noreply) |
guest_rsvp_confirmed_at | string | null | When the guest first RSVP'd yes |
created_at | string | When the call was created |
updated_at | string | When the call was last updated |
Call attribution fields are flat on the call payload (utm_source etc.), whereas lead attribution is nested under source. This intentional asymmetry mirrors the underlying schema: calls inherit UTM at booking time from the originating lead, but are stored as columns for fast pipeline filtering.
call.cancelled payload
call.cancelled is the booking funnel's cancel event, and it is the one event
that does not use the envelope above. Its body is the flat booking payload: the
call's fields sit at the top level, with "event": "call.cancelled", and there
is no data wrapper. Verify it with X-Aura-Signature-V1; it does not carry
the legacy X-Aura-Signature.
It replaces call.canceled, which takes no new subscriptions. Moving a
subscription from call.canceled to call.cancelled changes the body as well
as the event name, so update how your handler reads it.
LeadDeletedWebhookData
Sent on lead.deleted. It holds the lead as it was when Aura removed it.
You can't look the lead up after this, so this is the last copy of these
fields.
| Field | Type | Description |
|---|---|---|
id | string | The deleted lead's ID. Same as lead_id |
lead_id | string | The deleted lead's ID |
full_name | string | null | The lead's name when it was deleted. null when the person asked to be erased |
email | string | null | The lead's email when it was deleted. null when the person asked to be erased |
phone | string | null | The lead's phone number when it was deleted. null when the person asked to be erased |
consent | array | The lead's consent choices, the same entries as on LeadWebhookData. Empty when the person asked to be erased |
consent_summary | string | null | The same choices as text, one line per box |
consent_by_type | object | The lead's last answer for each kind of message, the same object as on LeadWebhookData. Every kind reads not_asked when the person asked to be erased |
reason | "deleted" | "erased" | deleted: an admin deleted the lead. erased: the person asked us to erase their data |
deleted_at | string | When Aura removed the lead |
event, org_id, timestamp, aura_version | string | The same record Zapier receives, so one handler can read both |
CallCloserReassignedWebhookData
Emitted on call.closer_reassigned. This is the only call event with a non-CallWebhookData shape — it carries the swap details rather than the full call record.
| Field | Type | Description |
|---|---|---|
call_id | string | Affected call ID |
previous_closer_id | string | null | The host being unassigned |
previous_closer_name | string | Cached display name of the previous host |
new_closer_id | string | The host being assigned |
new_closer_name | string | Cached display name of the new host |
scheduled_at | string | Call's scheduled time (unchanged by reassignment) |
conferencing_link | string | null | Conferencing URL on the new event |
new_event_id | string | The canonical Nylas event ID after the swap |
booking_link_id | string | null | Originating scheduler |
reassigned_by_user_id | string | Admin or Partner API caller who performed the swap |
old_event_orphaned | boolean | true if the old event could not be cleanly canceled on the previous host's calendar |
CallAnalysisWebhookData
Emitted on call.analysis_completed. Fired when the AI review of a call finishes. Carries headline scores for routing/alerting plus analysis_url for the full review — the full analysis body is intentionally not inlined (tens of KB of evolving JSON).
| Field | Type | Description |
|---|---|---|
analysis_id | string | Analysis record ID |
call_id | string | Call that was analyzed |
lead_id | string | null | Associated lead, if known |
overall_score | number | null | 0–100 overall score; null when unscorable (e.g. no-show) |
closing_score | number | null | 0–100 closing score |
emotional_score | number | null | 0–100 emotional score |
structure_score | number | null | 0–100 structure score |
buyer_certainty_index | number | null | Buyer certainty index |
verdict | string | null | "hire", "retain", or "replace" |
confidence | number | null | Model confidence 0–1 |
primary_objection | string | null | Primary objection category (e.g. "money", "timing") |
offer_made | boolean | null | Whether an offer was presented on the call |
analyzed_at | string | When analysis finished (ISO 8601) |
analysis_url | string | Absolute URL of GET /v1/calls/{id}/analysis for the full review |
PaymentWebhookData
Emitted on payment.succeeded, payment.failed, payment.refunded.
| Field | Type | Description |
|---|---|---|
id | string | Payment ID |
lead_id | string | null | Associated lead, if known |
call_id | string | null | Associated call, if the payment is tied to a deal |
amount | number | Amount in the currency's smallest unit (e.g. cents for USD) |
currency | string | ISO 4217 lowercase (usd, eur, etc.) |
status | string | Aura's canonical payment status (succeeded, failed, refunded, pending). Providers use different words for the same state — a Stripe Invoice says paid where a Charge says succeeded — so provider vocabulary is normalised on write. Treat succeeded as the single "money arrived" value. |
payment_date | string | When the payment was processed (ISO 8601) |
created_at | string | When the payment record was created |
Conventions
Timestamps
All timestamps are ISO 8601 UTC (Z suffix):
2026-05-12T14:30:00.000ZNull handling
Optional fields are emitted as null rather than omitted. Always treat the contract as nullable for any field marked string | null here:
const phone = data.phone ?? "(not provided)";Forward compatibility
Aura may add new fields to any payload without a major version bump. Consumers must ignore unknown fields. Removing or renaming a documented field is a breaking change and will trigger a schema-diff review on our side before shipping.
Payload version
Every webhook envelope carries the payload contract version in version, and every Zapier payload carries the same value in aura_version. It is the version of the Aura Zapier app, so the two always match.
In October 2026 it jumped from 1.8.5 to 1.11.0. Nothing in the payload changed with that jump: the value had fallen behind the Zapier app, and now it is pinned to it. The next release, 1.12.0, adds the consent fields, 1.13.0 hides the Lead Status Changed trigger, 1.14.0 adds form_completed to lead.disqualified, 1.15.0 adds the lead.deleted event, 1.15.1 changes only how Lead Disqualified is described: it fires 3 minutes after a lead stops, not 30, 1.16.0 adds deal payments to call.closed, and 1.17.0 adds consent_by_type. Treat version as information. Don't branch on it: check for the fields you need instead.
Verifying signatures
Webhook deliveries carry a signed header X-Aura-Signature-V1 of the form:
X-Aura-Signature-V1: t=1715520600000,v1=<sha256_hex>[,v1=<previous_sig>]The signed string is ${timestamp}.${rawBody} — not the body alone — which prevents replay attacks. Reject deliveries whose t differs from your server clock by more than 5 minutes.
During a 24-hour secret rotation window, two v1= segments are present. Accept the delivery if either signature verifies — this lets you rotate secrets with zero downtime.
The legacy bare-hex X-Aura-Signature header is still sent for backwards compatibility but offers no replay protection; new integrations should verify the V1 header only.
Next steps
- Event Types — when each event fires
- Webhooks Overview — setup, retries, SSRF rules
- API Reference — the same data, but pull-based