> For the complete documentation index, see [llms.txt](https://docs.adalysis.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.adalysis.com/integrations/webhook.md).

# Webhook

Send your Adalysis alerts to your own systems as signed JSON

The webhook integration sends every Adalysis notification as a JSON `POST` request to a URL you choose. Use it to feed alerts into your own systems, or into automation tools such as Make, n8n, Pipedream or Power Automate.

Each request is signed, so your endpoint can check that it really comes from Adalysis.

**What you can send to a webhook**

* Performance monitor alerts
* Performance goal alerts
* Critical alerts (issues stopping your ads from running)
* Routine tasks reminder
* Budget spend alerts
* Budget pacing alerts
* Budget rollover

Webhooks also receive the notices Adalysis always sends when it couldn't do something it was asked to do (for example, campaigns that couldn't be paused or budget changes that failed), and connection alerts when a login loses access to its ad platform.

***

**Before you connect**

* The **Owner and Admins** can add and remove webhooks and choose which webhook each login uses. Admins can't remove a webhook that also serves logins they don't manage.
* Your endpoint must:
  * use an `https` URL on port 443;
  * be reachable on the public internet (addresses on a private network are refused);
  * accept a JSON `POST` and reply with a `2xx` status within 10 seconds.
* Redirects are not followed, so use the final URL.
* You can add **as many webhooks as you need**. Each login sends to one webhook.

***

**Setting it up**

1. Open your **Adalysis account** (the tile at the top of the rail on the left) and go to **Notifications**.
2. On the **Notification destinations** card, click **Add a Webhook endpoint**. Give it a name (for example, the system it feeds), paste your endpoint URL and click **Add**.
3. Adalysis sends a test event to your endpoint first, and only adds it if your endpoint replies with a `2xx` status.
4. **Copy the signing key** that appears. It is shown only once. If you lose it, click **Rotate key** to get a new one (the old key stops working straight away).
5. Under **Ad platform logins**, choose a webhook in the **Webhook** column for each login, or **No Webhook**, and click **Save**.
6. Open each login in the app, go to **Settings** > **Notifications** and tick the **Webhook** column for each notification you want sent there, then save.

Scheduled notifications arrive at the delivery time and time zone set in the login's **Settings** > **Notifications**. Budget alerts are sent as soon as they happen.

**Tip:** each webhook on the **Notification destinations** card has a **Send test event** button, with a list of examples (for example, *Example: Budget pacing alerts*). Use it to see the exact data your endpoint will get for each kind of notification.

***

**Verifying the signature**

Every request has an `X-Adalysis-Signature` header:

```
X-Adalysis-Signature: t=1761469200,v1=5f2b0c...e91a
```

* `t` is the time the request was signed, in Unix seconds.
* `v1` is the hex-encoded HMAC-SHA256 of `t`, a full stop, and the raw request body (`t + "." + body`). The HMAC key is the whole signing key exactly as it was shown, encoded as UTF-8 text (do not base64-decode it).

To verify a request:

1. Recompute the HMAC over the **raw** body, before any JSON parsing.
2. Compare it with `v1` using a constant-time comparison.
3. Reject requests whose `t` is more than a few minutes old, to block replays.

Python:

```python
import hashlib, hmac, time

def is_from_adalysis(raw_body: bytes, header: str, signing_key: str, tolerance: int = 300) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(signing_key.encode("utf-8"), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
```

Node.js:

```js
const crypto = require("crypto");

function isFromAdalysis(rawBody, header, signingKey, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const { t, v1 } = parts;
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", Buffer.from(signingKey, "utf8"))
    .update(`${t}.`).update(rawBody).digest("hex");
  return expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
```

***

**The request body**

Adalysis sends one request per notification and ad account. The exceptions are the routine tasks reminder, which is sent once per login, and connection alerts, which are sent once per login that lost its connection. The body is JSON (`Content-Type: application/json`). Every field is always present and set to `null` when it doesn't apply.

```json
{
  "schema_version": 1,
  "event_id": "8f0c4a52-3d0e-4b4e-9a51-2f7d1c0b6e11",
  "event_type": "budget_pacing_alert",
  "event_label": "Budget pacing alerts",
  "category": "alert",
  "routable": true,
  "test": false,
  "sent_at": "2026-10-26T09:00:00Z",
  "dedupe_key": "budget_pacing_alert:google:1234567890:2026-10-26",
  "platform": "google",
  "login": { "email": "ppc@example.com" },
  "account": { "id": "1234567890", "display_id": "123-456-7890", "name": "Example Store", "currency": "USD" },
  "title": "…",
  "text": "…",
  "url": "https://…",
  "details": {},
  "items": [],
  "item_count": 0,
  "items_truncated": false
}
```

| Field             | Description                                                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `schema_version`  | Version of this format, currently `1`                                                                                   |
| `event_id`        | Unique ID of this event                                                                                                 |
| `event_type`      | The kind of notification (see below)                                                                                    |
| `event_label`     | Its readable name                                                                                                       |
| `category`        | `alert`, `system` (something Adalysis couldn't do), `connection` or `test`                                              |
| `routable`        | `true` for notifications you can turn on and off in Settings > Notifications                                            |
| `test`            | `true` for test events sent from the **Notification destinations** card                                                 |
| `sent_at`         | When it was sent (ISO 8601, UTC)                                                                                        |
| `dedupe_key`      | Identical for repeats of one notification for one account on one day; use it to skip duplicates. `null` for test events |
| `platform`        | `google`, `microsoft` or `meta`                                                                                         |
| `login.email`     | The login the notification came from                                                                                    |
| `account`         | The ad account: `id`, `display_id` (as shown in the ad platform), `name` and `currency`                                 |
| `title`, `text`   | A headline and a plain-text summary                                                                                     |
| `url`             | A link to the right screen in Adalysis                                                                                  |
| `details`         | Extra values specific to the event type                                                                                 |
| `items`           | The rows of the notification (campaigns, budgets, alerts…); their fields depend on the event type. At most 200 rows     |
| `item_count`      | The total number of rows, including any cut off                                                                         |
| `items_truncated` | `true` when there were more than 200 rows                                                                               |

**Event types**

| `event_type`                         | Notification                                   |
| ------------------------------------ | ---------------------------------------------- |
| `perf_monitor_alert`                 | Performance monitor alerts                     |
| `perf_goal_alert`                    | Performance goal alerts                        |
| `critical_alerts`                    | Critical alerts                                |
| `routine_tasks_reminder`             | Routine tasks reminder                         |
| `budget_spend_alert`                 | Budget spend alerts                            |
| `budget_pacing_alert`                | Budget pacing alerts                           |
| `budget_rollover`                    | Budget rollover                                |
| `campaign_pausing_failed`            | Campaigns could not be paused                  |
| `video_campaigns_to_pause`           | Video campaigns to pause by hand               |
| `auto_manage_disabled_total_budgets` | Budget automation turned off for total budgets |
| `budget_modification_failure`        | Budget changes failed                          |
| `connection_alert`                   | A login lost its connection to its ad platform |
| `test`                               | A test event                                   |

New event types and new fields may be added over time. Ignore any you don't recognise.

***

**Changing or removing a webhook**

To send a login to a different webhook, choose the new one for that login under **Ad platform logins**. To stop using a webhook, click **Remove** next to it.

If your endpoint keeps replying that it doesn't exist or refuses Adalysis (`401`, `403`, `404` or `410`), or can't be reached at all for several days, it is marked as stopped on the **Notification destinations** card and the account's owner gets an email about it. Fix the endpoint, then click **Send test event** to reconnect it. Temporary errors, such as a `5xx` reply, don't disconnect it.

For everything else, such as delivery time, email address and which accounts you get notified about, see [Notifications](https://docs.adalysis.com/onboarding/getting-started/core-account-settings/notifications).
