> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bodyguard.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks quick start

> Build a webhook endpoint and verify each Bodyguard request with a Standard Webhooks library.

Bodyguard signs every webhook request, so you can check that it comes from Bodyguard and that nobody altered it. Signatures follow the [Standard Webhooks](https://www.standardwebhooks.com) specification, so you verify them with the official open-source libraries instead of writing the cryptography yourself.

## Prerequisites

* The signing secret of your webhook. It starts with `whsec_`. See [Configuration](https://docs.bodyguard.ai/en/api-reference/webhooks/configuration.md#signing-secret).
* A server that can expose a public HTTPS endpoint.
  * The endpoint must complete a valid TLS handshake, with a certificate from a trusted certificate authority. Self-signed certificates are not accepted.
  * The endpoint must answer within 10 seconds. Otherwise, the delivery is considered a failure.

## How signing works

Bodyguard signs each request as described in the [Verifying webhook authenticity](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md#verifying-webhook-authenticity) section of the Standard Webhooks specification. Bodyguard uses the symmetric signature scheme: `HMAC-SHA256` with your `whsec_` signing secret, and the `v1` signature identifier.

## Create your endpoint

Install the library for your language:

```bash JavaScript theme={null}
npm install standardwebhooks express
```

```bash Python theme={null}
pip install standardwebhooks flask
```

```bash Go theme={null}
go get github.com/standard-webhooks/standard-webhooks/libraries/go
```

Then create an endpoint that accepts `POST` requests and verifies each one. Pass the signing secret in the `BODYGUARD_WEBHOOK_SECRET` environment variable. For the content of `event.data`, see [Payloads › Event types](https://docs.bodyguard.ai/en/api-reference/reference/webhooks/payloads.md#event-types).

```javascript JavaScript theme={null}
const express = require("express");
const { Webhook } = require("standardwebhooks");

const wh = new Webhook(process.env.BODYGUARD_WEBHOOK_SECRET); // "whsec_..."
const app = express();

// Keep the raw body: the signature covers the exact bytes Bodyguard sent.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, req.headers);
  } catch (err) {
    return res.status(400).send("Invalid signature");
  }

  switch (event.type) {
    case "message.updated":
      console.log("Message updated:", event.data.id, event.data);
      break;
    case "author.updated":
      console.log("Author updated:", event.data.id, event.data);
      break;
  }

  // Any 2xx response acknowledges the delivery.
  res.sendStatus(200);
});

app.listen(8080, () => console.log("Listening on port 8080"));
```

```python Python theme={null}
import os

from flask import Flask, request
from standardwebhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["BODYGUARD_WEBHOOK_SECRET"])  # "whsec_..."
app = Flask(__name__)


@app.post("/webhook")
def webhook():
    # Use the raw body: the signature covers the exact bytes Bodyguard sent.
    try:
        event = wh.verify(request.get_data(), request.headers)
    except WebhookVerificationError:
        return "Invalid signature", 400

    if event["type"] == "message.updated":
        print("Message updated:", event["data"])
    elif event["type"] == "author.updated":
        print("Author updated:", event["data"])

    # Any 2xx response acknowledges the delivery.
    return "", 200


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

```go Go theme={null}
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"

	standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)

func main() {
	wh, err := standardwebhooks.NewWebhook(os.Getenv("BODYGUARD_WEBHOOK_SECRET")) // "whsec_..."
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
		// Use the raw body: the signature covers the exact bytes Bodyguard sent.
		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "Cannot read body", http.StatusBadRequest)
			return
		}

		if err := wh.Verify(body, r.Header); err != nil {
			http.Error(w, "Invalid signature", http.StatusBadRequest)
			return
		}

		var event struct {
			Type string          `json:"type"`
			Data json.RawMessage `json:"data"`
		}
		if err := json.Unmarshal(body, &event); err != nil {
			http.Error(w, "Invalid payload", http.StatusBadRequest)
			return
		}

		switch event.Type {
		case "message.updated":
			log.Printf("Message updated: %s", event.Data)
		case "author.updated":
			log.Printf("Author updated: %s", event.Data)
		}

		// Any 2xx response acknowledges the delivery.
		w.WriteHeader(http.StatusOK)
	})

	log.Println("Listening on port 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

> [!WARNING]
>
> Verify the raw request body, exactly as received. If a framework parses the JSON and you serialize it again, whitespace and key order can change and the signature check fails.

The [`webhook.validate`](https://docs.bodyguard.ai/en/api-reference/reference/webhooks/payloads#webhook-validate) event does not need special handling: it is signed like any other event, so your endpoint verifies it and answers with a `2xx` status code.

Run your endpoint:

```bash JavaScript theme={null}
BODYGUARD_WEBHOOK_SECRET="whsec_..." node index.js
```

```bash Python theme={null}
BODYGUARD_WEBHOOK_SECRET="whsec_..." python app.py
```

```bash Go theme={null}
BODYGUARD_WEBHOOK_SECRET="whsec_..." go run .
```

### Other languages

Standard Webhooks also provides libraries for Java and Kotlin, Rust, Ruby, PHP, C#, and Elixir. See the [list of libraries](https://github.com/standard-webhooks/standard-webhooks#libraries).

> [accordion] Verify a signature without a library
>
> If no library fits your stack, implement the verification described in the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md):
>
> 1. Read the `webhook-id`, `webhook-timestamp`, and `webhook-signature` [headers](https://docs.bodyguard.ai/en/api-reference/reference/webhooks/payloads.md#headers).
> 2. Reject the request if `webhook-timestamp` is too far from the current time (the official libraries use five minutes).
> 3. Build the signed content: `{webhook-id}.{webhook-timestamp}.{raw body}`.
> 4. Remove the `whsec_` prefix from your signing secret and base64-decode the rest. Compute the HMAC SHA256 of the signed content with this key, and base64-encode the result.
> 5. Compare it, in constant time, with each `v1,` signature in the `webhook-signature` header. The request is valid if one of them matches.

## Test your endpoint

When you register the webhook or change its URL, Bodyguard sends a [`webhook.validate`](https://docs.bodyguard.ai/en/api-reference/reference/webhooks/payloads#webhook-validate) event: if your endpoint doesn't answer with a `2xx` status code, the registration fails. To check your endpoint at any time, click **Send test event** in the dashboard.

## Process events safely

* **Answer quickly.** Return a `2xx` status code within 10 seconds, as soon as you have verified and stored the event, and do the heavy work afterwards. Any other response, or no response, is a [failure](https://docs.bodyguard.ai/en/api-reference/webhooks/handling-errors.md#response-handling) and triggers a retry.
* **Deduplicate with `webhook-id`.** Bodyguard retries failed deliveries with the same [`webhook-id`](https://docs.bodyguard.ai/en/api-reference/reference/webhooks/payloads#headers) header. Store the identifiers you have processed, and skip the ones you have already seen.
* **Keep the secret private.** Load it from the environment, and never log it.

## Next steps

> [card] [Configuration](https://docs.bodyguard.ai/en/api-reference/webhooks/configuration.md)
>
> Events, signing secret, contacts, and webhook statuses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.