Bitbucket Integration
Receive and route Bitbucket Cloud webhooks for pushes, pull requests and issue activity.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Bitbucket Production",
"slug": "bitbucket",
"provider": "bitbucket",
"signingSecret": "your-webhook-secret"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/bitbucketWarning
Bitbucket signs under X-Hub-Signature, without the -256 suffix. GitHub uses
X-Hub-Signature-256 for the same hex HMAC-SHA256 digest behind the same sha256= prefix, so a reader
who knows GitHub will reach for the wrong provider here. A Bitbucket source set to github looks for a
header Bitbucket never sends, finds no signature at all, and marks every event unverified — and drops
it outright if rejectInvalidSignatures is on.
The header name is also shared: in Hookbase's scheme table, X-Hub-Signature is Intercom's header
too, with HMAC-SHA1 behind a sha1= prefix. The header does not identify the algorithm, so the
provider you set on the source is what decides how it is read.
2. Create the Bitbucket Webhook
- Open the repository's settings and go to its webhooks
- Add a webhook and set its URL to your Hookbase ingest URL
- Set a secret on the webhook and store the same value on your Hookbase source as
signingSecret - Choose the triggers you want delivered
- Leave the webhook active
Bitbucket only signs a delivery when the webhook has a secret configured. A webhook created without one sends no signature header, so Hookbase has nothing to verify and every event arrives unverified.
See Bitbucket's webhook documentation for the full trigger list.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "CI Trigger", "slug": "bitbucket-ci", "url": "https://api.myapp.com/webhooks/bitbucket"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Bitbucket to CI", "sourceId": "src_...", "destinationId": "dst_..."}'A route carries one destination. Fanning one source out to three endpoints is three routes.
Signature Verification
Bitbucket signs the raw request body with HMAC-SHA256 and sends the hex digest in the
X-Hub-Signature header behind a sha256= prefix:
X-Hub-Signature: sha256=9a1c0b2d3e4f50617283a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f708Hookbase verifies this automatically once the source has both fields set:
{
"provider": "bitbucket",
"signingSecret": "your-webhook-secret"
}The prefix is required, not tolerated: a header value that does not start with sha256= is treated as
not being this scheme's signature at all, rather than being compared as a bare digest. The HMAC key is
the secret exactly as stored — nothing is stripped and nothing is decoded.
There is no timestamp in this scheme, so nothing expires and no clock tolerance applies. The signature says the body came from a webhook holding your secret; it says nothing about when.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored. It defaults to false.
Common Events
Bitbucket names each delivery with an event key:
| Event key | Description |
|---|---|
repo:push | Commits were pushed, or a branch or tag changed |
pullrequest:created | A pull request was opened |
pullrequest:updated | A pull request was edited |
pullrequest:approved | A pull request was approved |
pullrequest:fulfilled | A pull request was merged |
pullrequest:rejected | A pull request was declined |
issue:created | An issue was created |
issue:updated | An issue was updated |
Info
Expect events with no event type on this source. Bitbucket puts the event key in a header of its
own rather than in the body. Hookbase derives an event type for a Bitbucket source from the payload,
trying event_type, then event, then type, then action — and Bitbucket's bodies carry none of
those, so the event type is empty.
Route on the payload instead. A push body has a push object, a pull request body has a pullrequest
object, and an issue body has an issue object, which is enough to tell them apart.
Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
Push Summary for a CI Trigger
function transform(payload) {
const changes = (payload.push && payload.push.changes) || [];
const first = changes[0] || {};
const target = first.new || first.old || {};
return {
repository: payload.repository ? payload.repository.full_name : null,
actor: payload.actor ? payload.actor.display_name : null,
ref_type: target.type || null,
ref_name: target.name || null,
commit_count: (first.commits || []).length,
received_at: new Date().toISOString()
};
}Slack Notification for Pull Requests
function transform(payload) {
const pr = payload.pullrequest || {};
const source = (pr.source && pr.source.branch) || {};
const destination = (pr.destination && pr.destination.branch) || {};
return {
text: `Pull request ${pr.state || "updated"}: ${pr.title || pr.id || "unknown"}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Repository:*\n${payload.repository ? payload.repository.full_name : "unknown"}` },
{ type: "mrkdwn", text: `*Title:*\n${pr.title || "unknown"}` },
{ type: "mrkdwn", text: `*Branch:*\n${source.name || "?"} to ${destination.name || "?"}` },
{ type: "mrkdwn", text: `*State:*\n${pr.state || "unknown"}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Pushes Only
{
"name": "Pushes Only",
"logic": "AND",
"conditions": [
{
"field": "push",
"operator": "exists",
"value": ""
}
]
}One Repository
{
"name": "Web App Repository",
"logic": "AND",
"conditions": [
{
"field": "repository.full_name",
"operator": "equals",
"value": "acme/web-app"
}
]
}Merged Pull Requests Only
{
"name": "Merged Pull Requests",
"logic": "AND",
"conditions": [
{
"field": "pullrequest.state",
"operator": "equals",
"value": "MERGED"
}
]
}Headers
| Header | Description |
|---|---|
X-Hub-Signature | Hex HMAC-SHA256 of the raw body, prefixed sha256= |
Bitbucket sends further headers of its own carrying the event key and delivery identifiers. Hookbase
uses X-Hub-Signature for verification and does not replay the rest: a delivery to an HTTP destination
carries Hookbase's own X-Event-ID and X-Delivery-ID headers plus whatever the destination is
configured to add, so your handler should read what it needs out of the payload.
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerisbitbucket, notgithub. The two schemes are identical except for the header name, which is exactly why the mistake is easy and silent - Confirm the webhook has a secret set in Bitbucket. Without one there is no signature header, and an event with no header to check cannot verify
- Confirm the stored
signingSecretmatches that webhook's secret character for character, with no surrounding whitespace — the secret is used as its own bytes - Confirm the secret belongs to the webhook that is delivering. A repository with several webhooks has several secrets
- Check nothing between Bitbucket and Hookbase rewrites the body. The digest covers the raw bytes
No Events Arriving
- Check the webhook URL matches your ingest URL exactly, org and source slugs included
- Check the triggers selected on the webhook actually cover what you are doing
- Check the webhook is active
- If the source has
rejectInvalidSignatures: true, a wrong secret shows up as401responses in Bitbucket's own delivery log rather than as stored events
Duplicate Events
Hookbase has no provider event-id extractor for Bitbucket, so on a source with dedupEnabled: true
the default auto strategy falls back to hashing the payload: two byte-identical deliveries inside the
dedup window collapse into one, and two that differ in any field do not. If the deliveries you receive
carry a unique per-request identifier header, name that header in dedupCustomHeader to dedup on it
instead.
Separately from that, a source with a signing secret gets a 24-hour payload-hash replay check on every
request whose signature verified, whether or not dedupEnabled is on.