Events and payloads
Every request body has the same envelope: id, type, createdAt, workspaceId and data. This page describes data for each event.
Records inside data use the same shape the v1 API returns for them, so code that already reads GET /api/v1/contacts can read data.contact. Dates are ISO 8601 strings and money is a number.
| Event | Fires when | data |
|---|---|---|
contact.created | A contact is added: in the app, by import, the API, a HubSpot or Zoho sync, or MCP | contact |
contact.updated | A contact's fields are saved | contact, changedFields |
lead.created | A lead is added, including from forms, bookings and POST /api/v1/leads | lead |
deal.created | A deal is added | deal |
deal.stage_changed | A deal moves stage: an edit, a bulk move, or a drag on the pipeline board | deal, previousStage |
task.created | A task, call, email reminder or meeting is added | task |
booking.created | Someone books time through a booking link | booking |
booking.rescheduled | A booked meeting moves to a new time | booking, previousStartAt, previousEndAt |
booking.cancelled | A booked meeting is cancelled by the invitee or the host | booking |
chat.started | A website visitor sends the first message of a new chat | conversation, firstMessage |
email.received | An inbound email arrives in a connected mailbox | email |
form.submitted | Someone submits an embedded lead capture form | form, lead, leadCreated, pageUrl, fields |
webhook.test | You press Send test event, or call the test endpoint | message, endpointId |
contact.created and contact.updated
data.contact is the Contact object. contact.updated adds changedFields, the fields that were written in that save (a field written with its current value is still listed).
{
"id": "evt_0c6e2f1a9b8d7c5e4f3a2b1c",
"type": "contact.updated",
"createdAt": "2026-09-27T14:00:00.000Z",
"workspaceId": "cmq1wksp0alk34567890",
"data": {
"contact": {
"id": "cmq2cont0alk34567891",
"name": "Ann Lee",
"firstName": "Ann",
"lastName": "Lee",
"title": "CTO",
"email": "ann@example.com",
"phone": null,
"status": "NEW",
"lifecycleStage": null,
"source": null,
"category": null,
"buyerType": null,
"website": null,
"city": null,
"state": null,
"country": null,
"doNotContact": false,
"emailOptOut": false,
"emailStatus": null,
"engagementScore": null,
"lastContactedAt": null,
"lastActivityAt": null,
"externalId": null,
"company": { "name": "Acme", "externalId": null },
"customFields": null,
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-27T14:00:00.000Z"
},
"changedFields": ["title"]
}
}lead.created
data.lead is the Lead object.
{
"lead": {
"id": "cmq2lead0alk34567891",
"name": "Eve Park",
"title": null,
"company": "Northwind",
"email": "eve@example.com",
"phone": null,
"source": "Web form: Contact us",
"status": "NEW",
"estimatedInterest": "MEDIUM",
"owner": null,
"interestedOffering": null,
"customFields": null,
"createdAt": "2026-09-27T14:00:00.000Z",
"updatedAt": "2026-09-27T14:00:00.000Z"
}
}deal.created and deal.stage_changed
data.deal is the Deal object with two more fields: pipelineId and pipelineStage ({ id, name }), the stage as it appears on your pipeline board. stage is the fixed stage (NEW, QUALIFIED, PROPOSAL, NEGOTIATION, WON, LOST).
deal.stage_changed adds previousStage. When the move was a drag on the board it also adds previousStageId, because two board stages can share one fixed stage.
{
"deal": {
"id": "cmq2deal0alk34567891",
"name": "Northwind renewal",
"value": 12500,
"stage": "WON",
"expectedCloseDate": null,
"notes": null,
"externalId": null,
"contact": { "name": "Ann Lee", "email": "ann@example.com", "externalId": null },
"company": { "name": "Northwind", "externalId": null },
"offering": null,
"campaign": null,
"customFields": null,
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-27T14:00:00.000Z",
"pipelineId": "cmq2pipe0alk34567891",
"pipelineStage": { "id": "cmq2stag0alk34567891", "name": "Won" }
},
"previousStage": "NEGOTIATION",
"previousStageId": "cmq2stag0alk34567890"
}task.created
data.task is the Task object. Meetings booked through a booking link are tasks with type: "MEETING".
booking.created, booking.rescheduled, booking.cancelled
There is no v1 bookings endpoint, so data.booking has its own shape:
| Field | Type | Notes |
|---|---|---|
id | string | The booking ID |
status | string | CONFIRMED or CANCELLED |
startAt, endAt | string | The meeting's start and end |
hostTimezone, inviteeTimezone | string | IANA time zone names |
invitee | object | name, email, phone, notes |
guests | string[] | Extra attendee emails |
answers | array | { label, value } for each booking question, as asked |
meetingType | object | id, title, slug, durationMin |
host | object | id, name, email of the person booked |
joinUrl | string or null | The video link. Often null on booking.created, because the link is made a moment later when the calendar event is written |
contactId, leadId, taskId | string or null | The CRM records the booking is attached to |
cancelledBy, cancelReason, cancelledAt | Set on booking.cancelled. cancelledBy is invitee or host | |
createdAt | string | When it was booked |
booking.rescheduled adds previousStartAt and previousEndAt.
{
"booking": {
"id": "cmq2book0alk34567891",
"status": "CONFIRMED",
"startAt": "2026-09-28T09:30:00.000Z",
"endAt": "2026-09-28T10:00:00.000Z",
"hostTimezone": "America/Toronto",
"inviteeTimezone": "Europe/London",
"invitee": { "name": "Dana Ruiz", "email": "dana@example.com", "phone": null, "notes": null },
"guests": [],
"answers": [{ "label": "What would you like to cover?", "value": "Pricing" }],
"meetingType": { "id": "cmq2type0alk34567891", "title": "Intro call", "slug": "intro", "durationMin": 30 },
"host": { "id": "cmq2user0alk34567891", "name": "Qasim", "email": "qasim@example.com" },
"joinUrl": "https://meet.google.com/abc-defg-hij",
"contactId": null,
"leadId": "cmq2lead0alk34567891",
"taskId": "cmq2task0alk34567891",
"cancelledBy": null,
"cancelReason": null,
"cancelledAt": null,
"createdAt": "2026-09-27T14:00:00.000Z"
},
"previousStartAt": "2026-09-21T09:00:00.000Z",
"previousEndAt": "2026-09-21T09:30:00.000Z"
}chat.started
Fires on the visitor's first message, not when they open the chat panel (people open it and leave).
{
"conversation": {
"id": "cmq2chat0alk34567891",
"channel": "WEB",
"site": { "id": "cmq2site0alk34567891", "name": "Main site" },
"handler": "WAITING",
"visitor": { "name": null, "email": null, "phone": null, "locale": "en-GB", "timezone": "Europe/London" },
"pageUrl": "https://www.example.com/pricing",
"referrer": null,
"contactId": null,
"leadId": null,
"createdAt": "2026-09-27T13:59:40.000Z"
},
"firstMessage": { "id": "cmq2cmsg0alk34567891", "body": "Do you ship to Canada?", "createdAt": "2026-09-27T14:00:00.000Z" }
}email.received
Fires for new inbound mail in a connected mailbox. Newsletters and other bulk mail, bounce reports, and mail older than the mailbox connection (a first sync importing history) do not fire it. The body is not included; fetch it with GET /api/v1/emails/{id}.
{
"email": {
"id": "cmq2mail0alk34567891",
"status": "SENT",
"direction": "INBOUND",
"subject": "Re: Quote for 200 units",
"toEmail": "sales@example.com",
"fromEmail": "ann@example.com",
"ccEmails": [],
"contactId": "cmq2cont0alk34567891",
"leadId": null,
"campaignId": null,
"threadId": "18c2f0a1b2c3d4e5",
"emailAccountId": "cmq2acct0alk34567891",
"replyClass": null,
"sentAt": "2026-09-27T13:59:58.000Z",
"createdAt": "2026-09-27T14:00:00.000Z",
"reviewUrl": "https://agentlyleads.com/emails/cmq2mail0alk34567891"
}
}form.submitted
Fires for every accepted submission of an embedded form. Submissions caught by the spam checks do not fire it. When the email matches an existing lead, that lead is updated and leadCreated is false.
{
"form": { "id": "cmq2form0alk34567891", "name": "Contact us" },
"lead": { "id": "cmq2lead0alk34567891", "name": "Eve Park", "email": "eve@example.com", "...": "..." },
"leadCreated": true,
"pageUrl": "https://www.example.com/contact",
"fields": { "name": "Eve Park", "email": "eve@example.com", "message": "Do you offer volume pricing?" }
}fields holds the form's configured fields as submitted (trimmed, empty ones as null).
webhook.test
{
"message": "This is a test event from AgentlyLeads. If you can read it, your endpoint works.",
"endpointId": "cmq9hook0alk34567890"
}Webhooks overview
Get a signed HTTPS request from agentlyleads when a contact, lead, deal, task, booking, chat, email or form submission changes, instead of polling the API.
Verifying signatures
Check that an agentlyleads webhook request is genuine and recent using the X-AgentlyLeads-Signature header, with Node.js and Python examples.