# Webhook Payloads

> Reference for webhook event payload shapes delivered to your server.

Reference for the exact payload delivered for each webhook event. All payloads follow the same envelope:

```json
{
  "event": "event.name",
  "timestamp": "2026-03-27T14:30:00.000Z",
  "data": { ... }
}
```

**Common fields in `data`:**

- `subAccountId` - Always present. Identifies which sub-account the event belongs to.
- `locationId` - A stable public identifier for the sub-account, included alongside `subAccountId`. Prefer this when keying records in external systems.
- `contactId` - Present when the event is associated with a specific contact.
- `tags` - Present when the associated contact has tags. Array of tag strings.

---

## Delivery headers and verification

Every delivery carries these headers:

| Header | What it is |
|---|---|
| `X-Webhook-Signature` | `sha256=<hex>`. HMAC-SHA256 of the raw request body, keyed with your subscription's signing secret. |
| `X-Webhook-Signature-V2` | `sha256=<hex>`. HMAC-SHA256 of `<timestamp>.<raw body>`, same secret. Optional, replay-protected. |
| `X-Webhook-Timestamp` | Delivery time in epoch milliseconds. This is the value signed by the V2 signature. |
| `X-Webhook-Event` | The event name, matching the `event` field in the body. |
| `X-Webhook-Delivery` | Unique id for this delivery attempt. |
| `X-Webhook-Event-Id` | Stable id for the underlying event. Use it to make your handler idempotent, since a retry repeats the same event id. |
| `User-Agent` | Carries the sub-account's display brand, so the receiving system sees your brand rather than the platform name. |

### Which signature to verify

Verify against the raw request body exactly as received, before any JSON parsing or re-serialization, or the bytes will not match.

**`X-Webhook-Signature` (original).** Signs the body alone. It proves the payload came from you and has not been altered. It cannot tell you *when* it was sent, so a captured request stays valid forever and can be replayed at any time.

**`X-Webhook-Signature-V2` (recommended for new integrations).** Signs `<timestamp>.<body>`, following the same scheme Stripe uses. Because the timestamp is part of the signed string, your handler can trust `X-Webhook-Timestamp` and reject anything outside a freshness window you choose, which shuts the replay window. Verifying the original signature leaves that timestamp unsigned, so an attacker replaying a captured delivery can rewrite it freely.

Both headers are sent on every delivery, signed with the same secret. Nothing breaks if you keep verifying the original one; move to V2 when it suits you.

```js
import crypto from "node:crypto";

// `rawBody` must be the exact bytes received, not a re-serialized object.
function verify(rawBody, headers, secret, toleranceMs = 5 * 60 * 1000) {
  const timestamp = headers["x-webhook-timestamp"];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const received = (headers["x-webhook-signature-v2"] || "").replace("sha256=", "");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");

  // Constant-time compare, then reject anything too old to be a live delivery.
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
  return Math.abs(Date.now() - Number(timestamp)) < toleranceMs;
}
```

Your signing secret is generated per subscription when you create the webhook, under Settings, Integrations, Webhooks.

---

## contact.created

Fires when a new contact is created.

```json
{
  "event": "contact.created",
  "timestamp": "2026-03-27T14:30:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "company": "Acme Corp",
    "source": "api",
    "tags": ["website-lead"],
    "lifecycleStage": "lead"
  }
}
```

---

## contact.updated

Fires when a contact record is updated.

```json
{
  "event": "contact.updated",
  "timestamp": "2026-03-27T14:31:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "company": "Acme Corp",
    "lifecycleStage": "qualified"
  }
}
```

---

## contact.lifecycle\_changed

Fires when a contact's lifecycle stage changes.

```json
{
  "event": "contact.lifecycle_changed",
  "timestamp": "2026-03-27T14:32:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "newStage": "customer",
    "previousStage": "qualified"
  }
}
```

---

## contact.tag\_added

Fires when a tag is added to a contact.

```json
{
  "event": "contact.tag_added",
  "timestamp": "2026-03-27T14:33:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "tagName": "vip"
  }
}
```

---

## message.received

Fires when an inbound message is received on any channel.

```json
{
  "event": "message.received",
  "timestamp": "2026-03-27T14:34:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "messageId": "msg456",
    "conversationId": "conv789",
    "channel": "sms",
    "bodyText": "Hi, I'd like to schedule an appointment",
    "fromAddress": "+15559876543",
    "direction": "inbound",
    "tags": ["website-lead", "vip"]
  }
}
```

---

## message.sent

Fires when an outbound message is confirmed sent by the provider.

```json
{
  "event": "message.sent",
  "timestamp": "2026-03-27T14:35:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "messageId": "msg457",
    "conversationId": "conv789",
    "channel": "sms",
    "status": "sent",
    "externalId": "SM1234567890abcdef"
  }
}
```

---

## message.delivered

Fires when an outbound message is confirmed delivered to the recipient.

```json
{
  "event": "message.delivered",
  "timestamp": "2026-03-27T14:36:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "messageId": "msg457",
    "conversationId": "conv789",
    "channel": "sms",
    "status": "delivered",
    "externalId": "SM1234567890abcdef"
  }
}
```

---

## message.failed

Fires when an outbound message fails to deliver or bounces.

```json
{
  "event": "message.failed",
  "timestamp": "2026-03-27T14:37:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "messageId": "msg458",
    "conversationId": "conv790",
    "channel": "email",
    "status": "bounced",
    "externalId": "postmark-msg-id-123"
  }
}
```

---

## appointment.booked

Fires when a new appointment is booked.

```json
{
  "event": "appointment.booked",
  "timestamp": "2026-03-27T14:38:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "calendarId": "cal001",
    "appointmentTypeId": "type001",
    "appointmentTitle": "30-Min Consultation",
    "startTime": 1743508800000,
    "endTime": 1743510600000,
    "status": "scheduled"
  }
}
```

---

## appointment.cancelled

Fires when an appointment is cancelled.

```json
{
  "event": "appointment.cancelled",
  "timestamp": "2026-03-27T14:45:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "status": "cancelled"
  }
}
```

---

## appointment.rescheduled

Fires when an appointment is moved to a new time.

```json
{
  "event": "appointment.rescheduled",
  "timestamp": "2026-03-27T14:46:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "startTime": 1743595200000,
    "endTime": 1743597000000,
    "status": "scheduled"
  }
}
```

---

## appointment.confirmed

Fires when an appointment is confirmed.

```json
{
  "event": "appointment.confirmed",
  "timestamp": "2026-03-27T14:47:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "status": "confirmed"
  }
}
```

---

## appointment.completed

Fires when an appointment is marked completed.

```json
{
  "event": "appointment.completed",
  "timestamp": "2026-03-27T14:48:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "status": "completed"
  }
}
```

---

## appointment.no_show

Fires when an appointment is marked as a no-show.

```json
{
  "event": "appointment.no_show",
  "timestamp": "2026-03-27T14:49:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "appointmentId": "appt001",
    "status": "no_show"
  }
}
```

---

## opportunity.created

Fires when a new opportunity is created in a pipeline.

```json
{
  "event": "opportunity.created",
  "timestamp": "2026-03-27T14:39:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "stageId": "stage001",
    "title": "Website Redesign",
    "value": 5000
  }
}
```

---

## opportunity.stage\_changed

Fires when an opportunity moves to a different pipeline stage.

```json
{
  "event": "opportunity.stage_changed",
  "timestamp": "2026-03-27T14:40:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "fromStageId": "stage001",
    "toStageId": "stage002",
    "title": "Website Redesign"
  }
}
```

---

## opportunity.won

Fires when an opportunity is marked as won.

```json
{
  "event": "opportunity.won",
  "timestamp": "2026-03-27T14:41:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "title": "Website Redesign",
    "value": 5000
  }
}
```

---

## opportunity.lost

Fires when an opportunity is marked as lost.

```json
{
  "event": "opportunity.lost",
  "timestamp": "2026-03-27T14:42:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "title": "Website Redesign",
    "value": 5000,
    "lostReason": "Budget constraints"
  }
}
```

---

## opportunity.updated

Fires when an opportunity record is updated.

```json
{
  "event": "opportunity.updated",
  "timestamp": "2026-03-27T14:50:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "name": "Website Redesign",
    "value": 6500,
    "changedFields": ["value"]
  }
}
```

The `changedFields` array lists the names of the fields that changed in this update.

---

## opportunity.deleted

Fires when an opportunity is deleted.

```json
{
  "event": "opportunity.deleted",
  "timestamp": "2026-03-27T14:51:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "opportunityId": "opp001",
    "pipelineId": "pipe001",
    "name": "Website Redesign"
  }
}
```

---

## form.submitted

Fires when a public form is submitted.

```json
{
  "event": "form.submitted",
  "timestamp": "2026-03-27T14:43:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "formId": "form001",
    "submissionId": "sub001",
    "formName": "Contact Us",
    "fields": {
      "name": "Jane Doe",
      "email": "jane@example.com",
      "message": "I'd like more info about your services"
    }
  }
}
```

---

## invoice.paid

Fires when an invoice payment is received.

```json
{
  "event": "invoice.paid",
  "timestamp": "2026-03-27T14:44:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "invoiceId": "inv001",
    "amount": 1500,
    "currency": "usd",
    "paidAt": "2026-03-27T14:44:00.000Z"
  }
}
```

---

## task.created

Fires when a new task is created.

```json
{
  "event": "task.created",
  "timestamp": "2026-03-27T14:52:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "taskId": "task001",
    "title": "Follow up with Jane",
    "status": "todo",
    "priority": "high",
    "dueDate": 1743595200000,
    "assignedTo": "user001"
  }
}
```

---

## task.updated

Fires when a task record is updated.

```json
{
  "event": "task.updated",
  "timestamp": "2026-03-27T14:53:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "taskId": "task001",
    "title": "Follow up with Jane",
    "status": "in_review",
    "priority": "high",
    "changedFields": ["status"]
  }
}
```

The `changedFields` array lists the names of the fields that changed in this update.

---

## task.completed

Fires when a task is marked completed.

```json
{
  "event": "task.completed",
  "timestamp": "2026-03-27T14:54:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "taskId": "task001",
    "title": "Follow up with Jane",
    "status": "completed",
    "priority": "high",
    "completedAt": 1743596400000
  }
}
```

---

## task.deleted

Fires when a task is deleted.

```json
{
  "event": "task.deleted",
  "timestamp": "2026-03-27T14:55:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "taskId": "task001",
    "title": "Follow up with Jane",
    "status": "todo",
    "priority": "high"
  }
}
```

---

_Payload shapes may vary slightly depending on the data available at the time of the event. Fields shown as present in examples may be `null` if the corresponding data was not set. The `contactId` field is included when the event is associated with a specific contact._

---
Source: https://seedlycrm.com/docs/help/webhooks
