Bitbucket merged-PR webhook to a Sume Format run: verify first
Verify Bitbucket's X-Hub-Signature (sha256=) with its published test values, then start a Sume Format run on pullrequest merged with a key built from the PR.

Verify the Bitbucket delivery first, then start the Sume run. Bitbucket Cloud sends an X-Hub-Signature header formatted as method=signature, currently sha256=<hex>, and you check it with a constant-time comparison before you spend anything. Only after that do you call POST /v1/formats/{handle}/{slug}/runs with an Idempotency-Key built from the repository and pull request.
A merged pull request is a natural trigger for a short release-notes clip or a changelog image set. It is also a trigger that can fire twice, so the order of checks matters more than the media.
What Bitbucket documents
Atlassian's Manage webhooks page says a repository can have up to 50 webhooks, that Bitbucket uses your secret token to create an HMAC signature sent with each payload, and that the signature currently uses sha256 (read 2026-10-10). It tells you to compare with a constant-time method, not a plain equality check. Its event list includes pull request events such as Created, Merged and Declined.
The page I read does not give a delivery timeout, a retry policy, a payload size limit or a disable-after-failures rule, and it describes no timestamp header. Design for that: answer fast, and do not assume Bitbucket resends or that it never resends.
| Topic | Bitbucket Cloud | Sume run webhook |
|---|---|---|
| Header | X-Hub-Signature: sha256=<hex> | x-sume-webhook-signature: sume-v1=<hex> |
| Signed bytes | The payload (see the page's test values) | <timestamp>.<raw_body> |
| Replay protection | No timestamp described on the page | Reject timestamps outside five minutes |
| Retries | Not documented on the page | Up to 10 attempts, 10 s timeout each |
| Dedupe | Not documented on the page | request_id, equal to the run id |
| Per-repo limit | 50 webhooks | One webhook_url per run |
Check your verifier against the published vector
Bitbucket's page publishes a test: secret It's a Secret to Everybody, payload Hello World!, expected sha256=a4771c39...63c9. The code below recomputes that value, so you can confirm your handler before a real delivery arrives. It refuses an empty secret and a header whose method is not sha256, and it compares in constant time.
import hashlib, hmac
def verify_bitbucket(raw: bytes, header: str, secret: str) -> bool:
if not secret or not header:
return False
method, _, sig = header.partition("=")
if method != "sha256":
return False
mac = hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
# Test values published on Bitbucket's "Manage webhooks" page
secret = "It's a Secret to Everybody"
payload = b"Hello World!"
header = "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9"
assert verify_bitbucket(payload, header, secret)
assert not verify_bitbucket(payload, header, "")
assert not verify_bitbucket(payload + b"!", header, secret)
print("bitbucket signature ok")Start the Sume run from the merged event
After the signature passes, parse the JSON, check that the event is a merge, and ignore everything else with a 2xx. Build the key like bb-<repo>-pr-<number>-notes-v1 and bump the version only when someone asks for a new take. If Bitbucket delivers the same merge twice, the second create returns 200 with the original receipt and idempotency_hit: true, so you do not pay twice.
Put the title, description and changed-file summary in input, which is caller data with at most 64 top-level keys and 2 MiB, and keep the instruction short. Bind an output_schema so the receipt returns one media URL, set primary_output_key, and add generation_spend_cap_usd. A Format without its own cap defaults to $400 and the platform maximum is $500.
Return the result and cover a missed event
Pass communication.webhook_url for your own Sume route and branch on outcome when it arrives. If a Bitbucket event never reaches you, nothing retries on your behalf that you can rely on, so reconcile: list recent runs with GET /v1/formats/{handle}/{slug}/runs?limit=20 and compare their idempotency keys with merged pull requests from the Bitbucket API.
Handle the unhappy results too. A failed run still carries every file it made in artifacts[], and its error.code says why, for example output_schema_unsatisfied. A degraded outcome means files exist but your schema was not met. Neither changes because delivery failed or succeeded: after ten refused attempts the run is still completed, and you can replay the event with POST /v1/format-runs/{run_id}/webhook/redeliver once your route is fixed.
Keep the pull request number on your side, keyed by the run id. Values sent in input do not come back in output, and Sume does not publish a cross-Format run list, so your own index is the lookup.
Sources
Related posts
More in Integrations
- 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.
- Cal.com BOOKING_CREATED webhook to a Sume video run, verified
Cal.com signs webhooks with x-cal-signature-256. Verify it, turn BOOKING_CREATED into a Sume Format run, and keep the Idempotency-Key stable on retries.
- Docker Agent YAML: add Sume as a remote MCP toolset
Docker Agent takes a remote MCP URL, headers and a tools allowlist. Here is the Sume entry with a Bearer key from the environment and a read-only tool list.
- Freshdesk Trigger Webhook: 1000 calls an hour and Sume bulk runs
Freshdesk automations cap webhook calls at 1000 an hour and retry failures every 30 minutes. Here is how a relay maps ticket bursts onto Sume bulk runs.
Written by Sume