Customer events
Keep your own database in step without asking us every minute. A worked receiver, and the two things that will bite you.
Sarah's accountant uses different software, and it needs to know when a customer's details change. Rather than exporting weekly, she has us tell it.
The events
| Event | When |
|---|---|
clients.client.created | Someone's added — one at a time or by import |
clients.client.updated | Any field changes, including a restore |
clients.client.deleted | Someone's deleted |
clients.note.added | A note is added — useful if your crew app shows access instructions |
data.object is the whole customer, so a create or update carries everything you need
without calling back.
Check the live list, not this table
The endpoint dialog under Webhooks shows what's actually available, and it grows without an API version bump. Trust it over a table in docs.
Note there's no separate "restored" event. A restore arrives as clients.client.updated,
which means your receiver should treat every update as "re-check this" rather than assuming
its own copy is still structurally sound.
Subscribe
Webhooks → New endpoint, then tick the three clients.* events.

With no site filter, you hear about all of them — each event's location_id says whether
it was Portland or Salem.
Keep the whsec_ secret you're given; the code below needs it.
A receiver worth copying
Webhooks arrive server to server, and checking one needs the signing secret — so a browser can never receive them directly.
To live-update a screen: your server receives and verifies, then tells the browser over a WebSocket or server-sent events, or simply invalidates a cache so the next render refetches.
// React to your own server, not to ours.
source.addEventListener('customer.changed', (event) => {
const { id } = JSON.parse(event.data);
queryClient.invalidateQueries({ queryKey: ['customers', id] });
});The two things that will bite you
Snapshots go stale. data.object is the customer as they were when the event fired. If
retries land out of order, the newest event isn't necessarily the newest state. Guard with
the timestamp you already store:
if (existing && existing.updatedAt >= customer.updated_at) return;Fields can arrive starred out. Webhook payloads are built the same way reads are, so if
the credential context lacks clients.view_email you'll receive dan***** rather than a real
address. If your copy needs real contact details, make sure the app's keys carry the view_*
scopes — otherwise you'll quietly build a database of masked emails and only notice when
something tries to send one.
Imports produce a lot of events
Sarah's 312-row import fires 312 clients.client.created events.
That's correct, and worth designing for. Batch your writes, and don't make a third-party call per event. If you're importing and mirroring the same data, consider switching the endpoint off for the duration and reconciling afterwards with an export — much less work than processing three hundred deliveries.