Webhooks
Subscribe to real-time events and verify signed payloads in Node or Python.
Webhooks push events to your server the moment something happens in Coastly, so you do not have to poll. You register an HTTPS endpoint in the vendor dashboard, choose the events you care about, and receive a signed POST for each one.
Events
| Event | Fires when |
|---|---|
reservation.created |
A reservation is created. |
reservation.confirmed |
A reservation is confirmed. |
reservation.modified |
A reservation's dates, rooms, guests, or add-ons change. |
reservation.cancelled |
A reservation is cancelled. |
order.created |
A marketplace or F&B order is created. |
order.updated |
An order's status or contents change. |
invoice.issued |
An invoice is issued. |
invoice.paid |
An invoice is paid. |
Payload shape
Each delivery is a JSON body with a stable envelope. data holds the resource in the same shape the REST API returns.
{
"id": "evt_7Hn3k9",
"type": "reservation.created",
"created": 1751328000,
"data": {
"id": "res_9Fq2c8",
"property_id": "prop_1a2b3c",
"status": "confirmed",
"check_in": "2026-07-12",
"check_out": "2026-07-15",
"guests": 2
}
}
Respond with a 2xx status quickly (within a few seconds). Do the heavy work asynchronously. Non-2xx responses and timeouts are retried with backoff.
Verifying signatures
Every delivery carries an X-Coastly-Signature header so you can prove it came from Coastly and was not altered:
X-Coastly-Signature: t=1751328000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
tis the Unix timestamp when the signature was generated.v1is the HMAC-SHA256, hex-encoded, of the string${t}.${rawBody}using your endpoint's signing secret (whsec_…).
To verify, in order:
- Read the raw request body exactly as received. Do not re-serialize the parsed JSON; whitespace changes break the signature.
- Extract
tandv1from the header. - Compute
HMAC_SHA256(secret, "${t}.${rawBody}")and compare tov1using a constant-time comparison. - Reject the request if the timestamp is stale:
abs(now - t) > 300seconds. This blocks replay attacks.
Node
import crypto from "node:crypto";
// `rawBody` MUST be the raw string/Buffer, not the parsed object.
export function verifyCoastlyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.trim().split("="))
);
const t = Number(parts.t);
const v1 = parts.v1;
// Reject stale timestamps (replay protection).
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Wire it into an Express route using the raw body:
import express from "express";
const app = express();
app.post(
"/webhooks/coastly",
express.raw({ type: "application/json" }), // keep the raw body
(req, res) => {
const ok = verifyCoastlyWebhook(
req.body, // Buffer from express.raw
req.header("X-Coastly-Signature"),
process.env.COASTLY_WEBHOOK_SECRET
);
if (!ok) return res.status(400).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// handle event.type … then ack fast
res.status(200).send("ok");
}
);
Python
import hashlib
import hmac
import time
def verify_coastly_webhook(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.strip().split("=", 1) for kv in header.split(","))
t = int(parts.get("t", "0"))
v1 = parts.get("v1", "")
# Reject stale timestamps (replay protection).
if not t or abs(time.time() - t) > 300:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Flask handler:
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/coastly")
def coastly_webhook():
raw = request.get_data() # raw bytes, unparsed
sig = request.headers.get("X-Coastly-Signature", "")
if not verify_coastly_webhook(raw, sig, WEBHOOK_SECRET):
abort(400)
event = request.get_json()
# handle event["type"] … then ack fast
return "ok", 200
Idempotent processing
Deliveries may be retried, so the same evt_… id can arrive more than once. Record processed event ids and skip duplicates so your side stays consistent even under retries.