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.

4 min readSume
All posts

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.

Bitbucket Cloud webhook facts (Atlassian Support page, read 2026-10-10) against Sume run webhooks (docs.sume.com)
TopicBitbucket CloudSume run webhook
HeaderX-Hub-Signature: sha256=<hex>x-sume-webhook-signature: sume-v1=<hex>
Signed bytesThe payload (see the page's test values)<timestamp>.<raw_body>
Replay protectionNo timestamp described on the pageReject timestamps outside five minutes
RetriesNot documented on the pageUp to 10 attempts, 10 s timeout each
DedupeNot documented on the pagerequest_id, equal to the run id
Per-repo limit50 webhooksOne 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

All Integrations posts

Written by Sume