# Send alerts to Slack, Discord or webhooks

Send AgentProof events to a Slack channel, a Discord channel or your own webhook. See the events, the payload fields and how to check signatures.

AgentProof can tell your team when a report is ready, when findings are recorded and when a fix is verified. Send these messages to a Slack channel, a Discord channel, or any web address that accepts a signed request, such as your own service, n8n or Zapier.

Workspace owners and admins set this up in **Settings**, under **Integrations**. Each workspace can have up to 10 destinations.

## Slack

1. In Slack, create an app for your workspace at [api.slack.com/apps](https://api.slack.com/apps), turn on **Incoming Webhooks**, and choose **Add New Webhook to Workspace**.
2. Pick the channel and copy the webhook address. It starts with `https://hooks.slack.com/services/`.
3. In AgentProof, open **Settings**, then **Integrations**. Choose **Slack**, enter a name, paste the address and pick the events.
4. Press **Add destination**, then **Send test**.

## Discord

1. In Discord, open the channel settings, then **Integrations**, then **Webhooks**, and choose **New Webhook**.
2. Copy the webhook address. It starts with `https://discord.com/api/webhooks/`.
3. In AgentProof, choose **Discord**, enter a name, paste the address and pick the events.
4. Press **Add destination**, then **Send test**.

Discord messages never mention `@everyone`, roles or people.

## Your own webhook

Choose **Webhook** and enter an `https` address on port 443 or 8443. AgentProof shows a signing secret once, when you add the destination. Copy it and keep it with your service. Press **New secret** to replace it; the old secret keeps working for 24 hours, so you can switch without missing messages.

## Events

| Event | When it is sent |
| --- | --- |
| `report.ready` | A run finished and its report is ready. |
| `findings.recorded` | An Attack run recorded confirmed findings. |
| `fix.opened` | The Defense Agent opened a pull request with a fix. |
| `fix.verified` | The retest on your live site confirmed the fix. |
| `fix.regressed` | The retest did not confirm the fix. |
| `schedule.skipped` | A scheduled run did not start, for example because no credits were left. |
| `billing.updated` | Your plan or subscription status changed. |
| `endpoint.test` | You pressed **Send test**. |

## Payload

Webhooks receive JSON in the [Standard Webhooks](https://www.standardwebhooks.com/) format. Messages hold names, statuses, counts and links. They never include evidence, agent replies or memory; open the link to read those on AgentProof.

```json
{
  "type": "findings.recorded",
  "timestamp": "2026-11-09T15:00:00Z",
  "data": {
    "workspace": { "id": "wks_...", "name": "Acme" },
    "run": {
      "id": "run_...",
      "agent": "Attack Agent",
      "team": "Attack",
      "status": "completed",
      "target": "https://example.com/",
      "url": "https://isagentproof.com/runs/run_.../"
    },
    "findings": { "total": 3, "high": 1, "medium": 2, "low": 0, "info": 0 }
  }
}
```

## Check the signature

Each request has three headers:

- `webhook-id`: the message ID. It stays the same when a message is sent again, so use it to skip duplicates.
- `webhook-timestamp`: the time of the attempt, in seconds.
- `webhook-signature`: `v1,` followed by a base64 HMAC-SHA256 signature. During a secret change it holds two signatures separated by a space.

Sign `webhook-id`, a dot, `webhook-timestamp`, a dot and the raw request body with your secret (the part after `whsec_`, decoded from base64). Accept the message if any signature matches, and reject timestamps more than five minutes old. In Node.js:

```js
import crypto from "node:crypto";

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

The official Standard Webhooks libraries for many languages do the same check.

## Retries

Answer with any 2xx status within 10 seconds. If the request fails, times out or gets 408, 429 or a 5xx status, AgentProof sends it again after 1 minute, 5 minutes, 30 minutes, 2, 5, 10, 14, 20 and 24 hours, and follows `Retry-After`. Redirects are not followed.

A destination is turned off if it answers `410 Gone`, or if every delivery fails for five days. Turn it back on in **Settings** with **Enable**. **Recent deliveries** shows the last 10 attempts and their results.

## Manage destinations with the API

API tokens with the `notifications:read` and `notifications:write` scopes can list, add, test and remove destinations at `/api/workspaces/current/notifications`. Assistants connected through the [MCP server](/docs/install-mcp-server/) can list destinations and send a test message, but they cannot add one.
