# Buildkite Webhooks

## When to Use This Skill

* How do I receive Buildkite webhooks?
* How do I verify the `X-Buildkite-Token` header?
* How do I verify an `X-Buildkite-Signature` HMAC signature?
* How do I handle `build.finished`, `job.finished`, or `agent.connected` events?
* Why is my Buildkite webhook verification failing?

## Essential Code (USE THIS)

Buildkite sends **one** of two headers on every webhook, depending on how the webhook's Token is configured in Notification Services:

* **Token mode (default):** the token is sent as plain text in `X-Buildkite-Token`. Compare it to your stored token in constant time. There is no body signature, so anyone who sees the token can forge requests.
* **Signature mode (more secure, optional):** `X-Buildkite-Signature: timestamp=<unix>,signature=<hex>`. The signature is HMAC-SHA256 (hex) of `"<timestamp>.<raw body>"`, keyed with the webhook token.

If the webhook goes through Hookdeck Event Gateway, use token mode: Hookdeck's Buildkite source verifies `X-Buildkite-Token`, not `X-Buildkite-Signature`.

The event name is in the `X-Buildkite-Event` header (and `event` in the JSON body).

### Verification (Node.js)

```javascript
const crypto = require('crypto');

function safeEqual(a, b) {
  const x = Buffer.from(a), y = Buffer.from(b);
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

// rawBody: Buffer of the exact request bytes (express.raw, not express.json)
function verifyBuildkite(rawBody, headers, token) {
  if (!token) return false;
  const sigHeader = headers['x-buildkite-signature'];
  if (sigHeader) {
    // "timestamp=1619071700,signature=<hex>"
    const parts = Object.fromEntries(sigHeader.split(',').map((kv) => kv.trim().split(/=(.*)/s).slice(0, 2)));
    if (!parts.timestamp || !parts.signature) return false;
    // Optional replay window, off by default: retries and replays keep the original timestamp
    const expected = crypto.createHmac('sha256', token)
      .update(`${parts.timestamp}.`).update(rawBody).digest('hex');
    return safeEqual(parts.signature, expected);
  }
  const tokenHeader = headers['x-buildkite-token'];
  return !!tokenHeader && safeEqual(tokenHeader, token);
}

```

### Verification (Python)

```python
import hashlib, hmac

def verify_buildkite(raw_body: bytes, headers, token: str) -> bool:
    if not token:
        return False
    sig_header = headers.get("x-buildkite-signature")
    if sig_header:
        parts = dict(kv.strip().split("=", 1) for kv in sig_header.split(",") if "=" in kv)
        if "timestamp" not in parts or "signature" not in parts:
            return False
        signed = parts["timestamp"].encode() + b"." + raw_body
        expected = hmac.new(token.encode(), signed, hashlib.sha256).hexdigest()
        return hmac.compare_digest(parts["signature"], expected)
    return hmac.compare_digest(headers.get("x-buildkite-token", ""), token)

```

A request that carries `X-Buildkite-Signature` is never allowed to fall back to the token check.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/buildkite-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/buildkite-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/buildkite-webhooks/examples/fastapi/).

## Common Event Types

Subscribe to events when creating the webhook; the name arrives in `X-Buildkite-Event`.

| Event | Description |
| --- | --- |
| `ping` | Webhook notification settings changed (sent when you save the webhook) |
| `build.scheduled` | A build was scheduled |
| `build.running` | A build started running |
| `build.failing` | A build is failing (a job failed, build still running) |
| `build.finished` | A build finished (check `build.state`) |
| `job.started` | A command step job started on an agent |
| `job.finished` | A job finished |
| `agent.connected` | An agent connected to the API |

Others: `build.skipped`, `job.scheduled`, `job.activated`, `job.promised_exit_status`, `agent.lost`, `agent.disconnected`, `agent.stopping`, `agent.stopped`, `agent.blocked`, `cluster_token.registration_blocked`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/buildkite-webhooks/references/overview.md).

> **For the full event reference**, see [Buildkite pipelines webhooks](https://buildkite.com/docs/apis/webhooks/pipelines).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Buildkite-Event` | Event type, e.g. `build.finished` |
| `X-Buildkite-Token` | Webhook token in plain text (token mode, the default) |
| `X-Buildkite-Signature` | `timestamp=<unix>,signature=<hex>` (signature mode); timestamp is dispatch time |

## Environment Variables

```bash
BUILDKITE_WEBHOOK_TOKEN=your_webhook_token   # "Token" field of the webhook in Notification Services
BUILDKITE_TIMESTAMP_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 buildkite --path /webhooks/buildkite

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/buildkite-webhooks/references/overview.md) - Buildkite webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/buildkite-webhooks/references/setup.md) - Notification Services configuration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/buildkite-webhooks/references/verification.md) - Token and signature verification details