Buildkite webhook: X-Buildkite-Token or Signature before a Sume run

Buildkite pipeline webhooks offer a clear-text token or an HMAC-SHA256 signature. Use the signature on build.finished before you start a paid Sume Format run.

4 min readSume
All posts

Use the HMAC signature, not the token. Buildkite pipeline webhooks can authenticate with an X-Buildkite-Token header sent in clear text, or with an X-Buildkite-Signature header that uses HMAC-SHA256. A receiver that starts paid media runs on build.finished should check the signature on the raw body, then call Sume with an Idempotency-Key built from the build.

The pairing is useful for release videos, changelog images and demo clips that you want after a green main build. It is also a place where a spoofed event costs real money, so the authentication choice is part of the budget.

What Buildkite documents

Buildkite's Pipelines webhooks page lists build events (build.scheduled, build.running, build.failing, build.finished, build.skipped), job events, agent events and a ping event (read 2026-10-10). Every request carries an X-Buildkite-Event header with the event type and a JSON body. The page says the last 20 webhook requests and responses are saved for debugging.

It does not document delivery timeouts or retry behavior, so this post treats delivery as best effort and shows how to reconcile instead.

Buildkite webhook authentication (Buildkite Docs, read 2026-10-10) and what each choice means before a paid Sume call
OptionHeader and mechanismConsequence for a Sume trigger
TokenX-Buildkite-Token, the token "in clear text"Anyone who can read the header can replay it; no body binding
SignatureX-Buildkite-Signature, HMAC-SHA256 with a secret keyBinds the check to the received body; refuse an empty secret
Event routingX-Buildkite-EventAct only on build.finished, answer 2xx to the rest
DebuggingLast 20 requests and responses savedNot an audit log; keep your own record by run id

Gate the paid call

Order the checks. First verify the signature on the raw bytes. Second, read the event header and ignore anything but build.finished. Third, read the build state from the body and continue only for the state you care about, such as a passed build on the main branch. Fourth, call Sume. Each earlier step is free; the last one is not.

Keep the secret out of the repository and out of build logs. Read it from your receiver's environment, refuse to start when it is empty, and rotate it if it ever appears in a pipeline output. A token sent in clear text is no better than a password in a URL, so if you must use it, compare it in constant time and never log the header.

Cap the damage anyway. Send generation_spend_cap_usd on every create so a wrong filter cannot spend more than the number you chose, and remember that without it the run inherits the Format's cap, which defaults to $400, with a platform maximum of $500.

Build the key from the build

Use a key such as bk-<pipeline>-<build number>-release-v1. The scope of a key is one Format and it can be up to 255 characters. If Buildkite delivers build.finished twice, the second create returns 200 with idempotency_hit: true. If the same key arrives with a different body, Sume returns 409 idempotency_conflict and starts nothing, which is the signal that your key derivation is unstable.

A concurrent duplicate gets the retryable 409 idempotency_key_in_use. Wait about a second and send the same request again to receive the original run.

Reconcile when an event never arrives

Since the page documents no retries, a lost delivery is possible. Keep a small table of builds you expected to process, and compare it with GET /v1/formats/{handle}/{slug}/runs?limit=100, which lists the newest runs first and pages with next_cursor. Any build without a run gets a manual create using the same key.

Store the run id next to the build number the moment the 202 arrives. That single row answers the three questions you will be asked later: which build produced this clip, what did it cost, and did it finish. The cost is usage.debited_usd_micros on the terminal receipt, which includes the orchestrating model turn, while usage.billable_amount_usd_micros counts only generation spend against the cap.

For the result, send communication.webhook_url pointing at your own route and verify Sume's signature there; result_url stays the backup if your endpoint is down. A canceled or skipped run never delivers a webhook, so read the response when you cancel.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume