Code hosting
How Bitbucket Cloud signs webhooks
Bitbucket Cloud signs webhooks that have a secret with an HMAC-SHA256 of the raw body, sent as X-Hub-Signature: sha256=….
Checked against Bitbucket Cloud's documentation on October 31, 2026. Bitbucket Cloud webhook docs
How the signature works
| Header | X-Hub-Signature: sha256={signature} |
|---|---|
| Signed content | {raw body} |
| Algorithm | HMAC-SHA256 |
| Encoding | hex |
| Key | Any text; can't be viewed again after saving |
| Where to find it | The secret set when the webhook was created (Repository settings → Webhooks). It can't be viewed again; set a new one if unsure. |
| Replay protection | No signed timestamp. |
- Bitbucket says the method in method=signature may change: read it from the header rather than assuming sha256.
verified Source: support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/
Verification code
Generated from the scheme above and tested against a signed example request, a changed body and a wrong secret. Pass the raw request body, before any JSON or form parsing.
const crypto = require('node:crypto');
/**
* Verify a Bitbucket Cloud webhook. rawBody: the request body as a Buffer, exactly as received
* (before any JSON parsing); headers: lower-case names, as in req.headers.
*/
function verifyWebhook(rawBody, headers, secret, url = '', method = 'POST') {
rawBody = Buffer.from(rawBody);
const header = headers['x-hub-signature'] || '';
if (!header.startsWith('sha256=')) return false;
const signatures = [header.slice(7)];
const signed = rawBody;
const key = secret;
const expected = crypto.createHmac('sha256', key).update(signed).digest('hex');
const valid = signatures.some((sig) => {
const a = Buffer.from(sig.toLowerCase());
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
if (!valid) return false;
return true;
}
// Express: keep the raw body for this route
// app.post('/webhooks/bitbucket', express.raw({ type: '*/*' }), (req, res) => {
// if (!verifyWebhook(req.body, req.headers, process.env.BITBUCKET_WEBHOOK_SECRET)) return res.sendStatus(401);
// res.sendStatus(200); // then process the event asynchronously
// });
import base64
import hashlib
import hmac
def verify_webhook(raw_body: bytes, headers, secret: str, url: str = '', method: str = 'POST') -> bool:
"""Verify a Bitbucket Cloud webhook. raw_body: the request body exactly as received (bytes);
headers: a mapping with lower-case names (Flask and Django headers are case-insensitive)."""
header = headers.get('x-hub-signature', '')
if not header.startswith('sha256='):
return False
signatures = [header[7:]]
signed = raw_body
key = secret.encode()
digest = hmac.new(key, signed, hashlib.sha256).digest()
expected = digest.hex()
if not any(hmac.compare_digest(sig.lower(), expected) for sig in signatures):
return False
return True
# Flask: verify_webhook(request.get_data(), request.headers, SECRET)
<?php
/**
* Verify a Bitbucket Cloud webhook. $rawBody: file_get_contents('php://input'), before any decoding;
* $headers: lower-case names, e.g. array_change_key_case(getallheaders()).
*/
function verify_webhook(string $rawBody, array $headers, string $secret, string $url = '', string $method = 'POST'): bool
{
$header = (string) ($headers['x-hub-signature'] ?? '');
if (! str_starts_with($header, 'sha256=')) {
return false;
}
$signatures = [substr($header, 7)];
$signed = $rawBody;
$key = $secret;
$expected = hash_hmac('sha256', $signed, $key);
$valid = false;
foreach ($signatures as $sig) {
$valid = $valid || hash_equals($expected, strtolower($sig));
}
return $valid;
}
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strings"
)
// VerifyWebhook checks a Bitbucket Cloud webhook. rawBody is the request body exactly as received
// (io.ReadAll(r.Body) before any decoding).
func VerifyWebhook(rawBody []byte, h http.Header, secret, url, method string) bool {
header := h.Get("X-Hub-Signature")
if !strings.HasPrefix(header, "sha256=") {
return false
}
signatures := []string{strings.TrimPrefix(header, "sha256=")}
signed := []byte{}
signed = append(signed, rawBody...)
key := []byte(secret)
mac := hmac.New(sha256.New, key)
mac.Write(signed)
expected := hex.EncodeToString(mac.Sum(nil))
valid := false
for _, sig := range signatures {
if hmac.Equal([]byte(strings.ToLower(sig)), []byte(expected)) {
valid = true
}
}
return valid
}
Headers Bitbucket Cloud sends
| Header | Meaning |
|---|---|
X-Event-Key | Event, e.g. repo:push or pullrequest:created |
X-Hook-UUID | The webhook's id |
X-Request-UUID | Id of this delivery |
X-Attempt-Number | Delivery attempt |
X-Hub-Signature | sha256= HMAC of the body (only with a secret) |
Timeouts and retries
| Response timeout | Not published. |
|---|---|
| Counts as success | 2xx (not stated explicitly). |
| Retries | Not described in the current documentation. |
| Ordering | Not documented. |
| Duplicates | X-Request-UUID identifies a delivery. |
| Disabling | Not documented. |
partly unverified Source: support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/
Event types
The event is in X-Event-Key header. Examples:
repo:pushpullrequest:createdpullrequest:fulfilledrepo:commit_status_updatedissue:created
verified Source: support.atlassian.com/bitbucket-cloud/docs/event-payloads/
Source IP addresses
Bitbucket Cloud sends webhooks from these addresses (11 ranges, as of 2026-10-31):
104.192.136.0/21 185.166.140.0/22 13.52.5.96/28 13.236.8.224/28 18.136.214.96/28 18.184.99.224/28 18.234.32.224/28 18.246.31.224/28 52.215.192.224/28 13.200.41.128/25 2401:1d80:3000::/36
Atlassian's outgoing (egress) ranges for Bitbucket; they cover all of Bitbucket's outbound traffic, not only webhooks.
Machine-readable list: https://ip-ranges.atlassian.com/ (we refresh our copy weekly)
An allow-list only filters traffic: proxies, CDNs and shared cloud addresses mean the signature is still the real proof of origin.
verified Source: support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/
Common mistakes
- Expecting a signature when no secret is set: Bitbucket only signs webhooks that have one.
- Hashing parsed JSON instead of the raw body.
- Assuming sha256 forever: read the method before the = sign.
Stuck? Point the webhook at a free webhook inspector bin: it detects Bitbucket, checks the signature with your secret in the browser and shows the expected value next to the received one.
Not confirmed in Bitbucket Cloud's documentation
- Timeout, retries and the X-Attempt-Number / X-Request-UUID headers are not in the current Bitbucket Cloud documentation; they come from observed deliveries.
- The User-Agent shown is an example.
FAQ
How do I verify a Bitbucket Cloud webhook signature?
Compute HMAC-SHA256 over {raw body} using webhook secret, encode it as hex and compare it in constant time with the value in X-Hub-Signature (format: sha256={signature}).
How long does Bitbucket Cloud wait for a webhook response?
Not published. Success means: 2xx (not stated explicitly).
Does Bitbucket Cloud retry failed webhooks?
Not described in the current documentation.
What IP addresses does Bitbucket Cloud send webhooks from?
Bitbucket Cloud publishes its webhook source addresses: https://ip-ranges.atlassian.com/. Signatures are still the real check, because proxies and CDNs can change the address you see.
Why does my Bitbucket Cloud signature check fail?
Expecting a signature when no secret is set: Bitbucket only signs webhooks that have one. Hashing parsed JSON instead of the raw body.
Other webhook providers
Stripe GitHub GitLab Shopify Slack Twilio Paddle Billing Zoom Linear Square PayPal SendGrid Mailgun Discord Svix Standard Webhooks Twitch EventSub Intercom HubSpot Jira Cloud Calendly Typeform