# How to Secure and Verify Bitbucket Webhooks

Webhooks are essential for automating workflows between Bitbucket and your applications. However, without proper security measures, your webhook endpoints are vulnerable to malicious actors who could send fake payloads to trigger unintended actions.

In this guide, we'll explain how to secure your Bitbucket webhooks using signature verification and how Hookdeck can simplify the process. For a comprehensive overview of Bitbucket webhook capabilities, see our [guide to Bitbucket webhooks](/webhooks/platforms/guide-to-bitbucket-webhooks-features-and-best-practices).

## Understanding Bitbucket webhook security

When you create a webhook in Bitbucket Cloud, you have the option to add a secret token. This secret enables Bitbucket to sign every webhook payload using HMAC (Hash-based Message Authentication Code), allowing your server to verify that incoming requests genuinely originated from Bitbucket and haven't been tampered with during transit.

### How Bitbucket signs webhooks

Bitbucket Cloud uses your secret token to create an HMAC signature that accompanies each webhook payload. The signature appears in the `X-Hub-Signature` header with the following format:

```
X-Hub-Signature: sha256=<signature>

```

The signature is calculated using:

* Your secret token: A high-entropy string you provide when creating the webhook
* The raw request body: The exact payload bytes sent by Bitbucket
* SHA-256 hashing algorithm: Currently the only algorithm Bitbucket uses

## Setting up a secure webhook in Bitbucket

### Step 1: Create the webhook

![Create Bitbucket webhook](./images/untitled--92-.png)

1. Navigate to your Bitbucket repository
2. Go to Repository settings > Webhooks
3. Click Add webhook
4. Configure the webhook:
  
  * Title: A descriptive name (e.g., "CI/CD Pipeline Trigger")
  * URL: Your webhook endpoint URL
  * Secret: Enter a random, high-entropy string or click Generate secret to let Bitbucket create one

Important: Record your secret in a secure location immediately. Once saved, the secret cannot be viewed or retrieved from Bitbucket.

### Step 2: Select trigger events

By default, webhooks trigger on repository pushes. To customize:

1. Select Choose from a full list of triggers
2. Choose the events relevant to your use case

Common Bitbucket webhook events include:

| Event Key | Description |
| --- | --- |
| `repo:push` | When commits are pushed to the repository |
| `repo:fork` | When the repository is forked |
| `pullrequest:created` | When a new pull request is opened |
| `pullrequest:approved` | When a pull request receives approval |
| `pullrequest:merged` | When a pull request is merged |
| `pullrequest:rejected` | When a pull request is declined |
| `pullrequest:comment_created` | When someone comments on a pull request |

## Verifying webhook signatures

To ensure incoming webhooks are legitimate, your server must verify the HMAC signature before processing the payload.

### Verification process overview

1. Extract the `X-Hub-Signature` header from the incoming request
2. Parse the algorithm and signature (format: `sha256=<hex_signature>`)
3. Compute an HMAC of the raw request body using your stored secret
4. Compare your computed signature with the one from Bitbucket using a timing-safe comparison

### Node.js verification example

```javascript
const express = require("express");
const crypto = require("crypto");

const app = express();
const WEBHOOK_SECRET = process.env.BITBUCKET_WEBHOOK_SECRET;

// Capture raw body for signature verification
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

function verifyBitbucketSignature(req) {
  const signature = req.get("X-Hub-Signature");

  if (!signature) {
    return false;
  }

  // Parse the signature header (format: sha256=<signature>)
  const [algorithm, receivedSignature] = signature.split("=");

  if (algorithm !== "sha256") {
    return false;
  }

  // Compute expected signature
  const hmac = crypto.createHmac("sha256", WEBHOOK_SECRET);
  const expectedSignature = hmac.update(req.rawBody).digest("hex");

  // Use timing-safe comparison to prevent timing attacks
  const receivedBuffer = Buffer.from(receivedSignature, "utf8");
  const expectedBuffer = Buffer.from(expectedSignature, "utf8");

  if (receivedBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}

app.post("/webhook/bitbucket", (req, res) => {
  if (!verifyBitbucketSignature(req)) {
    console.error("Invalid webhook signature");
    return res.status(401).send("Unauthorized");
  }

  // Process the verified webhook
  const eventType = req.get("X-Event-Key");
  console.log(`Received verified ${eventType} event`);

  // Handle different event types
  switch (eventType) {
    case "repo:push":
      handlePush(req.body);
      break;
    case "pullrequest:created":
      handlePullRequest(req.body);
      break;
    default:
      console.log(`Unhandled event type: ${eventType}`);
  }

  res.status(200).send("OK");
});

app.listen(3000, () => {
  console.log("Webhook server listening on port 3000");
});

```

### Python verification example

```python
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("BITBUCKET_WEBHOOK_SECRET")

def verify_bitbucket_signature(payload, signature_header):
    if not signature_header:
        return False

    # Parse signature header (format: sha256=<signature>)
    try:
        algorithm, received_signature = signature_header.split("=", 1)
    except ValueError:
        return False

    if algorithm != "sha256":
        return False

    # Compute expected signature
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Use timing-safe comparison
    return hmac.compare_digest(expected_signature, received_signature)

@app.route("/webhook/bitbucket", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Hub-Signature")

    if not verify_bitbucket_signature(request.data, signature):
        abort(401, "Invalid signature")

    event_type = request.headers.get("X-Event-Key")
    payload = request.json

    print(f"Received verified {event_type} event")

    # Process the webhook payload
    if event_type == "repo:push":
        handle_push(payload)
    elif event_type == "pullrequest:created":
        handle_pull_request(payload)

    return "OK", 200

if __name__ == "__main__":
    app.run(port=3000)

```

## Critical security best practices

### 1. Use timing-safe comparisons

Never use standard equality operators (`==` or `===`) to compare signatures. These can leak timing information that attackers exploit. Always use:

* Node.js: `crypto.timingSafeEqual()`
* Python: `hmac.compare_digest()`
* Go: `subtle.ConstantTimeCompare()`

### 2. Preserve the raw request body

The HMAC signature is computed on the exact bytes Bitbucket sends. Any parsing, reformatting, or beautification of the payload before verification will produce a different signature.

### 3. Secure secret storage

* Store secrets in environment variables or a secrets manager
* Never hardcode secrets in source code
* Never commit secrets to version control
* Rotate secrets periodically

### 4. Handle UTF-8 encoding

Bitbucket payloads may contain Unicode characters. Ensure your server handles the payload as UTF-8 when computing the HMAC.

## Simplifying verification with Hookdeck

Manually implementing and maintaining webhook verification across multiple providers can be complex and error-prone. Hookdeck provides a webhook gateway that handles verification automatically, allowing you to focus on processing events.

### What is Hookdeck?

[Hookdeck](https://hookdeck.com) provides an event gateway that sits between webhook providers (like Bitbucket) and your application. It provides:

* Automatic signature verification
* Event queuing and retry logic
* Request logging and debugging tools
* Local development tunneling

### Setting up Bitbucket webhooks with Hookdeck

#### Step 1: Install the Hookdeck CLI

macOS:

```bash
brew install hookdeck

```

Windows:

```bash
scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git
scoop install hookdeck

```

#### Step 2: Authenticate

```bash
hookdeck login

```

This opens your browser for authentication. If you don't have a Hookdeck account, you can create one during this step.

#### Step 3: Create a connection

```bash
hookdeck listen 3000 bitbucket-source --path /webhook/bitbucket

```

This command:

* Creates a public URL for receiving webhooks
* Forwards events to `http://localhost:3000/webhook/bitbucket`
* Displays the Source URL to configure in Bitbucket

#### Step 4: Configure Bitbucket

1. Copy the Hookdeck Source URL from the CLI output
2. In Bitbucket, create or edit your webhook
3. Set the URL to your Hookdeck Source URL
4. Add your secret token
5. Save the webhook

#### Step 5: Configure source verification

1. Open the Hookdeck Dashboard
2. Navigate to Connections and select your source
3. Under Advanced Source Configuration, enable Source Authentication
4. Select HMAC as the authentication method
5. Configure the HMAC settings:
  
  * Algorithm: SHA-256
  * Header: X-Hub-Signature
  * Encoding: Hex
  * Secret: Your Bitbucket webhook secret

### How Hookdeck verification works

When verification is enabled:

1. Hookdeck receives the webhook from Bitbucket
2. Hookdeck validates the `X-Hub-Signature` against the payload
3. Valid requests are forwarded to your endpoint with `x-hookdeck-verified: true`
4. Invalid requests are rejected and logged as "Verification Failed"

This means your application can trust any request from Hookdeck without implementing its own verification logic.

### Verifying Hookdeck signatures

For additional security, you can also verify that requests to your endpoint come from Hookdeck by checking the `x-hookdeck-signature` header:

```javascript
const crypto = require("crypto");

function verifyHookdeckSignature(req, hookdeckSecret) {
  const signature = req.get("x-hookdeck-signature");

  if (!signature) {
    return false;
  }

  const hmac = crypto.createHmac("sha256", hookdeckSecret);
  const expectedSignature = hmac.update(req.rawBody).digest("base64");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

```

Note: Hookdeck signatures are Base64-encoded, unlike Bitbucket's hex-encoded signatures.

## Troubleshooting common issues

### Signature mismatch

If signatures don't match, verify:

1. Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
2. Secret accuracy: Confirm the secret matches what's configured in Bitbucket
3. Encoding: Bitbucket uses hex encoding for signatures

### Missing signature header

If `X-Hub-Signature` is missing:

1. Verify a secret is configured for the webhook in Bitbucket
2. Check that your proxy or load balancer isn't stripping headers

### Webhook not firing

1. Check the webhook's Recent deliveries in Bitbucket settings
2. Verify your endpoint URL is publicly accessible
3. Ensure your server responds within Bitbucket's timeout window

## Conclusion

Securing Bitbucket webhooks requires verifying the HMAC signature on every incoming request. While you can implement this verification manually, [Hookdeck](https://hookdeck.com) simplifies the process by providing automatic signature verification, event management, and development tools, allowing you to focus on building your integration rather than infrastructure.

## Additional resources

* [Bitbucket Cloud Webhooks Documentation](https://support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/)
* [Bitbucket Event Payloads Reference](https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/)
* [Hookdeck Documentation](https://hookdeck.com/docs)
* [Hookdeck CLI](https://hookdeck.com/docs/cli)
* [Hookdeck Source Verification](https://hookdeck.com/docs/signature-verification)