Agent skill

Buildkite Webhooks Skill

Receive and verify Buildkite webhooks. Use when setting up Buildkite pipeline webhook handlers, debugging X-Buildkite-Token or X-Buildkite-Signature verification, or handling CI/CD events like build.finished, job.finished, build.failing, or agent.connected.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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)

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/, examples/nextjs/, examples/fastapi/.

Common Event Types

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

EventDescription
pingWebhook notification settings changed (sent when you save the webhook)
build.scheduledA build was scheduled
build.runningA build started running
build.failingA build is failing (a job failed, build still running)
build.finishedA build finished (check build.state)
job.startedA command step job started on an agent
job.finishedA job finished
agent.connectedAn 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.

For the full event reference, see Buildkite pipelines webhooks.

Important Headers

HeaderDescription
X-Buildkite-EventEvent type, e.g. build.finished
X-Buildkite-TokenWebhook token in plain text (token mode, the default)
X-Buildkite-Signaturetimestamp=<unix>,signature=<hex> (signature mode); timestamp is dispatch time

Environment Variables

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

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →