# Outbound Webhooks

Get an HTTP POST the moment Haystack finishes triaging a PR — instead of polling.

## Overview

Webhooks push Haystack events to any HTTPS endpoint you control. Each event's payload is byte-identical to the corresponding CLI `--json` output — anything built against `haystack triage --json` works on the webhook unchanged.

Events available today:

- `triage.completed` — analysis finished; payload = `haystack triage <ref> --json` (rating, findings, per-finding `agentFixPrompt`)
- `fixer.completed` — the merge-queue fixer finished a run on a PR
- `merge_queue.state_changed` — a PR moved between queued / merging / merged / blocked

Typical uses: gate a CI pipeline on triage instead of polling, post findings to Slack, or feed `agentFixPrompt` straight into your own coding agent when review completes.

## Register an Endpoint

From your repo (requires push access — Haystack verifies this):

```bash
npx -y @haystackeditor/cli@latest webhooks register \
  --url https://example.com/haystack \
  --events triage.completed
```

This records the registration and prints an HMAC secret **once**. Store it in your secret manager — Haystack keeps only an encrypted copy (use `rotate-secret` if you lose it). The registration is recorded in `.haystack.json` with a `secret_ref` pointing at your env var.

## Verify Deliveries

Every delivery is a JSON POST with these headers:

```
X-Haystack-Event:      triage.completed
X-Haystack-Delivery:   <unique delivery id>
X-Haystack-Event-Id:   <repo>:<pr>:<sha>:<event>   (idempotency key)
X-Haystack-Attempt:    1
X-Haystack-Signature:  t=<unix_seconds>,v1=<hex hmac>
```

The signature is `HMAC-SHA256(secret, \`${t}.${rawBody}\`)`. Verify it, reject timestamps older than 5 minutes (replay protection), and dedupe on `X-Haystack-Event-Id`.

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

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

Respond with any 2xx quickly (10-second timeout). Do heavy work asynchronously.

## Delivery Semantics

- Per-repo ordered delivery; failures retry with backoff: 1s, 10s, 1m, 10m, 1h, 6h, 24h (7 attempts, with jitter), then dead-letter
- Duplicates are possible by design — always dedupe on `X-Haystack-Event-Id`
- Payload bodies retained 30 days for inspection and replay

## Manage Registrations

```bash
haystack webhooks list                    # registrations for this repo
haystack webhooks deliveries              # recent deliveries + response codes
haystack webhooks replay <delivery-id>    # redeliver one
haystack webhooks rotate-secret           # mint a new secret (old one dies)
```

All commands authenticate with your normal `haystack login` session and require push access to the repo.

## Links

- [Verification Flow](/docs/verification-flow) - Automated PR verification
- [Agent Setup Skill](/skill.md) - Let your coding agent set up Haystack
