# Events and what each one carries

> Every event an endpoint can subscribe to, when each one is sent, and the payload a delivery of it carries.

An endpoint subscribes to the events it wants, by name, on the project's
webhooks screen. Each delivery's signed body names the event in `event` and
carries the payload below in `data`, beside the delivery's own `id`: [Verify a
delivery](/docs/webhooks/) shows the whole body. Every id is a public
id, the same one the API answers with, so a payload can be followed up with a
call.

## The events

| `project` | |
|---|---|
| `project.created` | Sent as a project is created, which is before it can have an endpoint, so no endpoint receives it today. |
| `project.updated` | The project's name, timezone or currency changed. |
| `project.deleted` | Sent as a project is deleted, once its endpoints are already gone, so no endpoint receives it today. |

| `module` | |
|---|---|
| `module.enabled` | A part was switched on for the project. |
| `module.disabled` | A part was switched off for the project. |

| `page` | |
|---|---|
| `page.published` | A page went live, or a new version of it did. |
| `page.unpublished` | A page was taken off the website, and kept as a draft. |
| `page.deleted` | A page was deleted. |

| `booking` | |
|---|---|
| `booking.created` | A booking was made. It may still be waiting for payment, or for you. |
| `booking.confirmed` | A booking was confirmed: paid for, accepted by hand, or confirmed on its own. |
| `booking.cancelled` | A booking was cancelled, by the customer or by the business. |
| `booking.rescheduled` | A booking moved to another time. The payload says where it was. |
| `booking.expired` | A booking waiting for payment ran out of time, and its slot was given back. |
| `booking.completed` | An appointment took place: sent once the business's day it was on is over, unless it was marked as a no-show first. |

| `payment` | |
|---|---|
| `payment.succeeded` | Money arrived. |
| `payment.failed` | A payment was declined or abandoned. |
| `payment.refunded` | Part or all of a payment was given back. |
| `payment.expired` | A payment page ran out without being paid. |

| `message` | |
|---|---|
| `message.received` | A message from the website's contact form arrived in the inbox, or was moved there out of spam. Spam is never announced. |


## What a payload carries

Every event about one subject carries the same fields in `data`, so one
handler reads them all. The values below are examples. Every time is an instant
in UTC: show it on the project's timezone.

### Bookings

```json title=booking.*
{
  "booking_id": "8f0c2b41-5d3e-4a7b-9c11-2e6f8a9d0b7c",
  "livemode": true,
  "service_id": "6f1c2a9e-4b7d-4e0a-9c55-2d8e1f0b7a31",
  "staff_id": "b3e04c1d-9a6f-4f2e-8d1b-5c7a0e9f3d24",
  "customer_id": "0d9e7f61-2c4b-4a8e-b5f3-71a2c6d8e9f0",
  "starts_at": "2026-10-06T07:00:00+00:00",
  "ends_at": "2026-10-06T07:45:00+00:00",
  "status": "confirmed",
  "amount_due": 2000,
  "currency": "CHF"
}
```

`amount_due` is in minor units: 2000 is CHF 20.00. `livemode` is false for a
booking a test key made, and its events reach only your test endpoints. A few
events add one or two fields of their own: `booking.confirmed` names who
confirmed it in `confirmed_by` when a person did, `booking.cancelled` says who
in `cancelled_by`, and `booking.rescheduled` adds `previous_starts_at` and
`moved_by`.

### Payments

```json title=payment.*
{
  "payment_id": "3c7d9e2a-1f4b-4c6d-8e0a-5b9f2d7c4e18",
  "livemode": true,
  "subject_type": "bookings.booking",
  "subject_id": "8f0c2b41-5d3e-4a7b-9c11-2e6f8a9d0b7c",
  "status": "paid",
  "amount": 2000,
  "currency": "CHF",
  "is_final": false,
  "metadata": {
    "booking_id": "8f0c2b41-5d3e-4a7b-9c11-2e6f8a9d0b7c"
  }
}
```

`subject_type` and `subject_id` say what the money was for, here a booking. A
payment opened on its own, with `POST /v1/checkouts`, was for nothing but itself:
its `subject_type` is empty and its `subject_id` is null. `metadata` is the
references the checkout was given, the same ones the API returns on the payment,
so a handler can find its order without a second call.
`livemode` is false for a payment a test key asked for, which took no real money.
`is_final` is true once nothing more can happen to the payment, so a handler
can stop expecting more events about it.

### Pages

```json title=page.*
{
  "event": "page.published",
  "project": "atelier-rosa",
  "page": {
    "id": "5a2e8c71-9d4f-4b3a-a6e0-1c7b9f2d8e45",
    "slug": "home",
    "locale": "en",
    "title": "Home",
    "status": "published",
    "url_path": "/home"
  },
  "cache_version": 42,
  "version": {
    "number": 7,
    "published_at": "2026-10-06T08:15:00+00:00"
  }
}
```

`cache_version` rises on every publish, so a static site can rebuild when it
moves and not otherwise. `version` is only on `page.published`.

### Messages

```json title=message.*
{
  "message_id": "c41f8a62-7b3d-4e9a-8f15-2d6b0e9c3a77",
  "livemode": true,
  "received_at": "2026-10-06T07:12:00+00:00"
}
```

Which message, and nothing anybody wrote in it: read the name, the address and
the words with `GET /v1/messages/{id}` and a secret key. Spam is never
announced, and a message moved out of spam is announced then, once.

### The project

```json title=project.*
{
  "project_id": "7e4b1d93-6a2c-4f8e-9b05-3d1a7c9e2f64",
  "slug": "atelier-rosa",
  "name": "Atelier Rosa",
  "organization_id": "2b8f6e1a-4d9c-4e7b-a3f0-9c5d1e7b2a86",
  "timezone": "Europe/Zurich",
  "currency": "CHF"
}
```

```json title=module.*
{
  "project_id": "7e4b1d93-6a2c-4f8e-9b05-3d1a7c9e2f64",
  "module": "bookings"
}
```

## Verifying a delivery

A payload is only worth acting on once its signature checks out. [Verify a
delivery](/docs/webhooks/) shows how, with the SDK or without it.
