Home

BugTape docs

Webhooks

Signed JSON for Cursor Automations and Claude routines.

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.

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

Destinations

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

TypeWhat BugTape sends
slack, discord, telegram, whatsappThat product's own message. No BugTape signature header.
githubOpens a GitHub issue. The token stays in the destination config.
imessageJSON to your bridge (bridgeUrl, address). Optional config.secret adds only X-BugTape-Signature (HMAC of that bridge body).
webhook, agentThe 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:

{
  "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.

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:

{"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.