# Webhooks

BugTape can POST a bug to a URL you own. Use this for Slack, Discord, GitHub Issues, Telegram, WhatsApp, an iMessage bridge, or a signed JSON endpoint (a generic webhook or an agent destination).

The signed JSON path is what a Cursor Automation, a worker, or your own server should use. Full agent decision (MCP listen, webhook, or poll): [agents.md](./agents.md).

`POST /v1/ingest`, SDK init, and `X-BugTape-Key` are unchanged.

## Destinations

Create one in **Alerts → Destinations**, or **Settings → Integrations**, or `POST /v1/integrations`.

| Type | What BugTape sends |
|---|---|
| `slack`, `discord`, `telegram`, `whatsapp` | That product's own message. No BugTape signature header. |
| `github` | Opens a GitHub issue. The token stays in the destination config. |
| `imessage` | JSON to your bridge (`bridgeUrl`, `address`). Optional `config.secret` adds only `X-BugTape-Signature` (HMAC of that bridge body). |
| `webhook`, `agent` | The signed JSON below. Optional `Authorization: Bearer`. |

Webhook and agent destinations get a signing secret when you create them. Leave the secret blank and BugTape generates `whsec_…`. You can paste your own. The secret is encrypted at rest, shown once, and later reads show `***` plus the last four characters. `POST /v1/integrations/:id/rotate-secret` issues a new secret. The previous secret still verifies for 24 hours.

An optional bearer is write-only. Reads return `{ present, last4 }`. Send an empty `bearerToken` to clear it. BugTape sends it only on webhook and agent JSON posts, as `Authorization: Bearer <token>`.

## When it fires

Subscribe the destination to the events you want. Defaults include `new_bug`, `regression`, and `status_change`. Agent and webhook destinations also get `agent_notify`. Human chat destinations and webhooks also get `agent_update`.

Other events the sender can emit: `comment`, `assigned`, `recommended_bug`, and `test` (the Send test button).

`agent_notify` is a handoff (**Send to agent**, or auto-notify). The body matches `new_bug` plus `"trigger": "auto" | "manual"`. The project alert floor and quiet hours do not hold it.

`agent_update` is progress: `"agentAction": "working" | "pr_opened"`, `"agentActor"`, and optional `"prUrl"`. Same exception for the project floor and quiet hours.

Project **Rules** (severity floor, excludes, quiet hours) still apply to the other events before a delivery is kept.

### Feed and severity on a destination

Webhook and agent destinations can also filter **new issues and regressions**:

- `deliveryFeed`: `all` (default), `urgent_critical`, or `ui_ux`. Same predicate as the inbox feeds.
- `minSeverity`: `critical`, `high`, `medium`, or `low`. This is an extra floor. Omit it to use the project rules only.

Other events ignore these two filters.

`urgent_critical` and `ui_ux` need the issue's triage facets. Those land after the first report, so a matching delivery can wait up to 15 minutes. If facets never arrive, BugTape cancels that delivery. `all` sends immediately.

A new issue is deduped per destination: one `new_bug` delivery for that issue. A regression is not deduped that way.

## Payload

Version `2026-10-08`. Fields already on the body stay. New fields are added. Nothing is removed.

A new critical issue looks like this:

```json
{
  "event": "new_bug",
  "source": "bugtape",
  "version": "2026-10-08",
  "title": "TypeError: Cannot read properties of undefined (reading 'total')",
  "severity": "critical",
  "reportCount": 3,
  "release": "1.4.2",
  "bugId": "3c2e1a00-0000-4000-8000-000000000001",
  "consoleUrl": "https://app.bugtape.ai/console/issues/3c2e1a00-0000-4000-8000-000000000001",
  "evidence": { "mcp": "get_repro_context({ bugId: \"3c2e1a00-0000-4000-8000-000000000001\" })" },
  "timestamp": "2026-10-08T00:00:00.000Z"
}
```

Ingest events also carry the facts BugTape has: `url`, `projectId`, `occurrenceId`, `fingerprint`, `platform`, `environment`, `userId`, `sessionId`, `userEmail`, and a fuller `evidence` object (`mcp`, `events`, `occurrences`, `users`). `occurrenceId` sits just before `timestamp`. Status, comment, and assign events carry what they know.

`evidence.mcp` is the next call. With an agent token, call MCP `get_repro_context` (or `GET /v1/bugs/:id/events`).

`timestamp` is the event time when BugTape has one. It stays the same across retries. It is not the signature time.

## Headers

On every webhook and agent POST:

```
Content-Type: application/json
X-BugTape-Event: new_bug
User-Agent: BugTape-Webhook/1.0
```

Agent destinations use `User-Agent: BugTape-AgentWebhook/1.0`.

When the delivery is tied to a domain event or a delivery row:

```
X-BugTape-Event-Id: <domain event id>
X-BugTape-Delivery-Id: <delivery id>
```

When a signing secret exists:

```
X-BugTape-Signature: sha256=<hex HMAC-SHA256 of the raw body, current secret>
X-BugTape-Signature-256: t=<unix seconds>,v1=<hex>
```

During the 24 hours after a rotation the new header carries both secrets:

```
X-BugTape-Signature-256: t=<unix seconds>,v1=<current hex>,v1=<previous hex>
```

When a bearer is stored:

```
Authorization: Bearer <token>
```

`X-BugTape-Signature-256` is the header to verify. `t` is send time, in unix seconds. The MAC is HMAC-SHA256 of the UTF-8 string `"<t>.<raw body>"` with the signing secret. Reject a header when `|now - t|` is more than 300 seconds. Compare the hex with a constant-time compare. The older `X-BugTape-Signature` is HMAC-SHA256 of the raw body only, with the current secret, prefixed `sha256=`. It does not include `t`.

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

function verifyBugTape(rawBody, header, secrets, nowSec = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(String(header).split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(nowSec - t) > 300) return false;
  const presented = String(header).split(',').filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
  const material = `${t}.${rawBody}`;
  return secrets.some((secret) => presented.some((got) => {
    const expected = createHmac('sha256', secret).update(material).digest('hex');
    const a = Buffer.from(expected);
    const b = Buffer.from(got);
    return a.length === b.length && timingSafeEqual(a, b);
  }));
}
```

Read the raw body before any JSON parse. Respond `2xx` within 10 seconds, then do the work.

## Delivery

Slack, Discord, Telegram, WhatsApp, GitHub, and iMessage use the queue default: one attempt, then 4 retries (10 seconds, exponential), then `webhook-dlq`.

Webhook and agent deliveries that come from capture use a longer schedule after the first send: 10 seconds, 1 minute, 5 minutes, 30 minutes, then 2 hours. That is six sends. A `401`, `403`, or other client rejection (except `408`, `425`, and `429`) stops at once. A network error, `408`, `425`, `429`, or `5xx` waits for the next delay. The row stays `deferred` between tries, so the 10-minute failure window does not mark it failed early. The last failure is `failed`, and a copy goes to `webhook-dlq`. A direct send that is not on the capture path (some regressions, and Send test) keeps the short queue.

**Alerts → Delivery log** lists each attempt. A failed row with `can_retry` can be retried by hand. Quiet hours keep the same delivery id and a minute sweep resumes it. `deferred_until` is the next check, not a promised send time.

Delivery is at-least-once. A receiver that accepted the POST can still see a duplicate if the worker fails after that.

Webhook and agent URLs must be public HTTPS. Private and internal addresses are refused.

## Cursor Automations

1. In Cursor, open Automations. Add a trigger of type Webhook. Pick the repo the agent should edit.
2. Connect BugTape MCP at `https://app.bugtape.ai/mcp`. The token needs `get_repro_context` and `ack_bug`.
3. Copy the automation webhook URL (`https://api2.cursor.sh/automations/webhook/<id>`). Generate the auth header (`crsr_…`).
4. In BugTape, **Alerts → Webhook** (or **Agent**). Paste the URL. Paste the bearer. Set the feed to Urgent and critical and the minimum severity to High, or leave both open. Save. Copy the signing secret once. Send test.
5. Prompt the automation to read the issue id, call `get_repro_context`, `ack_bug` with `working`, open a draft pull request, then `ack_bug` with `prUrl`. Do not merge.

Cursor accepts this JSON body. The bearer is the Cursor auth header, not the BugTape signing secret.

## Claude Code Routines

Set `config.preset` to `claude_routine` on a webhook or agent destination. There is no separate integration type. The URL must be:

```
https://api.anthropic.com/v1/claude_code/routines/<id>/fire
```

Any other host, path, query, or scheme is rejected. BugTape does not send the Anthropic headers to any other URL.

The wire body is the signed JSON wrapped as text. The signature is over these bytes, not the inner JSON:

```json
{"text":"BugTape issue\nTitle: TypeError: Cannot read properties of undefined (reading 'total')\nSeverity: critical\nCount: 3\nRelease: 1.4.2\nIssue: 3c2e1a00-0000-4000-8000-000000000001\nOpen: https://app.bugtape.ai/console/issues/3c2e1a00-0000-4000-8000-000000000001\nNext: get_repro_context({ bugId: \"3c2e1a00-0000-4000-8000-000000000001\" })\n\n{\"event\":\"new_bug\",\"source\":\"bugtape\",\"version\":\"2026-10-08\",\"title\":\"TypeError: Cannot read properties of undefined (reading 'total')\",\"severity\":\"critical\",\"reportCount\":3,\"release\":\"1.4.2\",\"bugId\":\"3c2e1a00-0000-4000-8000-000000000001\",\"consoleUrl\":\"https://app.bugtape.ai/console/issues/3c2e1a00-0000-4000-8000-000000000001\",\"evidence\":{\"mcp\":\"get_repro_context({ bugId: \\\"3c2e1a00-0000-4000-8000-000000000001\\\" })\"},\"timestamp\":\"2026-10-08T00:00:00.000Z\"}"}
```

Headers added only for this preset:

```
anthropic-beta: experimental-cc-routine-2026-04-01
anthropic-version: 2023-06-01
```

`Authorization: Bearer` is the routine token (`sk-ant-oat01-…`). `X-BugTape-Signature-256` and `X-BugTape-Signature` stay, over the wrapped body.

1. At claude.ai/code/routines, pick the repo and add the BugTape connector (remote MCP, bearer PAT with `get_repro_context` and `ack_bug`). Add an API trigger, save, copy the `/fire` URL, and generate the token. The token is shown once.
2. The prompt must treat the routine payload as data. Use only the issue id. Fetch the rest with `get_repro_context`. Then `ack_bug` with `working`, fix on a `claude/` branch, open a draft pull request, and `ack_bug` with `prUrl`. Do not merge.
3. In BugTape, **Alerts → Webhook** (or **Agent**). Set Receiver to Claude Code Routine. Paste the `/fire` URL and the token as the bearer. Set the feed and severity the same way as Cursor. Save. Copy the signing secret once. Send test.

The same filters, signature, and capture retry schedule apply. A live `/fire` call was not part of this change.

## API

Same auth as the other integration routes: console JWT and `X-BugTape-Org`.

`POST /v1/integrations` for `webhook` or `agent` accepts `deliveryFeed`, `minSeverity`, and `bearerToken`. `config.preset` may be `claude_routine` when the URL is the Anthropic `/fire` URL. The 201 body includes `signingSecret` once, plus `signing` and `bearer`.

`PATCH /v1/integrations/:id` accepts the same fields. `bearerToken: ""` clears the bearer. A new `config.secret` rotates the signing secret. `"***"` leaves the stored secret.

`POST /v1/integrations/:id/rotate-secret` returns `{ id, signingSecret, signing, bearer }`.

`POST /v1/integrations/:id/test` sends a signed test payload.
