Webhooks
Webhooks push events to your server so you don't have to poll.
Register an endpoint
Requires scope webhooks:manage. Up to 10 endpoints per client.
curl -X POST https://searchurtrip.com/api/v1/webhooks \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"url":"https://partner.example.com/hooks/waggish","events":["booking.created","booking.cancelled"]}'
{
"data": {
"id": "whk_4",
"url": "https://partner.example.com/hooks/waggish",
"events": ["booking.created", "booking.cancelled"],
"status": "active",
"createdAt": "2026-09-26 02:20:38",
"secret": "whsec_…"
}
}
The secret is returned only once. Store it to verify signatures. An empty events array subscribes to everything. In production the URL must be public https; private, loopback and link-local hosts are rejected.
Events
| Event | Sent when |
|---|---|
booking.created |
A booking is created through the API (live keys only) |
booking.cancelled |
A booking is cancelled through the API |
invoice.paid |
An invoice for your tenant becomes fully paid |
Delivery format
POST to your URL with a JSON body:
{
"id": "evt_9c1e0d5b7a3f2c4e6a",
"type": "booking.created",
"created": "2026-09-26T02:20:38.104Z",
"livemode": true,
"data": { "id": "bkg_42", "pnr": "J3P74T", "type": "flight", "status": "Upcoming", "amount": 27143, "currency": "INR", "reference": "PO-1001" }
}
Headers:
| Header | Value |
|---|---|
X-Waggish-Event |
Event type |
X-Waggish-Delivery |
Delivery id (use with id to de-duplicate) |
X-Waggish-Signature |
t=<unix seconds>,v1=<hex hmac> |
User-Agent |
Waggish-Webhooks/1.0 |
Respond with any 2xx within 5 seconds to acknowledge. Do the real work asynchronously.
Verifying the signature
Compute HMAC_SHA256(secret, "<t>.<raw body>") and compare it (constant-time) to v1. Reject if t is more than 5 minutes old.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/waggish", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Waggish-Signature") ?? "";
const [, t, v1] = /t=(\d+),v1=([a-f0-9]+)/.exec(header) ?? [];
const expected = crypto
.createHmac("sha256", process.env.WAGGISH_WEBHOOK_SECRET)
.update(`${t}.${req.body.toString("utf8")}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
const valid = v1 && v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
// …handle event, de-duplicating on event.id
res.sendStatus(200);
});
Python (Flask)
import hashlib, hmac, os, re, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/waggish")
def hook():
m = re.match(r"t=(\d+),v1=([a-f0-9]+)", request.headers.get("X-Waggish-Signature", ""))
if not m: abort(400)
t, v1 = m.groups()
expected = hmac.new(os.environ["WAGGISH_WEBHOOK_SECRET"].encode(),
f"{t}.{request.get_data(as_text=True)}".encode(), hashlib.sha256).hexdigest()
if abs(time.time() - int(t)) > 300 or not hmac.compare_digest(v1, expected): abort(400)
event = request.get_json()
return "", 200
Always verify against the raw body, before any JSON parsing or re-serialisation.
Retries
A non-2xx response, a timeout, or a connection error is retried with increasing delays: 1 min, 5 min, 30 min, 2 h, 12 h. After the final attempt the delivery is marked failed. Deliveries are at-least-once: de-duplicate on the event id. Delivery attempts and their status are visible in your console's API Access screen.
Managing endpoints
curl https://searchurtrip.com/api/v1/webhooks -H "Authorization: Bearer $KEY" # list (secrets are never returned)
curl -X DELETE https://searchurtrip.com/api/v1/webhooks/whk_4 -H "Authorization: Bearer $KEY"