Webhooks
What Easol sends to a webhook endpoint, how it signs requests and how it handles your response.
Easol sends each webhook as an HTTPS POST request to the endpoint URL you set
in your Easol admin. To set up a webhook and build an endpoint, follow
Receive webhooks. For the list of events, see
Webhook events.
Easol webhooks follow the Standard Webhooks specification, so you can check them with any Standard Webhooks library.
Request headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Easol-Webhooks/1.0 |
webhook-id | The ID of this delivery, such as msg_4f9c2a7b1e8d3c6a5b0f9e2d. It stays the same when Easol retries the delivery. |
webhook-timestamp | When Easol made this attempt, as a Unix timestamp: the number of seconds since 00:00:00 UTC on 1 January 1970. |
webhook-signature | v1, followed by the signature. See Signatures. |
Header names aren't case-sensitive.
Each webhook gets its own webhook-id. If two of your webhooks subscribe to the
same event, each receives a different ID for it.
Payload
The request body is a JSON object:
{
"type": "booking.confirmed",
"timestamp": "2026-10-21T09:30:00Z",
"data": {
"id": "bk_mR2nL5pW",
"object": "booking"
}
}| Field | Description |
|---|---|
type | The event type, such as booking.confirmed. |
timestamp | When the event happened, in ISO 8601 and UTC. Unlike the webhook-timestamp header, it stays the same when Easol retries a delivery. |
data.id | The ID of the booking the event is about. |
data.object | The type of object. Always booking. |
The payload doesn't include the booking's details. To get them, retrieve the
booking with GET /bookings/{id}.
The response describes the booking as it is when you ask, so it reflects
changes made after the event. The payload has the same shape for every API
version.
Booking events
Booking states
| State | Meaning |
|---|---|
pending | Started, but not yet confirmed. |
pending_payment | Confirmed, with a balance still owed. |
completed | Confirmed and paid in full, including free bookings and overpayments. |
cancelled | Cancelled. The booking may or may not have been confirmed before cancellation. |
“Confirmed” isn't a state. A booking is first confirmed when it moves from
pending to pending_payment or completed. Easol sets booked_at at that
point and sends booking.confirmed once. Cancelling a pending booking doesn't
confirm it.
A later payment or price reduction can move a booking from pending_payment
to completed, sending booking.fully_paid. An amendment that adds an unpaid
balance can move it back to pending_payment. Paying that balance sends
booking.fully_paid again, but not booking.confirmed.
Amended and updated
booking.amended means an
edit to what was booked or what it costs has been applied. This includes items,
packages, quantities, dates, reservations, prices, discounts and tax exemption.
Easol sends one event per applied edit, even when the saved values are unchanged.
Previews and edits that are rolled back don't send it.
booking.updated covers
changes to a confirmed booking's own fields: state, money totals, billing
address, purchase order number, VAT number and custom fields. It doesn't cover
every change to the guests or line items included in the API response.
An action can send both events. Use booking.amended to hear about booking
edits; use booking.updated to hear about changes to those booking fields.
Subscribe to both if you need both kinds of change.
The following examples assume the booking has already been confirmed. The
columns compare these two events only; an action can also send another event,
such as booking.fully_paid or booking.cancelled.
| Action | booking.amended | booking.updated |
|---|---|---|
| Apply an edit that changes a price, quantity, discount or tax exemption | Yes | Yes, even if the total ends up the same |
| Apply an edit that only changes a reservation, without changing price or state | Yes | No |
| Record a payment | No | Yes |
| Change booking custom fields, billing address, purchase order number or VAT number | No | Yes |
| Cancel the booking | No | Yes |
| Change only guest details | No | No |
| Issue a refund | No | No; refunds don't send booking.updated yet |
Before confirmation, Easol can send booking.amended, but it doesn't send
booking.updated. One action can send booking.updated more than once. This
is separate from delivery retries, which keep the same webhook-id.
Events can arrive out of order. Their payload identifies the booking; retrieve it through the API for its current state and details, rather than treating the event name as its current state.
Signatures
Easol signs every request with the webhook's signing secret, so you can check
that the request came from Easol and that nobody changed it. Each webhook has
its own secret. It starts with whsec_, followed by a base64-encoded key.
A Standard Webhooks library checks the signature for you. To check it yourself:
- Join the
webhook-idheader, thewebhook-timestampheader and the raw request body with full stops, in that order:{webhook-id}.{webhook-timestamp}.{body}. - Remove
whsec_from the start of the secret and base64-decode the rest to get the key. - Use your language's HMAC-SHA256 function to sign the joined string with the key, and base64-encode the result.
- Compare your result with the value after
v1,in thewebhook-signatureheader. Use a constant-time comparison function, such ascrypto.timingSafeEqualin Node.js orhmac.compare_digestin Python. An ordinary string comparison can let an attacker work out the signature from how long your server takes to respond. - Reject the request if
webhook-timestampis more than 5 minutes before or after your server's time. This stops someone from sending you an old request again.
Use the request body exactly as you received it. If you parse the JSON and convert it back to a string, the bytes can change and the signature won't match.
The webhook-signature header can hold several signatures, separated by
spaces. Easol sends one, but accept the request if any of them matches.
Responses
| Your response | What Easol does |
|---|---|
Any 2xx status | Records the delivery as successful. Easol ignores the response body. |
410 Gone | Turns off the webhook straight away and stops retrying. |
Any other status, including a 3xx redirect | Records a failed attempt and retries. Easol doesn't follow redirects. |
| No response within the time limits, or a failed connection | Records a failed attempt and retries. |
Easol allows 5 seconds to look up your host and connect, and 30 seconds for the whole request.
Easol keeps the first 10 KB of each response body to show in the delivery log.
Retries
Easol makes up to 10 attempts for each event over roughly three days. After successive failures, the delays are 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours and 24 hours. Each delay includes up to 10% extra random time to spread out retries.
Internal Easol errors appear in the delivery history but do not consume this retry limit or count towards disabling your webhook. Easol retries those errors with backoff, so they can extend the delivery period.
Due retries are picked up once a minute, so the first retry usually arrives within about a minute. Queue load can delay any attempt further.
To ask Easol to wait longer, return a Retry-After header containing a number
of seconds. Easol uses the longer of that value and its scheduled delay,
capping the requested wait at 24 hours. Invalid values and HTTP dates are
ignored.
Pending deliveries survive a lost background job or a restart. A delivery interrupted before its result is saved becomes eligible for retry after 10 minutes.
Each retry has the same webhook-id and body as the first attempt, with a new
webhook-timestamp and signature.
If every attempt fails, Easol stops trying to deliver that event. You can't resend it from Easol. To find what you missed, use the API, as described in Receive webhooks.
Before each attempt, Easol checks that the webhook is still turned on and still listens for the event type. If it isn't, Easol skips the attempt and stops trying to deliver that event.
Order and duplicates
Easol doesn't guarantee the order of events. A retry can arrive after a later event, and two events about the same booking can arrive at the same time.
Your endpoint can receive the same delivery more than once. Store the
webhook-id of each delivery you handle, and skip any delivery whose ID you've
already seen.
Automatic turn-off
Easol turns a webhook off in either of these cases:
- Your endpoint responds with
410 Gone. - Every attempt since the last successful delivery has failed, there have been at least 10 failed attempts, and the first of them was more than 3 days ago.
A webhook that Easol has turned off shows as Disabled in your Easol admin. Easol doesn't email you when this happens. Easol doesn't send events to a turned-off webhook, and doesn't send them later when you turn it back on.
Turning a webhook back on resets its count of failed attempts.
Endpoint URL rules
Easol only sends webhooks to endpoints on the public internet. An endpoint URL must:
- start with
https:// - use a public domain name, not an IP address or a local name such as
localhostor a.internal,.localor.testdomain - not include a username or password
- not point to an Easol domain, including a site address on an Easol domain
- resolve only to public IP addresses
Each webhook in your company must use a different URL.
Easol checks the domain name again before each delivery. If the name no longer resolves, or resolves to an address that isn't public, the attempt fails and counts towards turning the webhook off.
Delivery log
Easol records every delivery attempt: the request body, your response status and body, and how long your endpoint took. You can see these on the webhook's Event deliveries tab for 3 months. Pending events and their attempts are kept until delivery finishes or retries are exhausted. Failures before Easol prepares a request are also recorded, with an error and no request body.
Idempotency
Safely retry write requests without creating duplicates.
Accommodations
An accommodation is a place to stay — a hotel, villa, hostel, and so on. Each accommodation has one or more variants (the bookable room types, e.g. Deluxe Double) and carries its own imagery, location, and published state. Create accommodations here, then add variants under them. Variants carry occupancy, pricing, and stock, and inherit their parent accommodation's published state — use `hidden` to withhold a single variant from the storefront without unpublishing the whole accommodation.