Webhooks
A webhook sends an event from Booked55 to another system the moment something happens, so a tool such as Zapier, Make or n8n can act on it. Booked55 sends its own payload format; the receiving tool maps it onto whatever it needs.
Who can use webhooks
Owners and managers. Webhooks also have to be turned on for your organization: until they are, the Webhooks page doesn't appear in the sidebar. Booked55 support turns the feature on. If it is turned off later, new events stop being sent too.
Creating an endpoint
- 1
Open Webhooks
In the sidebar, go to Workspace → Workspace Settings → Advanced → Webhooks.
- 2
Click "Add endpoint"
The Add endpoint window opens.
- 3
Enter the Endpoint URL
The address that should receive events, for example a Zapier "Catch Hook" URL. It must start with https://. Addresses that point at private or internal networks are refused.
- 4
Add a Label
Say what the endpoint is for, such as "Zapier → JobAdder". It is the easiest way to tell endpoints apart later.
- 5
Choose the Events
Tick the events this endpoint should receive. Today that is deal.created.
- 6
Click "Create endpoint" and copy the signing secret
The Signing secret window shows the secret once. Copy it into the receiving system before you close the window. It can’t be shown again, only regenerated.
Events
| Event | When it fires |
|---|---|
deal.created | A deal is created: by hand, when a pipeline card enters a stage that marks the contact as a client, from a CSV import, or through the API. |
Booked55 starts recording events for an organization once an endpoint subscribes to them. Deals created before that are not sent.
What a delivery contains
Each delivery is an HTTPS POST with a JSON body. The deal, its contact and its company are included in full, so the receiving tool doesn't need to look anything up. A field with no value is sent as null rather than left out. A shortened example:
{
"id": "evt_a1b2c3...",
"type": "deal.created",
"created_at": "2026-08-13T14:02:11.402Z",
"account_id": "6a7e...",
"actor": { "type": "user", "id": "6a7e...", "name": "Sam Lee" },
"test": false,
"data": {
"deal": { "id": "...", "value": 12500, "description": "...", "client_type": { ... }, "assigned_to_user": { ... } },
"contact": { "id": "...", "name": "...", "email": "...", "phone_numbers": [ ... ] },
"company": { "id": "...", "name": "...", "domain": "acme.com", "website": "..." }
}
}id is unique per event: use it to ignore a delivery you have already handled. test is true for test events, so they never create real records on your side.
Every request also carries these headers:
| Header | Value |
|---|---|
Booked55-Signature | t=<timestamp>,v1=<signature>, see below |
Booked55-Timestamp | When the request was signed, in Unix seconds |
Booked55-Event-Id | The event's id |
Booked55-Event-Type | For example deal.created |
Booked55-Delivery-Id | This delivery |
Booked55-Attempt | The attempt number, starting at 1 |
Verifying the signature
Check the signature before trusting a request, so nobody else can send fake events to your endpoint. Booked55 signs each request with the endpoint's signing secret (it starts with whsec_):
- Read
tandv1from theBooked55-Signatureheader. - Refuse the request if
tis more than 5 minutes away from your current time. - Compute HMAC-SHA256 of
<t>.<raw request body>, using the whole secret,whsec_included, as the key. Write the result as lowercase hex. - Compare it with
v1using a constant-time comparison. Accept the request only if they match.
Node.js:
const crypto = require('crypto')
// rawBody must be the exact bytes Booked55 sent, before any JSON parsing.
function isFromBooked55(secret, rawBody, signatureHeader, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((piece) => piece.trim().split('='))
)
const timestamp = Number(parts.t)
if (!timestamp || !parts.v1) return false
// Refuse old deliveries so a captured request can't be replayed later.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const expectedBytes = Buffer.from(expected, 'hex')
const receivedBytes = Buffer.from(parts.v1, 'hex')
return expectedBytes.length === receivedBytes.length
&& crypto.timingSafeEqual(expectedBytes, receivedBytes)
}Python:
import hashlib, hmac, time
def is_from_booked55(secret: str, raw_body: bytes, signature_header: str, tolerance_seconds: int = 300) -> bool:
parts = dict(piece.strip().split("=", 1) for piece in signature_header.split(","))
timestamp = int(parts.get("t", "0"))
received = parts.get("v1", "")
if not timestamp or not received:
return False
if abs(time.time() - timestamp) > tolerance_seconds:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)Deliveries, retries and redelivery
Click an endpoint to open its page. It shows the endpoint's status, its events, a preview of the secret, the last successful delivery, and Delivery history, which you can filter by outcome (Delivered, Failed, Retrying, Pending, Skipped) and by event.
- Delivered — Your endpoint answered with a 2xx status.
- Retrying — Your endpoint didn't answer within 10 seconds, couldn't be reached, or answered with a status that is worth retrying: 5xx, 408, 429, or a redirect (redirects are never followed). Booked55 tries again with growing gaps (about 1 minute, then 2, 4, 8 and so on, up to every 4 hours), at most 14 attempts within 24 hours.
- Failed — Your endpoint answered with a 4xx status other than 408 or 429, or the retries ran out. A 4xx is not retried, because sending the same request again won't change the answer.
- Skipped — The endpoint was disabled when the event happened.
Click a delivery to see the full request, headers and your endpoint's response. Click Redeliver there to send the same event again. Redeliver adds a new row, marked Manual, and keeps the original. It needs the endpoint to be active, and it sends to the endpoint's current URL.
Request and response bodies are kept for 7 days, delivery rows for 30 days, and events for 90 days. After 90 days an event can no longer be redelivered.
Send test event
On the endpoint page, Send test event sends sample data (not a real deal) with "test": true. It is tried once, with no retries, and the result appears straight away. Test events don't count toward auto-disable.
Auto-disable
After 10 deliveries in a row fail, Booked55 disables the endpoint, shows it as Auto-disabled with the reason, and sends the organization's owners a notification in Booked55. Fix the receiving side, then click Edit, tick Active and save. Re-enabling also clears the failure count. Events that happen while an endpoint is disabled are recorded as Skipped.
Editing, secrets and deleting
- Edit — Change the URL, Label, Events or Active. Changing the URL sends every future event to the new address.
- Regenerate secret — On the endpoint page. The new secret is shown once, and every request after that is signed with it, so your receiver rejects them until you update it there.
- Delete — The trash icon on the Webhooks page. Nothing more is delivered to that URL.
Webhooks and the AI assistant
The AI assistant can list endpoints and deliveries, and it can create, edit, test, redeliver and delete endpoints. Each of those actions waits on a confirmation card, and nothing happens until you click Confirm. When you create an endpoint or change its URL, the card leads with the destination address and the warning “Booked55 will send each new deal's contact and company details to this address.” After you confirm a new endpoint, the card shows the signing secret once. The assistant itself never sees the secret, and it can't regenerate one; use Regenerate secret on the endpoint page.
Ready to get started?
Create your free Booked55 account and start managing your sales pipeline in minutes.