Your AI Connector Docs

Webhooks

Webhooks let Your AI Connector automatically notify your other business tools whenever something important happens — a new contact being created, an appointment being booked, a message being received. Instead of manually checking for updates, your connected systems get an instant notification the moment something happens.


What Are Webhooks?

Think of a webhook like an automatic text message between two apps. When something happens in Your AI Connector (like a new contact signing up), the platform instantly sends a notification to another system of your choice. You provide a web address (called a “webhook URL”) where these notifications should be sent — this is typically provided by your CRM, automation platform, or developer.

Webhooks only send data OUT of Your AI Connector. A webhook is a one-way street from Your AI Connector to your other tools. There is no webhook URL that sends leads, contacts, or messages INTO the platform. To push a new lead in — from a website form, your CRM, or GoHighLevel — your system makes an API call instead. See API Access (the Create a Contact operation) and Funnels. The only thing you need for the inbound direction is your API key, which lives in its own section — see API Access. The Webhooks page described here is exclusively for the outbound direction.

Note: Setting up webhooks involves some technical configuration. If you’re not comfortable with this, share this page with your developer or use an automation platform like Zapier, Make, or Pabbly, which provide webhook URLs with no coding required.

Common uses include:

  • Syncing new contacts to your CRM.
  • Triggering a workflow in Zapier, Make, or Pabbly when a tag is applied.
  • Notifying your team in Slack when a human is alerted.
  • Updating your calendar system when an appointment is booked.
  • Logging conversation summaries to your database.

Setting Up Webhooks

  1. In the left sidebar, click Settings (gear icon).
  2. In the Settings sidebar, under the Integrations group, click Webhooks.

On an account with no webhooks configured yet, the page looks like this:

  1. Click New webhook, top-right. A form opens inline on the page:
  1. Fill in:
    • Endpoint URL — the web address Your AI Connector will send event notifications to. You get this from your external system (CRM, automation platform, or custom server).
    • Name — a label you’ll recognize later (e.g. “Slack alerts” or “CRM sync”). For your reference only.

Your webhook URL must be a publicly reachable https:// address. Plain http:// addresses, localhost or private-network addresses, and platform-internal addresses are rejected when you save. To test from your own machine, use a public tunnel (webhook.site or ngrok) instead of localhost.

  1. Under Events, click the events you want this webhook to receive — all 22 are listed in The 22 Webhook Events.
  2. (Optional) Turn on Retry failed deliveries if you want Your AI Connector to keep trying on a temporary failure — see Retrying Failed Deliveries.
  3. Click Create webhook. It appears in the list below the form, and you can click Test on its row any time to fire a sample payload at your endpoint.

Permission needed. Adding, editing, or testing webhooks requires the Integrations “edit” permission (view-only team members see a read-only notice instead of the form).

Signing a webhook requires it to already be saved first — open an existing webhook’s row to edit it, and the Signing secret panel appears at the bottom of the edit form. A brand-new, unsaved draft has no signing option yet — see Signed Payloads below.


One Webhook for All Your Client Accounts (Agencies)

If you run an agency, you don’t have to re-create the same webhook on every client account. On the agency account, the webhook form has an extra toggle: Also fire for all client accounts. Turn it on and this webhook also receives events that happen on every client account under your agency — one endpoint, whole agency.

How it behaves:

  • The user block tells you which client an event belongs to. Every notification already carries a user block identifying the account the event happened on, so your automation can route per client.
  • Your webhook’s own settings apply everywhere. The events you selected, the signing secret, and the retry setting are used for client-account deliveries too.
  • No double deliveries. If a client account has its own webhook pointing at the same URL, that one is used for that account’s events instead — the same event never arrives twice at one endpoint.
  • Clients don’t see it. The webhook does not appear on the client account’s own Webhooks page, and clients can’t switch it off — it’s yours to manage.
  • Reliability is tracked per client account. If your endpoint keeps failing, it is switched off automatically for the account whose deliveries failed (see Webhook Reliability), not for the whole agency at once.

The toggle only appears on agency accounts. Setting it over the API is also supported — see the apply_to_sub_accounts field in the Webhooks API.


Available Trigger Events

You can enable or disable each of the 22 webhook events independently. When an event fires, Your AI Connector sends a notification to your webhook URL with the relevant data. Every event, what it means, and the event code it puts in the payload are listed together in The 22 Webhook Events further down this page.

Good to know: Task Created, Task Updated, and Task Completed are fully selectable and save correctly. Daily Summary Created is also a recent addition. See Task Completed Webhook below for that payload shape.


Tag-Based Webhook Triggers

subscribed_to_tags does not scope a webhook’s events to a tag. It only narrows which tags produce a conversation-summary notification. To get a request when a specific tag is applied, set a webhook URL on that tag in the Tags tab of the agent (or campaign).

The webhook form itself has no tag picker, either when creating a new webhook or when editing one, so subscribed_to_tags can only be read or changed through the Webhooks API, or by asking support.

Good to know: editing an existing webhook that has a subscribed_to_tags list (renaming it, changing its events, toggling retries) no longer clears that list — since the form has no tag picker to send back, saving from this page now leaves the existing list untouched. (This was a real bug before July 21, 2026: saving from the webhook form used to erase the list because it always sent an empty tag list. If a webhook lost its subscribed_to_tags list before that date, it will need to be reconfigured through the API.)

Generate Summary for Tagged Contacts

Where a webhook has a subscribed_to_tags list, you can turn on Generate Summary. When enabled, Your AI Connector automatically generates a conversation summary for the contact when one of those tags is applied, and includes it in the webhook data — full context without a separate request.


Testing Your Webhook

  1. Open Settings → Integrations → Webhooks.
  2. On your webhook’s row, click Test.
  3. Check your external system to confirm it received the test data.
  4. Review the data format to make sure your system can parse it correctly.

For a full end-to-end test, send a message that would trigger one of your configured events (a broadcast, or an incoming message on a connected channel) and verify the webhook fires with the real data.

Tip: Use a tool like webhook.site or RequestBin during development to inspect the raw webhook data before connecting your production system.

What Counts as a Successful Delivery

Whether you click Test or the event fires for real, we send the same thing:

  • A POST request (never GET), with the body as JSON and Content-Type: application/json.
  • The headers listed under Signed Payloads. Signature headers are only included once you’ve set a signing secret.

We treat the delivery as successful when:

  • Your endpoint answers with any 2xx status (200, 201, 204 — all fine).
  • It answers within 30 seconds.

A few things that surprise people:

  • The response body is ignored. You don’t need to return any particular JSON. An empty 200 is enough.
  • Redirects count as a failure. We don’t follow them, so a 301 or 302 (including a trailing-slash redirect, or http to https) is recorded as a failed delivery. Save the final URL, not one that redirects.
  • Query strings are fully supported. https://your-app.com/hook?token=abc123 is sent exactly as you saved it, so putting a token in the query string works just as well as putting it in the path.
  • Your URL must be https:// and publicly reachable. Addresses that belong to Your AI Connector's own infrastructure are rejected, but your own endpoints on Google Cloud Functions, Cloud Run, App Engine, Firebase Hosting or anywhere else are fine.
  • A firewall or bot-protection layer in front of your endpoint can block us. The most common case is Cloudflare: if your zone has Bot Fight Mode or a managed challenge on, our request gets a “Just a moment…” challenge page with a 403 instead of reaching your server — and a server-to-server request can never pass a browser challenge, so both the Test button and real events fail the same way. The Test button will tell you when this is happening (“Cloudflare is showing a bot challenge to our request”). Fix it in Cloudflare with a Security / WAF rule that skips challenges for your webhook path (or for the Webhook-Delivery/1.0 user agent), then click Test again.
  • If your firewall needs an IP allowlist instead (for example Cloudflare’s free plan, where plain Bot Fight Mode can’t be skipped by a WAF rule, but an IP Access Rule set to Allow runs before it), we can help: every delivery, whether from the Test button or a live event, is sent from one fixed IPv4 address (no ranges, no IPv6, no rotation). Contact support and we will give you the address to allowlist. Keep signature verification as your actual trust check, since it validates every payload regardless of where it came from.
  • The Test result tells you exactly what your endpoint answered. A failed test now shows the real reason (the HTTP status your endpoint returned, a timeout, or that we couldn’t reach the address at all) instead of a generic error, and a test on a saved webhook is sent signed when signing is on, exactly like a live event.

Using n8n, Make, or Zapier (“Test URL” vs “Production URL”)

Automation platforms usually give you two different webhook addresses, and this trips people up:

  • A Test URL (in n8n it contains /webhook-test/). This only receives data while you’re actively watching the canvas and have just clicked Listen for test event (or Test workflow). It captures a single event and then stops listening — so clicking Test in Your AI Connector several times in a row only catches the first one, and only if the listen window is active at that exact moment. To test: click Listen for test event in n8n first, then come back to Your AI Connector and click Test once.
  • A Production URL (in n8n it contains /webhook/, no -test). This is the one to paste into Your AI Connector for live events. It only works once your workflow is switched Active. If the workflow isn’t active, n8n rejects the request with a “404 / webhook not registered” error, even though Your AI Connector sent the data correctly.

In short: test with the Test URL while listening, but for the webhook to keep working on real contacts, save the Production URL in Your AI Connector and make sure the workflow is Active.


Webhook Data Format

When a webhook fires, Your AI Connector sends structured data (JSON) to your webhook URL. If you’re using an automation platform like Zapier or Make, it parses this data for you automatically. If you’re building a custom integration:

{
  "event": "contactCreated",
  "contact": { "id": "<contact-id>", "first_name": "Jane", "...": "..." },
  "campaign": { "id": "<campaign-id>", "name": "AI Receptionist", "status": "Live" },
  "agent": { "id": "<agent-id>", "name": "Front Desk" },
  "user": { "id": "<account-id>", "email": "owner@example.com" }
}
Field Description
event The exact event string that triggered the notification (for example, contactCreated, booked). This is not the display label shown in the events list; every label and its matching code is in The 22 Webhook Events.
contact The contact the event is about, or null for events not tied to a contact (such as creditsRecharged).
campaign The campaign the contact belongs to, or null if there isn’t one.
agent The agent handling the conversation, or null if there isn’t one.
user Basic identity information for the account that owns the data.

campaign or agent — usually one, not both. If your account uses agents, your contacts sit with an agent rather than a campaign, so campaign arrives as null and agent tells you which one handled it. Older campaign-based accounts see the reverse. Read whichever one is filled in; don’t assume campaign is always there.

The agent block arrived on 15 August 2026. It sits alongside campaign on the events tied to a conversation — a concluded chat, do-not-disturb, a resume, an unarchive, an AI pause, a new message, a conversation summary, and the webhook you can set on a tag — and carries the handling agent’s id and name, or null when no agent is involved. It is purely additive: every field you already receive is unchanged, so a receiver you built before that date keeps working with nothing to update.

Some events add their own extra top-level block. For example, Appointment Booked adds an appointment block (see Appointment Booked Webhook), New Message adds a full message block with the text (see New Message Webhook), and Deliveries and Reads add a short message block with just the message’s ID and status (see Deliveries and Reads Webhook).

Deliveries and Reads tell you which message, but not what it said. They carry a message block containing the message’s id and status — and that id is the same messageId the send message endpoint hands back, so you can match a delivery or read receipt to the exact message you sent — but no message body. Replies carries no message block at all. If you need the words that were sent or received, subscribe to New Message alongside them.

Two things to know before you write your receiver. There’s no timestamp field, and no data wrapper. Every block sits at the top level of the JSON object, as shown above.

The 22 Webhook Events

The 22 webhook events, with the display label you tick in the app and the event code sent in the payload. The event code is a short string that does not match the display label, so match your receiver on the code, not the label:

Display label (in the app) event code in the payload What it means
Contact Created contactCreated A new contact is added to your account (manually, via import, or via API).
Contact Paused contact_paused A contact conversation is paused (bot stops responding).
Contact Resumed contact_resumed A paused contact conversation is resumed.
Contact Do Not Disturb contact_do_not_disturb_changed A contact’s Do Not Disturb setting is turned on.
Contact Unarchived contact_unarchived An archived contact sends a new message, bringing them back into your active inbox.
New Message new_message Any message is added to a conversation on any channel — both messages your contact sends you and messages your AI or your team sends them. This is the only event that carries the actual message text (see New Message Webhook).
Replies replied A contact replies to a message.
Reads read A contact reads a message (on channels that support read receipts). Carries the ID of the message that was read — see Deliveries and Reads Webhook.
Deliveries delivered or undelivered A message is successfully delivered to a contact (undelivered when delivery fails). Carries the ID of the message — see Deliveries and Reads Webhook.
Human Alerted humanAlerted The AI bot determines it cannot handle a conversation and flags it for human attention.
Chat Concluded chat_concluded The AI bot decides a conversation has reached its end (booking made, lead disqualified, etc.).
Appointment Booked booked A contact books an appointment through the booking system.
Credits Spent creditsSpent Credits are deducted from your account.
Credits Recharged creditsRecharged Credits are added to your account via auto-recharge or manual purchase.
Low Credit Balance lowCreditBalance on a Test delivery, Low Credit Balance on a real one An early warning that your credit balance has dropped below your alert threshold (100 credits unless you set your own). Aimed at agencies, whose sub-accounts all spend from one pool. It carries balance, threshold and account_email instead of a contact block, is sent at most once every 24 hours while the balance stays low, and re-arms as soon as the balance goes back above the threshold.
Task Created taskCreated A task is created.
Task Updated taskUpdated A task changes without moving into a completion stage.
Task Completed taskCompleted A task moves into a stage configured as a completion stage.
Daily Summary Created dailySummaryCreated Your daily summary report is generated.
Channel Connected channelConnected Not sent yet — selectable, but nothing emits it today. Don’t build against it. Intended for when a messaging channel finishes connecting.
Broadcast Started broadcastStarted A broadcast begins sending (its status changes to Sending). Fires once per start, including when a paused broadcast is resumed. Carries a broadcast block instead of a contact block: id, name, channel, status, previous status, the list it targets (list_id, list_name, is_smart_list), scheduled_at, total_contacts.
Broadcast Completed broadcastCompleted A broadcast finishes (its status changes to Sent or Failed). Same broadcast block plus completed_at and, when available, completion_summary (total_sent, permanently_failed, unique_replied, failure_rate, had_errors). Use these two to connect a Smart Broadcast List to external tools.

Two more codes never appear in that list because you don’t subscribe to them: contact_tags_updated, sent by a webhook URL set on an individual tag, and summary_generated, sent when a chat summary is written for a tag in a webhook’s subscribed_to_tags list.

Channel Connected is not sent yet. It appears in the events list, but nothing emits it today. Don’t build against it.

Tag-based and task notifications use their own separate shapes. See Contact Tags Updated and Task Completed.


Contact Created Webhook

Sent when the Contact Created event fires (a new contact is added manually, via import, or via API).

Event name

contactCreated

Payload format

{
  "event": "contactCreated",
  "contact": {
    "id": "<contact-id>",
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith",
    "human_alerted": false,
    "human_alert_reason": null,
    "is_bot_active": true,
    "ad_referral": null
  },
  "campaign": {
    "id": "<campaign-id>",
    "name": "AI Receptionist",
    "status": "Live"
  },
  "agent": {
    "id": "<agent-id>",
    "name": "Front Desk"
  },
  "user": {
    "id": "<account-id>",
    "email": "owner@example.com",
    "first_name": "Alex",
    "last_name": "Doe"
  }
}
Field Description
event Always contactCreated for this event.
contact.id The unique ID of the new contact.
contact.email / contact.phone_number The contact’s email and phone, if known (either may be empty depending on the channel).
contact.first_name / contact.last_name The contact’s name, if known.
contact.human_alerted / contact.human_alert_reason Whether the contact is flagged for human attention, and why.
contact.is_bot_active Whether the AI bot is currently active on this contact.
contact.ad_referral Meta Click-to-WhatsApp ad attribution, or null — see Click-to-WhatsApp Ad Attribution.
campaign The campaign the contact was created under, or null.
agent The agent assigned to the contact, or null.
user Basic identity information for the account that owns the contact.

The “Test” sample and a real event look slightly different. The test button sends placeholder data (John Doe, a sample campaign). A real Contact Created event carries the actual contact’s details, and some fields may be empty depending on the channel.


New Message Webhook

This webhook fires every time a message is added to a conversation, on any channel. It covers both directions: messages your contact sends you, and messages your AI, your team, or a campaign sends them. It is the only webhook that includes the message text, so it is the one to use when you want to mirror conversations into an external system.

Event name

new_message

Payload format

{
  "event": "new_message",
  "contact": {
    "id": "<contact-id>",
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith",
    "human_alerted": false,
    "human_alert_reason": null,
    "is_bot_active": true,
    "ad_referral": null
  },
  "agent": {
    "id": "<agent-id>",
    "name": "Front Desk"
  },
  "user": {
    "id": "<account-id>",
    "email": "owner@example.com",
    "first_name": "Alex",
    "last_name": "Doe"
  },
  "message": {
    "id": "<message-id>",
    "body": "Hi, are you open on Saturday?",
    "direction": "inbound",
    "status": "received",
    "created_at": "2026-07-30T17:27:06.000Z",
    "channel": "whatsapp_web"
  }
}
Field Description
event Always new_message for this event. Note this is the exact string sent — it is not the display label “New Message”.
contact The contact whose conversation the message belongs to. Same shape as in Contact Created.
agent The agent handling the conversation (id and name), or null if no agent is involved.
user Basic identity information for the account that owns the conversation.
message.id The unique ID of the message.
message.body The message text. Empty for a message that carries only an attachment (image, voice note, document).
message.direction inbound for a message from the contact, outbound for one sent by your AI or by your team from the inbox, and outbound-api for one sent by a campaign, a broadcast, a template send, or the API.
message.status Where the message is in its lifecycle: received for incoming, and queued / sent / delivered / read / failed / undelivered for outgoing. This is the status at the moment the message was created, so an outgoing message usually arrives here as queued or sent and reaches delivered afterwards — use the Deliveries and Reads events if you need those later transitions. They carry the same message.id as this block, so you can match the transition to this message (see Deliveries and Reads Webhook).
message.created_at When the message was created, in UTC (ISO 8601).
message.channel The channel the message went through, for example whatsapp, whatsapp_web, sms, instagram, messenger, telegram, email or custom.

There is still no campaign block in this payload. New Message sends contact, agent, user and message. The agent block was added on 15 August 2026 and tells you which agent handles the conversation; if you need campaign context as well, look the contact up through the API using contact.id.

Internal AI records do not fire this webhook. Alongside real messages, the platform keeps its own bookkeeping rows in a conversation (the AI’s tool calls and internal turn records). Those are never sent — you only receive messages that were genuinely sent or received.


Deliveries and Reads Webhook

These two events report what happened to a message after it left Your AI Connector: Deliveries fires when a message reaches the contact (or fails to), and Reads fires when the contact opens it, on the channels that support read receipts.

Both carry a message block with the ID of the message the event is about, so you can match the update to the exact message you sent.

Event names

delivered and undelivered for Deliveries, read for Reads.

Payload format

{
  "event": "delivered",
  "contact": {
    "id": "<contact-id>",
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith",
    "ad_referral": null
  },
  "campaign": {
    "id": "<campaign-id>",
    "name": "AI Receptionist",
    "status": "Live"
  },
  "agent": {
    "id": "<agent-id>",
    "name": "Front Desk"
  },
  "user": {
    "id": "<account-id>",
    "email": "owner@example.com",
    "first_name": "Alex",
    "last_name": "Doe"
  },
  "message": {
    "id": "<message-id>",
    "status": "delivered"
  }
}
Field Description
event delivered or undelivered for Deliveries, read for Reads.
contact The contact the message was sent to.
campaign The campaign the contact belongs to, or null.
agent The agent handling the conversation, or null.
user Basic identity information for the account that owns the data.
message.id The ID of the message this update is about. It is the same value the send message endpoint returns as messageId, and the same message.id a New Message notification carries.
message.status The new status, always the same string as event (delivered, undelivered or read).

How to match an update to the message you sent. Store the messageId you get back when you send a message through the API. When a Deliveries or Reads notification arrives, look up that stored ID against message.id in the payload — that’s your delivery or read receipt for that exact message.

There is no message text here. The message block carries only the ID and the status. Subscribe to New Message if you also need the body.

The message block is only present when we know which message it was. On the rare update we can’t tie back to a stored message, the block is left out entirely rather than sent empty — so check that message exists before reading message.id.

One notification per status change. A single outgoing message normally produces a delivered notification and then, on channels with read receipts, a read one. A failed send produces undelivered instead.


Appointment Booked Webhook

Fires when a contact books an appointment. It fires the same way whether the AI booked it during a conversation, you booked it by hand, or it came in through the API.

Event name

booked

Payload format

{
  "event": "booked",
  "contact": {
    "id": "<contact-id>",
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith"
  },
  "campaign": {
    "id": "<campaign-id>",
    "name": "AI Receptionist",
    "status": "Live"
  },
  "user": {
    "id": "<account-id>",
    "email": "owner@example.com"
  },
  "appointment": {
    "appointment_id": "<appointment-id>",
    "start_time": "2026-07-20T15:00:00.000Z",
    "end_time": "2026-07-20T15:30:00.000Z",
    "status": "confirmed",
    "room_name": "Room 1",
    "description": "Discovery call",
    "summary": "30 min intro",
    "google_calendar_event_id": null,
    "event": {
      "id": "<service-id>",
      "event_name": "Intro Call",
      "slot_duration": 30,
      "location": "Zoom",
      "meeting_link": "https://...",
      "event_type": "online"
    }
  }
}
Field Description
event Always booked for this event.
contact The person who booked. email and phone_number may be empty depending on the channel.
appointment.appointment_id The unique ID of the booking.
appointment.start_time / end_time Start and end of the booked slot, in UTC (ISO 8601).
appointment.status The booking’s current status.
appointment.room_name The room the booking was placed in, if used.
appointment.description / summary Free-text details captured with the booking.
appointment.google_calendar_event_id Google Calendar’s ID for the synced event. It is often null in the Appointment Booked webhook, because the calendar event is created at the same moment the notification is sent — re-fetch the appointment by its appointment_id a moment later if you need it, and expect a permanent null on accounts with no Google Calendar connected.
appointment.event The service that was booked: name, slot length, location, meeting link, type.

google_calendar_event_id is often null in this webhook, and that is normal. The Google Calendar event is created at the same moment this notification goes out, so the ID usually isn’t ready yet. Re-fetch the appointment by its appointment_id a moment later if you need it. It stays null permanently if the account has no Google Calendar connected, so don’t wait on it forever.

The “Test” button does not include the appointment block. Use it to confirm your endpoint answers, then make one real booking to see the full payload.

Two cases where this webhook does not fire: appointments imported from an external calendar, and bookings that come in through the Formitable integration.


Contact Tags Updated Webhook

Fires when a tag is applied to a contact, and that tag has a webhook URL configured on the agent or campaign the contact belongs to.

Event name

contact_tags_updated

When it fires

  • A tag is applied to a contact that has an agent assigned, a campaign assigned, or both.
  • At least one of the applied tags has a webhook URL set in the Tags tab of that agent or campaign.

If the contact has both and the campaign’s tags carry webhook URLs, those win; the agent’s are used otherwise.

If multiple tags with different webhook URLs are applied in the same update, one request is sent per URL, each containing only the tags that map to that URL.

Removing a tag never sends a request. Most people point these URLs at an action — collect a deposit, book a slot, alert a rep — so a tag coming back off a contact used to re-run that action. It no longer can. A removal still shows up in removed_tags when it happens in the same update as an apply that goes to the same URL, so an automation that reads both arrays keeps the full picture; what it will never see is a request caused by a removal alone. (Changed August 12, 2026. Before that date, removals sent a request too.)

Payload format

{
  "event": "contact_tags_updated",
  "contact": {
    "id": "<contact-id>",
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith",
    "human_alerted": false,
    "is_bot_active": true,
    "ad_referral": {
      "ctwa_clid": "ARAbc123...",
      "source_id": "120210000000000",
      "source_type": "ad",
      "source_url": "https://fb.me/xxxx",
      "headline": "Get 20% off today",
      "body": "Message us now to claim your discount",
      "channel": "whatsapp"
    }
  },
  "added_tags": ["qualified-lead"],
  "removed_tags": ["new-lead"],
  "agent": {
    "id": "<agent-id>",
    "name": "Front Desk"
  },
  "user": {
    "email": "owner@example.com",
    "first_name": "Alex",
    "last_name": "Doe"
  }
}
Field Description
event Always contact_tags_updated for this webhook.
contact.id The unique ID of the contact whose tags changed.
contact.email / contact.phone_number The contact’s email/phone, if known.
contact.first_name / contact.last_name The contact’s name.
contact.human_alerted Whether the contact is currently flagged for human attention.
contact.is_bot_active Whether the AI bot is currently active on this contact’s conversation.
contact.ad_referral Present only when the contact first reached you through a Meta Click-to-WhatsApp (CTWA) ad or post. null otherwise.
added_tags Array of tag names applied in this update. Never empty — an apply is what triggers the request.
removed_tags Array of tag names removed in the same update, if any. A removal on its own does not send anything.
agent The agent handling the contact’s conversation (id and name), or null if no agent is involved. Added 15 August 2026.
user Basic identity information for the account that owns the contact.

Testing a tag webhook

Next to the webhook URL field on the Tags tab there is a Test button. It sends a sample payload to that URL immediately, so you can confirm your automation receives it before waiting on a real conversation.

The test sends the same contact_tags_updated shape shown above, using a placeholder contact, with the tag you are testing in added_tags and an empty removed_tags. What your automation sees in the test is what it will see in production.

Two things to know:

  • Save the tag first. The test looks the tag up by its saved name, so a brand new tag or an unsaved rename can’t be tested yet. The button stays greyed out until the name on screen matches the saved one.
  • A failed test doesn’t count against your webhook. Tests never contribute to the automatic switch-off after repeated failures described in Webhook Reliability.

If the test fails, the message tells you what your endpoint answered (for example a 404 or 500), which is usually enough to spot a wrong URL or a workflow that isn’t switched on.


Task Completed Webhook

For reference only. Task webhooks (as data) are documented here for developers; the Task Created, Task Updated and Task Completed events are selectable in the standard events list on the webhook form like any other event — see Available Trigger Events and The 22 Webhook Events.

This payload is sent when a task transitions into a stage marked as a completion stage. A task moving between non-completion stages sends the taskUpdated shape instead.

Event name

taskCompleted

When it fires

  • A task is updated.
  • Its stage value changed compared to its previous value.
  • The new stage is configured as a completion stage on the account’s task stage settings.

Payload format

{
  "event": "taskCompleted",
  "contact": {
    "email": "jane@example.com",
    "phone_number": "+15551234567",
    "first_name": "Jane",
    "last_name": "Smith",
    "human_alerted": false,
    "human_alert_reason": null
  },
  "user": {
    "email": "owner@example.com",
    "first_name": "Alex",
    "last_name": "Doe"
  },
  "message": {
    "id": "<task-id>",
    "title": "Follow up with Jane",
    "description": "Confirm pricing and send proposal",
    "type": "follow_up",
    "priority": "high",
    "stage": "<stage-id>",
    "due_date": "2026-01-20T15:00:00Z",
    "source": "ai",
    "source_detail": "<source-detail>",
    "campaign_id": "<campaign-id>",
    "linked_human_alert": "<human-alert-id>",
    "tags": ["qualified-lead"],
    "notes": "Customer requested a callback"
  }
}
Field Description
event Always taskCompleted for this webhook. The same payload shape is sent as taskUpdated when a task changes without entering a completion stage.
contact The contact linked to the task, if any. null when not linked.
contact.human_alert_reason The reason the contact was flagged for human attention, if applicable.
user Basic identity information for the account that owns the task.
message.id The unique ID of the task.
message.title / description The task’s title and description.
message.type The task type (for example, follow_up, call, custom).
message.priority The task priority (low, medium, high).
message.stage The ID of the stage the task is now in.
message.due_date The task’s due date, if set.
message.source What created the task (ai, manual, api).
message.source_detail Additional detail about the source.
message.campaign_id The ID of the linked campaign, or null.
message.linked_human_alert The ID of the linked human alert, if any.
message.tags Tags applied to the task.
message.notes Free-form notes on the task.

Switching a Webhook Off (or Deleting It)

Every webhook has an on/off switch, right on its row. Switching one off stops it receiving events, but keeps everything you configured — the URL, the events, any signing secret. Switch it back on and it picks up where it left off; nothing that happened while it was off is delivered afterwards.

Use it when you want deliveries to stop for a while: your endpoint is being rebuilt, you’re debugging a noisy integration, or you’re pausing an automation.

Deleting a webhook (the trash icon on its row) removes it for good, including its signing secret. If you only want deliveries to stop, switch it off instead — delete is for when you’re done with the endpoint entirely.

This is not the same as a webhook being switched off automatically. If we disable your webhook after repeated failures (see Webhook Reliability), the toggle above won’t bring it back. Once your endpoint is fixed, edit the webhook and save it with a changed URL (any URL change re-enables it), or call the re-enable endpoint over the API — or ask support and we’ll flip it back for you.


Signed Payloads (Verifying a Webhook Really Came From Us)

Anyone who learns your webhook URL could send a fake request to it. If you act on webhooks automatically — updating billing, creating CRM records — turning on signing lets you verify each request genuinely came from us.

Signing is optional and off by default, and you switch it on per webhook, from that webhook’s edit view (open a saved webhook’s row).

Turning on signing

  1. Open the webhook (Settings → Integrations → Webhooks → click your webhook’s row).
  2. In the Signing secret section, click Generate.
  3. Copy the secret (it starts with whsec_) and store it in your receiving system. Treat it like a password.

You can come back and reveal, copy, rotate, or turn off the secret any time from this same panel.

What we send

Once signing is on, every delivery for that webhook carries these two extra HTTP headers:

Header Meaning
X-Webhook-Signature The signature, in the form v1=<hex>.
X-Webhook-Timestamp When we sent it, as a Unix timestamp in seconds.

These three are on every delivery, signed or not:

Header Meaning
X-Webhook-Delivery A unique ID for this event. Stays the same across retries, so it’s what you dedupe on.
X-Webhook-Attempt Which attempt this is (1 is the first try).
X-Webhook-Event The event name, so you can route without reading the body.

How to verify

The signature is an HMAC-SHA256 of the string <timestamp>.<raw request body>, using your signing secret as the key.

Verify against the raw request body — the exact bytes you received. If your framework parses the JSON and re-serializes it before checking, the bytes can change and the signature won’t match.

Node.js example:

const crypto = require("crypto");

function verify(rawBody, headers, secret) {
  const timestamp = headers["x-webhook-timestamp"];
  const signature = headers["x-webhook-signature"]; // "v1=<hex>"

  // Reject anything older than 5 minutes so a captured request can't be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature.replace("v1=", "")), Buffer.from(expected));
}

Python example:

import hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers["X-Webhook-Timestamp"]
    signature = headers["X-Webhook-Signature"].replace("v1=", "")

    # Reject anything older than 5 minutes so a captured request can't be replayed later.
    if abs(time.time() - int(timestamp)) > 300:
        return False

    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()

    return hmac.compare_digest(signature, expected)

Compare signatures with a timing-safe function (timingSafeEqual / compare_digest), not ==. It costs nothing and avoids a subtle class of attack.

Rotating the secret

Click Rotate to replace the secret. The switch is immediate: the very next delivery is signed with the new secret only. If your endpoint is live, accept both the old and the new secret for a few minutes while you deploy the new one.

Turning signing off simply stops the signature headers being sent.


Retrying Failed Deliveries

By default, a delivery that fails is not retried — if your system is down at that moment, that event is missed.

Turn on Retry failed deliveries on a webhook (in the create/edit form) and we’ll keep trying:

Attempt When
1 Immediately
2 1 minute later
3 5 minutes later
4 30 minutes later
5 2 hours later

That spans roughly 2 hours and 40 minutes, so a webhook can survive a maintenance window or a short outage on your side.

What gets retried: temporary problems — your server returning a 5xx error, a timeout, or a connection failure.

What does not: if your endpoint rejects the request itself (any 4xx), we don’t retry — sending the identical request again would only produce the identical rejection.

Which events retry: tag webhooks (contact_tags_updated), the three task events, and the daily summary. The rest are sent once, so for those the switch has nothing to act on. Every event still carries X-Webhook-Delivery, so one dedupe rule covers all of them.

Turn on retries only if your endpoint is idempotent. Retries mean the same event can arrive more than once. Use the X-Webhook-Delivery header to recognise a repeat: it stays the same across every attempt for one event, so you can safely ignore an ID you’ve already handled.

Retries interact with the automatic switch-off after repeated failures (see Webhook Reliability) in the way you’d want: the failure counter counts a whole delivery, only after every retry has been used up — not each individual attempt.


Webhook Reliability

  • Your AI Connector sends webhooks over a secure connection (HTTPS). Make sure the web address you provide uses HTTPS.
  • If your system returns an error, the delivery is considered failed.
  • Monitor your receiving system’s uptime to avoid missing events.
  • For critical workflows, turn on Retrying Failed Deliveries, and consider a fallback mechanism as well.

Webhooks are switched off automatically after repeated failures. If your webhook URL fails repeatedly (about 5 errors in a row, or 3 in a row for configuration-type errors), Your AI Connector automatically stops sending events to that URL. To bring it back once your endpoint is healthy: edit the webhook and save it with a changed URL (any URL change re-enables it), or use the re-enable endpoint over the API — re-saving with the same URL is not enough. Support can also re-enable it for you.


Troubleshooting

Problem Solution
Webhook not firing First check the webhook isn’t switched off on its row. Then confirm the correct events are selected and your URL is reachable from the internet.
Test event works but real events do not Make sure the specific event type is enabled. If you expected a request when a tag is applied, note that subscribed_to_tags does not scope a webhook’s events to a tag — it only narrows which tags produce a conversation-summary notification. To get a request when a specific tag is applied, set a webhook URL on that tag in the Tags tab of the agent (or campaign) — see Contact Tags Updated Webhook.
Nothing arrives in n8n / Make / Zapier You’re probably using the platform’s Test URL, which only listens for a single event right after clicking “Listen for test event.” For live events, save the Production URL and switch the workflow to Active.
Receiving duplicate events Check for multiple webhooks pointing at the same URL. If Retry failed deliveries is on, a repeat is expected whenever your endpoint accepted an event but failed to answer in time — dedupe on X-Webhook-Delivery.
Signature check always fails Almost always because the body was re-serialized before checking. Verify against the raw request body, sign <timestamp>.<body>, and confirm you’re using the current secret if you recently rotated.
Retries not happening Retries are off unless enabled on that specific webhook. We don’t retry 4xx responses.
The campaign block is always null Expected if your account uses agents: contacts sit with an agent rather than a campaign. Read the agent block instead — see Webhook Data Format.
Data is empty or malformed Verify your receiving system accepts JSON. Check your server logs for parsing errors.
Webhook URL returns errors Test your URL with a tool like Postman or webhook.site.
Webhook stopped firing entirely after an outage Repeated failures automatically disable a webhook. Re-saving does not re-enable it — fix your endpoint, then contact support.
Save or Test gives a permission error You need the Integrations “edit” permission. Ask the account owner to grant it.
A webhook’s subscribed_to_tags list came back empty subscribed_to_tags does not scope a webhook’s events to a tag — it only narrows which tags produce a conversation-summary notification. Editing from the webhook form no longer clears that list (fixed July 21, 2026). If a webhook lost its list before that date, set subscribed_to_tags again via the Webhooks API — see Tag-Based Webhook Triggers.

Next Steps