Webhooks
Last updated October 12, 2026
A business using Tendroom can send what happens in its portal (a new client, a booking, a paid invoice) to an address of its own, as it happens. This page is for whoever builds that address. The business adds it in its portal under Settings → Connected apps, where it also finds the signing secret and a log of every delivery.
What Tendroom sends
Each event is one POST to the address, with a JSON body and these headers. They follow Standard Webhooks, so its libraries can check them.
webhook-id: the event's id. It's the same on every try of the same event.webhook-timestamp: when this try was sent, in seconds since 1970 (UTC).webhook-signature:v1,and the signature (below).content-type: application/jsonanduser-agent: Tendroom-Webhooks/1.
{
"id": "evt_01JAB3XK9Q6T8VZ5M2N4P7R0WD",
"type": "invoice.paid",
"occurredAt": "2026-10-17T15:04:05.000Z",
"business": { "id": "tnt_01J9…", "name": "Golden Hour Studio" },
"data": {
"id": "inv_01JA…",
"number": "GH-0042",
"status": "paid",
"issueDate": "2026-10-01",
"dueDate": "2026-10-15",
"totalCents": 42500, "total": "425.00",
"paidCents": 42500, "paid": "425.00",
"balanceCents": 0, "balance": "0.00",
"currency": "usd",
"client": { "id": "cli_01J…", "name": "Ana Ruiz", "email": "ana@example.com" },
"url": "https://golden.tendroom.com/portal/invoices/inv_01JA…"
}
}Money is in whole cents and as text, in US dollars. Dates are the business's own calendar dates and times its local time. url is the page in the portal, which needs signing in. Events never include a client's private link or private notes.
Events
The business chooses which of these its address gets, or all of them (including any added later).
client.createdA client is addedclient.updatedA client's details changeinquiry.receivedAn inquiry comes inbooking.createdA booking is madebooking.movedA booking is movedbooking.cancelledA booking is cancelledestimate.acceptedAn estimate is acceptedestimate.declinedAn estimate is declinedinvoice.sentAn invoice is sentpayment.receivedA payment comes in, deposits tooinvoice.paidAn invoice is paid in fullpayment.refundedA payment is refundedinvoice.voidedAn invoice is voidedtask.createdA task is addedtask.completedA task is donetime.submittedHours are submittedtime.approvedHours are approvedpingA test event, sent when the business presses Send test event
Checking the signature
Check every request before trusting it: anyone can send a POST to your address. With a Standard Webhooks library (Node shown; there are others for most languages):
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.TENDROOM_WEBHOOK_SECRET); // whsec_…
export async function handle(request) {
const body = await request.text(); // the raw body, before any JSON parsing
try {
const event = webhook.verify(body, Object.fromEntries(request.headers));
// event.id is the same on every try: skip one you've already handled.
return new Response(null, { status: 204 });
} catch {
return new Response("Bad signature", { status: 400 });
}
}Or by hand:
signed = webhook-id + "." + webhook-timestamp + "." + raw body key = base64-decode(the secret without its "whsec_" prefix) expected = base64(HMAC-SHA256(key, signed)) webhook-signature is "v1,<signature>", or several of them separated by spaces while a secret is being changed: accept the request if any one matches expected (compare in constant time), and refuse it if webhook-timestamp is more than 5 minutes from now.
When the business changes the secret, the old one keeps signing for 24 hours alongside the new one, so you can switch over without missing anything.
Answering, retries and pausing
- Answer with any 2xx status within 5 seconds. The response body is ignored, so do slow work after answering.
- Anything else (another status, a redirect, a timeout) is tried again: 8 tries in all, after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and a day. A 429 with Retry-After waits that long.
- Redirects aren't followed: use the final address.
- The same event can arrive more than once, and events can arrive out of order. Use the event id to skip repeats, and occurredAt (or fetch the latest from your own records) for order.
- If 20 tries in a row to an address fail over at least an hour, or every try fails for 3 days, Tendroom pauses it and emails the business's owners. Nothing more is sent until they resume it; what was waiting is kept and sent then.
- A 429 isn't counted as failing: Tendroom waits as long as its Retry-After asks.
- The business can send any delivery from the last 7 days again from its log.
Questions
Write to privacy@tendroom.com.