Fetch chases

Send events to other apps with webhooks

Updated New
View as Markdown

A webhook sends a short message from Rookery to an address you choose, the moment something happens. Use it to start a Zap, a Make scenario, an n8n workflow or your own code when a client replies, documents come in or someone books a time.

Webhooks come with the Pro and Team plans, with the API. Only owners and admins can set them up.

Add a webhook

  1. Go to Settings → Integrations → Webhooks.
  2. Under Send to, paste the address your other app gave you. It must start with https://.
  3. Add a Description, like "Zapier: new client replies", so you know what it's for later.
  4. Under Send these events, tick what it should hear about.
  5. If you have a private inbox, choose From which inboxes: Team inboxes, or Only my private inbox.
  6. Click Add webhook.
  7. Copy the Signing secret now. It starts with whsec_, and it won't be shown again.

Then open the webhook and click Send test event to check it arrives. The answer shows straight away, and in the log.

The events

EventWhen it's sent
ask.completedEverything you asked a client for is in (document requests today)
client.repliedSomeone writes back on a conversation, by email or on a secure page
conversation.assignedA conversation is given to someone, or to nobody
turn.changedWhose turn it is changes: ours, theirs or done
draft.approvedA person approves a helper's or an agent's draft
message.sentAn email goes out
booking.createdSomeone books a time with you
booking.cancelledA booking is cancelled
scout.heldScout holds a likely scam
webhook.testYou click Send test event

What a message looks like

Each message is a POST with a small JSON body. It has ids and a one-line summary. It never has the email's words, a note, a file or an attachment. To read more, use the ids with the API.

{
  "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, so your app can skip one it has already handled.
  • version changes only if an event changes in a way that could break your app. New fields can be added at any time.

Who hears about private mail

A webhook for Team inboxes hears about team inboxes only, never anyone's private inbox. A webhook for Only my private inbox hears about yours, and only you can see it, its log and what it sent.

Check that a message came from Rookery

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

  • webhook-id: the event's id
  • webhook-timestamp: when it was sent, in seconds
  • webhook-signature: v1, and a signature. Sometimes there are two, with a space between them

To check it, sign webhook-id.webhook-timestamp.body with HMAC-SHA256, using the secret after whsec_ (decoded from base64) as the key. Accept the message 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 for you. 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(" ")
    )

Change the secret

Open the webhook and click Rotate secret. Copy the new one. For the next 24 hours every message is signed with both secrets, so you can update your app without missing anything. After that, only the new one works.

Retries and the delivery log

  • Any 2xx answer counts as delivered. Answer quickly, within 10 seconds, 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. That's about a day in all.
  • Rookery doesn't follow redirects. Use the final address.
  • An answer of 410 Gone means "stop sending". Rookery turns the webhook off and emails you.
  • If a webhook fails for a whole day without one success, Rookery pauses it and emails you. Nothing more is sent until you click Resume.

The webhook's page has a Delivery log of the last 30 days. Filter it by Failed or Delivered, or by event. Open a row to see each try, what your app answered and exactly what was sent. Resend sends that event again, now.

You can Pause a webhook any time, and Delete it when you're done. Deleting can't be undone.

Recipes

Zapier

  1. Make a Zap with Webhooks by Zapier as the trigger, and choose Catch Hook.
  2. Copy the address Zapier shows, and add it in Rookery as a webhook.
  3. In Rookery, click Send test event. Back in Zapier, click Test trigger and pick the request.
  4. Use type and data.summary in your Zap. To act only on replies, add a Filter step: type exactly matches client.replied.

Make

  1. Make a scenario that starts with Webhooks → Custom webhook, and click Add to make a webhook.
  2. Copy its address and add it in Rookery.
  3. Click Re-determine data structure in Make, then Send test event in Rookery.
  4. Add a Router with a filter on type for each event you handle.

n8n

  1. Add a Webhook node. Set HTTP Method to POST and Respond to "Immediately".
  2. Copy the Production URL (it must be https) and add it in Rookery.
  3. Activate the workflow, then click Send test event in Rookery.
  4. To check signatures, add a Code node with the Node.js check above, using the raw body.

Good to know

  • Rookery only sends to public https:// addresses. It won't send to private networks or to an address that points to one.
  • Each webhook can have its own events. Add as many as you need.
  • Every change to a webhook is in the audit log, with who made it.