> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rescueconsole.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Tell another system every time something changes in RescueConsole: what is sent, how it is signed, and what happens when a delivery fails.

Open **Settings → Advanced & security → Webhooks** to tell another system every time something here changes: an animal adopted, a person added, a donation logged. The other system can be Zapier, Make, a spreadsheet script or your own website.

Each address you add is an **endpoint**: an https address RescueConsole sends a signed message to whenever a record changes.

Only an **Organization admin** can see and manage webhooks. Anyone else who opens the page is told *Only an organization admin can manage webhooks.*

<Warning>
  Every message carries what changed, including personal details of the people involved. Send it only to systems you trust.
</Warning>

## Adding an endpoint

<Steps>
  <Step title="Get an address from the other system">
    In Zapier, make a Zap that starts with **Webhooks by Zapier → Catch Hook** and copy the address it gives you. In Make, use a **Custom webhook**. Your own code needs an https address that accepts a POST.
  </Step>

  <Step title="Add it">
    Press **Add an endpoint**. In the **New endpoint** card, give it a **Label**, such as *Zapier: new adoptions*, and paste the **Address**.
  </Step>

  <Step title="Choose what it hears about">
    Tick the workspaces and the changes it should be sent. Leave a group with nothing ticked to be sent all of them. Press **Add endpoint**.
  </Step>

  <Step title="Copy the signing secret">
    The signing secret is shown once. Copy it if your own code will check messages, then press **I've saved it**.
  </Step>

  <Step title="Test it">
    Press **Test** on the endpoint and read the answer under it.
  </Step>
</Steps>

| Field                | What it means                                                                                                                |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Label**            | Required. What is on the other end, up to 80 characters.                                                                     |
| **Address**          | Required. It must start with `https://`, must not carry a username or password, and cannot point back at RescueConsole.      |
| **Which workspaces** | People, Organizations, Animals, Fundraising, Shifts, Events, Public Registry, Communications. None ticked means all of them. |
| **Which changes**    | **Created**, **Changed**, **Deleted**, **Status changed**. None ticked means all of them.                                    |

An organization can have up to 20 endpoints. **Add an endpoint** is greyed out once you have 20.

The page does not edit an endpoint's address or choices once it is added. To change them, add a new endpoint and remove the old one.

## The signing secret

The signing secret is what proves a message came from RescueConsole. It is shown once, when the endpoint is added and each time it is rotated, in a panel headed *Copy the signing secret for "Zapier: new adoptions" now. This is the only time it is shown*. It starts `whsec_`.

Zapier and Make do not need it. Your own code should use it to [check the signature](#checking-the-signature) on every message. If it is lost, rotate it and paste the new one where your code keeps it.

## Each endpoint

Each endpoint in the list shows its label, its address, which workspaces and changes it is sent, and the last delivery: when, the answer it gave (such as HTTP 200), and how many deliveries have failed in a row. *Nothing delivered yet* means just that.

| Button            | What it does                                                                                                                                                                                              |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Test**          | Sends a test message now, signed like a real one, and shows the answer: *Delivered. The endpoint answered 200 in 140 ms.* or *Not delivered:* with the reason. A failed test counts as a failed delivery. |
| **Switch off**    | Stops sending to this endpoint. It is marked **Switched off**. Press **Switch on** to start again. Changes made while it was off are not sent later.                                                      |
| **Rotate secret** | Makes a new signing secret and shows it once. The old secret stops working at once, so update your code straight away.                                                                                    |
| **Remove**        | Asks you to confirm, then removes the endpoint. Nothing will be sent to it again. This cannot be undone.                                                                                                  |

## What is sent

Each change is sent as a POST with a JSON body:

```json theme={null}
{
  "id": "the id of the change",
  "type": "animals.animal.update",
  "api_version": "2026-09-10",
  "occurred_at": "2026-09-18T14:03:22.118Z",
  "organization": { "slug": "contoso", "name": "Contoso Animal Rescue" },
  "data": {
    "module": "animals",
    "entity_type": "animal",
    "entity_id": "the id of the record",
    "action": "update",
    "before": { "the record before the change": "..." },
    "after": { "the record after the change": "..." },
    "diff": { "name": { "from": "Max", "to": "Maxwell" } }
  },
  "actor": { "type": "user", "id": "the id of the person who made the change" }
}
```

| Key                         | What it holds                                                                                                                                                  |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                        | The change. It is the same on every try of the same change, so use it to ignore a message you have already handled.                                            |
| `type`                      | The workspace, the kind of record and the action, joined with dots, such as `people.person.create` or `animals.animal.state_change`. A test message is `ping`. |
| `api_version`               | The shape of this body.                                                                                                                                        |
| `organization`              | Your organization: `slug`, the first part of your RescueConsole address, and `name`.                                                                           |
| `data.action`               | `create`, `update`, `delete` or `state_change`.                                                                                                                |
| `data.before`, `data.after` | The whole record before and after the change. `before` is null for a create.                                                                                   |
| `data.diff`                 | For an update, each field that changed, with `from` and `to`.                                                                                                  |
| `actor`                     | Who made the change.                                                                                                                                           |

A test message has `type` set to `ping`, and `data.after` holds a message saying the endpoint works.

Every message also carries these headers:

| Header                      | What it holds              |
| --------------------------- | -------------------------- |
| `X-RescueConsole-Signature` | The signature, below.      |
| `X-RescueConsole-Event`     | The same as `type`.        |
| `X-RescueConsole-Delivery`  | An id for this one try.    |
| `User-Agent`                | `RescueConsole-Webhooks/1` |

## Checking the signature

The `X-RescueConsole-Signature` header looks like `t=1789740202,v1=5f0c…`, where `t` is the time the message was signed, in seconds since 1970, and `v1` is the signature. It is the same scheme Stripe uses for its webhooks.

To check it:

1. Split the header on commas, and each part on its first `=`. Take `t`, and every `v1`.
2. Refuse the message if `t` is more than 300 seconds (five minutes) from your own clock.
3. Work out the HMAC-SHA256 of `t`, a full stop, and the body exactly as it arrived, using the whole signing secret (with `whsec_`) as the key. Write the result as lower-case hex.
4. Accept the message if it equals a `v1`. Compare in constant time.

Use the body as it arrived, before any parsing: parsing and writing JSON again changes the bytes and the signature will not match.

In Node.js:

```js theme={null}
const crypto = require('crypto');

function verifyRescueConsole(rawBody, header, secret) {
  if (!header) return false;
  let t = NaN;
  const signatures = [];
  for (const part of header.split(',')) {
    const eq = part.indexOf('=');
    if (eq === -1) continue;
    const key = part.slice(0, eq).trim();
    const value = part.slice(eq + 1).trim();
    if (key === 't') t = Number(value);
    else if (key === 'v1') signatures.push(value);
  }
  if (!Number.isFinite(t) || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  return signatures.some((s) =>
    s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
```

## Deliveries and retries

A delivery counts as delivered when your endpoint answers with a 2xx status within 10 seconds. Anything else fails: a redirect is not followed, a 4xx or 5xx is recorded with its code, and no answer is recorded as *No response within 10 seconds*.

A failed delivery is tried again up to five more times, waiting longer each time: about a minute, then about two, four, eight and sixteen minutes. An endpoint that has already taken a change is not sent it again when a try is repeated for another endpoint's sake.

After 20 failed deliveries in a row, the endpoint is switched off and the reason is written under it: *Switched off after 20 failed deliveries in a row. The last was:* and the last error. Nothing more is sent to it until you fix the other end and press **Switch on**, which also clears the count.

## Recent deliveries

Press **Recent deliveries** under an endpoint to see its last 50 deliveries, newest first.

| Column     | What it shows                                              |
| ---------- | ---------------------------------------------------------- |
| **When**   | When the delivery was made.                                |
| **Event**  | The message's `type`.                                      |
| **Result** | *Delivered* with the status code, or the reason it failed. |
| **Took**   | How long your endpoint took to answer.                     |

Press **Hide recent deliveries** to fold it away.
