Skip to main content
Your endpoint’s URL is public, so anyone can send it a POST. Verify the signature on each request so your handler only acts on deliveries that came from Brudcast.
Before you start: you need the endpoint’s signing secret, which is shown once when you create the endpoint or rotate its secret. Your framework must also let you read the request body as raw bytes, before any JSON parsing.

How Brudcast signs a request

X-Webhook-Signature is:
  • an HMAC-SHA256
  • keyed with the endpoint’s secret, used as-is, as a UTF-8 string
  • computed over the raw request body, byte for byte
  • written as lowercase hexadecimal, 64 characters, with no prefix such as sha256=
No timestamp goes into the signature, and there’s no separate timestamp header. The time is inside the body, as timestamp, so the signature covers it too.
The secret looks like hex, but don’t hex-decode it. Brudcast uses the 64-character string itself as the HMAC key.

Verify each request

1

Read the raw body

Capture the exact bytes Brudcast sent. If a JSON parser reads the body first, and you then serialize it again, key order and spacing can change and the signature won’t match.
2

Compute the expected signature

Calculate HMAC-SHA256 of the raw body, keyed with your secret, and hex-encode it in lowercase.
3

Compare in constant time

Compare your value with X-Webhook-Signature using a constant-time function. A normal string comparison stops at the first difference, and the time it takes can leak how much of a forged signature was right. If the values differ, return 401 and stop.
4

Check the timestamp

Parse the body and reject deliveries whose timestamp is older than your tolerance. See Choose a tolerance below.
5

Skip repeats

Look up delivery_id in your store. If you’ve already processed it, return 200 and do nothing. Otherwise, record it along with the event.
6

Respond quickly

Return a 2xx once the event is stored, and do the real work afterwards. If your handler takes longer than the endpoint’s timeout, Brudcast treats the attempt as failed and sends the same delivery again.

Code

Each example reads the secret from BRUDCAST_WEBHOOK_SECRET and listens on /webhooks/brudcast.

Choose a timestamp tolerance

timestamp is the time Brudcast recorded the delivery, and it doesn’t change. A retry, or a resend you trigger by hand, arrives with the original value. A tight window, such as five minutes, rejects legitimate retries on endpoints with many attempts, and any resend you make later. Your delivery_id check is what stops a captured request from being replayed. The timestamp limits how long you need to remember those IDs. Pick a tolerance that covers your endpoint’s retry schedule, and keep processed IDs for at least that long. The full schedule is in Retries and deliveries. The examples above use 24 hours.

Troubleshooting

Why: usually the body was changed before you hashed it. A JSON middleware may have parsed it, a framework may have decoded it, or a proxy may have re-encoded it. Less often, the secret is wrong: it has a trailing newline from an environment file, it was hex-decoded, or it’s from before a rotation.Fix: hash the raw bytes, as in the examples. Print the length of the secret you load: it should be 64. If you’ve lost the secret, rotate it and update your server.
Why: Brudcast retries only when it gets no response. A 401 is a response, so the delivery is marked delivered.Fix: fix the verification, then reprocess what you missed. Open each delivery in Delivery Logs. Its Request Body holds the event’s data.
Why: your server took longer than the endpoint’s timeout, so Brudcast treated the attempt as failed and tried again with the same body.Fix: skip any delivery_id you’ve already processed, and return 200 before doing slow work.

Payload reference

The headers and fields you’re verifying.

Create an endpoint

Point an endpoint at your own machine while you build.