Developer docsWebhooks
Guides

Webhooks

Rookery tells your app when something happens, with a small signed POST. Events, verification, retries and the delivery log.

Updated View as Markdown

A webhook is Rookery calling your app: when a client replies, documents come in or someone books a time, Rookery sends a small, signed POST to an address you choose. Your app doesn't have to keep asking.

Webhooks come with the Pro and Team plans, with the API. Owners and admins set them up. For Zapier, Make and n8n, see Webhooks in Help.

Add a webhook

  1. Go to Settings → Integrations → Webhooks.
  2. Under Send to, paste your app's address. It must be a public https:// address.
  3. Add a Description, and under Send these events, tick what it should hear about.
  4. If you have a private inbox, choose From which inboxes: Team inboxes, or Only my private inbox.
  5. Click Add webhook, then copy the Signing secret. It starts with whsec_ and is shown once.
  6. Open the webhook and click Send test event. Rookery sends webhook.test, and shows your app's answer straight away.

The events

EventWhen it's sent
ask.completedEverything you asked a client for is in (documents today)
client.repliedSomeone wrote back on a conversation
conversation.assignedA conversation was given to someone, or to nobody
turn.changedWhose turn it is changed: yours, theirs, or done
draft.approvedA person approved a helper's or an agent's draft
message.sentAn email went out
booking.createdSomeone booked a time with you
booking.cancelledA booking was cancelled
scout.heldScout held a likely scam
webhook.testSent when you press Send test event

Each event's fields, with an example, are in the event reference. The full schemas are in the OpenAPI spec, under webhooks.

What Rookery sends

Every event has the same envelope. data is thin on purpose: ids to look things up with the API, a one-line summary, and a url to the conversation when there is one. It never has an email's words, a note, a file or an attachment.

{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "client.replied",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "message_id": "msg_7h2kq9m3x1c8v5bn",
    "mailbox_id": "mbx_d56zb3gp9g8qj3qt",
    "secure": false,
    "summary": "Dana Wu replied",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}
  • id is the same on every try and on a Resend. Keep the ids you've handled and skip repeats.
  • version changes only when an event changes in a way that could break your app. New fields can appear at any time, so ignore ones you don't know.
  • Events can arrive out of order. Use created_at, or read the conversation with the API for its current state.

Private inboxes

A webhook for Team inboxes hears about team inboxes, and about the workspace as a whole (bookings outside a conversation, test events). It never hears about anyone's private inbox.

A webhook for Only my private inbox hears about its maker's own private inboxes and nothing else. Only its maker can see it, its log and what it sent. This matches API keys, which never see someone else's private mail.

Check the signature

Every event is signed the Standard Webhooks way, with three headers:

HeaderWhat it is
webhook-idThe event's id, the same as id in the body
webhook-timestampWhen this try was sent, in seconds
webhook-signaturev1, and a base64 signature. For 24 hours after a rotation there are two, separated by a space

To check it, sign webhook-id.webhook-timestamp.body with HMAC-SHA256, keyed with your secret after whsec_ (decoded from base64). Accept the event if your result matches any of the signatures, and refuse a timestamp more than 5 minutes old. Always use the raw body, before any JSON parsing.

The standardwebhooks library for your language does all of this. Without it:

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

export function fromRookery(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
  return headers["webhook-signature"].split(" ").some((part) => {
    const [version, signature] = part.split(",");
    const given = Buffer.from(signature ?? "", "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Python

import base64, hashlib, hmac, time

def from_rookery(secret: str, headers: dict, raw_body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(
        part.startswith("v1,") and hmac.compare_digest(part[3:], expected)
        for part in headers["webhook-signature"].split(" ")
    )

To change the secret, open the webhook and click Rotate secret. Both secrets sign every event for the next 24 hours, so you can switch over without missing anything.

Answering, retries and backoff

  • Answer 2xx within 10 seconds. Any 2xx counts as delivered. Put the event on your own queue and do the slow work afterwards.
  • Anything else is tried again after about 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours, each give or take 20%. That's about a day in all, then Rookery stops trying that event.
  • Redirects aren't followed. Use the final address.
  • 410 Gone means "stop". Rookery turns the webhook off and emails its owner.
  • A day of failures pauses it. If a webhook fails for a whole day without one success, Rookery pauses it and emails its owner. Nothing more is sent until someone clicks Resume.

The delivery log and Resend

Each webhook's page has a Delivery log of the last 30 days. Filter it by Failed or Delivered, or by event. Open a delivery to see every try, the status code and what your app answered, and exactly what was sent.

Resend sends that event again, now, with the same id. Use it after fixing a bug in your app; if you skip ids you've handled, a Resend of one you did handle is harmless.

Good to know

  • Rookery only sends to public https:// addresses. It refuses private networks, and addresses whose name points to one, both when you add the webhook and before every send.
  • Each webhook has its own events. Add as many as you need, like one for Zapier and one for your own app.
  • Every change to a webhook is in the audit log, with who made it.