# Webhooks

Send PYLO's events to Zapier, Make, n8n or your own code. A recipe decides when, a webhook decides where.

A webhook is an address PYLO posts to when something happens: a call booked, moved or cancelled, someone leaving a booking page unfinished, a call outcome, a payment, a new lead, a form or a tag. Zapier, Make and n8n all give you such an address, and your own code can too.

A webhook on its own sends nothing. A recipe decides when it is used: its trigger, and conditions such as which booking link the call came through. So one webhook can take only the bookings from your VSL link, and another every cancellation.

## Who can open it

Owners and admins, under Settings, Integrations and automations.

## Setting one up with Zapier, Make or n8n

1. **Get an address.** In Zapier, start a Zap with **Webhooks by Zapier**, then **Catch Hook**, and copy the URL. In Make, add a **Custom webhook** and copy its address. In n8n, add a **Webhook** node set to POST and copy its production URL.
2. **Add it here.** Press **Add webhook**, name it after what it receives, and paste the address. You can leave the address out and press **Drop link** instead: whoever has the automation opens the link and pastes it there. The address is never shown again after it is saved, only its host and its last four characters.
3. **Copy the signing secret.** It is shown once. You need it only if you check signatures (below).
4. **Send a test.** Pick an event and press **Send a test**. The sample uses a made-up person (Test Lead, test@example.com) with your real booking link and question names, so your automation can map every field.
5. **Add a recipe.** Ask your agent for one, for example "send every booking on my VSL link to the VSL bookings webhook". It plans the recipe with that trigger, the condition and the step **send to webhook**, and it arrives in **Recipes** in shadow: **Show deliveries** then lists what it would have sent, and nothing goes out. Set it live in **Recipes** when the rows look right.

## Moving a Calendly zap onto PYLO booking

Copy the zap, change its trigger to **Catch Hook**, and add that address here. Ask your agent for a recipe on **A call is booked** (or cancelled) with the condition **event_type_key is** your link's key. Send a test, then remap each step:

- name to `data.invitee.full_name`, email to `data.invitee.email`, phone to `data.invitee.phone`
- each answer to `data.answers_by_key.<question key>`
- the date to `data.start_time`
- on cancellations, `data.cancellation.canceled_by` and `data.cancellation.reason`

`data.flat` carries the same names PYLO's confirmation page passes (`invitee_full_name`, `event_start_time`, `answer_1` and so on), so a step built for those maps one to one. Turn the new zap on, then the Calendly one off.

## What each post holds

Every post is JSON: `id` (the delivery id), `event`, `api_version`, `occurred_at`, `test`, `workspace` and `data`. Any field PYLO or the scheduler does not have is `null`, never missing, so a mapping never breaks on a booking that lacks it.

The events are `booking.created`, `booking.rescheduled`, `booking.cancelled`, `booking.partial`, `booking.partial_recovered`, `call.outcome`, `payment.succeeded`, `payment.failed`, `lead.created`, `form.submitted` and `tag.added`.

## Retries and the delivery log

A post counts as delivered when your address answers with a 2xx code within 10 seconds. Anything else is tried again after 1, 5, 15, 60, 180 and 720 minutes. After the seventh try PYLO gives up, posts one admin alert and opens an inbox task. If three deliveries in a row give up, PYLO pauses the webhook and says so; press **Resume** once the receiver works. Nothing old is sent unless you press **Send again** on it.

PYLO does not follow redirects: use the final address. Each post's payload is kept 14 days for **Send again**, then cleared.

## Checking it came from PYLO

Each post carries three headers:

- `X-Pylo-Event`: the event's name.
- `X-Pylo-Delivery`: the delivery id. It stays the same on every retry, so you can drop repeats.
- `X-Pylo-Signature`: `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`, keyed by the webhook's secret. For 24 hours after you rotate the secret it carries two `v1` values, so the old secret keeps working while you update it.

In Node, or a **Code by Zapier** step:

```js
const crypto = require("crypto");
const [t, ...sigs] = header.split(",");
const time = t.slice(2);
const want = crypto.createHmac("sha256", secret).update(`${time}.${rawBody}`).digest("hex");
const ok = sigs.some((s) => s === `v1=${want}`) && Math.abs(Date.now() / 1000 - Number(time)) < 300;
```

Make and n8n can do the same with their HMAC function.

## Your agent

Your agent can list webhooks, plan one, send a drop link, send a test and read the delivery log. It never asks for or sees an address or a secret.
