# Webhooks

> Tell your own service when something happens in Adea. Each message is signed, carries ids, titles and links, and is sent again if it doesn't arrive.

Source: https://adea.app/docs/webhooks

A webhook is an address of your own that Adea calls when something happens. Use it to start work in your own tools: open a ticket when an insight appears, update a sheet when a list changes, or tell a chat room when someone is given a row.

Only administrators can add and change webhooks. They are on the **Developers** page in **Settings**, under the API keys.

## Add a webhook

1. Open **Settings**, then **Developers**, and find **Webhooks**.
2. Write the address of your service. It has to start with `https://` and be reachable from the internet. Addresses on a private network are refused.
3. Choose what it hears about, then choose **Add webhook**.
4. Copy the signing secret and keep it somewhere safe. It is shown once.

Each webhook has its own signing secret. A company can have up to 10 webhooks. Adding one is written to the security log.

## What you can listen for

| Event | Sent when |
|---|---|
| `insight.created` | Adea creates an insight. |
| `list.changed` | A list gets new rows, or rows leave it. |
| `list.row_assigned` | A row of a list is given to someone. |
| `answer.saved` | Someone saves an answer. |

## What a message holds

A message is JSON, sent with `POST`. It carries ids, titles and a link, never the values in your rows and never personal data. It doesn't say who a row was given to either. To see more, follow the link, where Adea's usual access rules apply.

```json
{
  "id": "0198f1c2-7d1e-7c3a-9b5e-2f6a8d4c1e90",
  "type": "insight.created",
  "createdAt": "2026-10-07T08:30:12.000Z",
  "data": {
    "insightId": "0198f1c2-5a0b-7e11-8c44-91d2b7e3a6f5",
    "title": "Orders from Aarhus fell 18 %",
    "url": "https://your-company.adea.app/insights/0198f1c2-5a0b-7e11-8c44-91d2b7e3a6f5"
  }
}
```

The fields in `data` depend on the event. Every event has a `title` and a `url`.

## Check that it came from Adea

Every message has these headers:

- `Adea-Signature`: `t=<seconds>,v1=<signature>`
- `Adea-Event`: the event, like `insight.created`
- `Adea-Delivery`: the message's id, the same as `id` in the body

The signature is an HMAC with SHA-256 over the timestamp, a dot and the exact body, using the webhook's signing secret. Compute it yourself and compare. Refuse a message whose timestamp is more than five minutes old.

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

export function fromAdea(secret, header, rawBody) {
  const t = /t=(\d+)/.exec(header)?.[1];
  const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(v1, "hex");
  return expected.length === given.length && timingSafeEqual(expected, given);
}
```

Use the body exactly as it arrived, before any parsing.

## Answer quickly

Answer with any `2xx` status within 10 seconds. Do the slow work after you have answered. Adea doesn't follow redirects.

## If it doesn't arrive

A message that doesn't get a `2xx` answer is sent again, after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and a day. After the eighth try Adea gives up on it. A message can arrive more than once, so use `Adea-Delivery` to ignore one you have already handled.

If 20 messages in a row fail, Adea pauses the webhook and the page says so. Fix your service, then switch the webhook on again.

## Test and look back

- **Send a test** sends a message of type `test` to the address, signed like the real ones. It holds none of your data.
- **Last messages** shows the last 50 sent to a webhook: which event, whether it arrived, the status your service answered with and when the next try is. A message that didn't arrive can be sent again from there.

## Switch off or delete

Switch a webhook off to stop messages without losing its settings. Delete it to remove it and its log. Deleting one is written to the security log.

## With the API

Everything on the page is an action, so you can manage webhooks from a script too: `webhooks.list`, `webhooks.create`, `webhooks.change`, `webhooks.test`, `webhooks.log`, `webhooks.retry` and `webhooks.delete`. See [the API](/docs/api).
