Signatures
The Medblocks-Signature header, constructEvent, the raw-body trap, and the five failure reasons.
Every webhook delivery from Medblocks includes a Medblocks-Signature header. Verifying it before trusting the payload is non-negotiable. Without verification, anyone who knows your endpoint URL can fire fake events at it.
The Header Format
Medblocks-Signature: t=1714060800,v1=8d1b3c...tis the unix-seconds timestamp when Medblocks signed the request.v1is the hex-encoded HMAC-SHA256 of${t}.${rawBody}keyed by your endpoint’s signing secret.
Medblocks.webhooks.constructEvent parses this header, checks the timestamp window, recomputes the HMAC, and constant-time compares.
Verifying With The SDK
constructEvent is a static method on Medblocks.webhooks. It is pure. No network, no SDK instance needed.
import { Medblocks, MedblocksSignatureError } from "@medblocks/connect";
async function handle(req: Request): Promise<Response> {
const rawBody = await req.text();
const signature = req.headers.get("Medblocks-Signature");
try {
const event = await Medblocks.webhooks.constructEvent(
rawBody,
signature,
process.env.MEDBLOCKS_WEBHOOK_SECRET!,
);
// event is now a typed WebhookEvent - safe to act on
return new Response("ok", { status: 200 });
} catch (err) {
if (err instanceof MedblocksSignatureError) {
console.warn("signature failed", err.reason);
return new Response("bad signature", { status: 400 });
}
throw err;
}
}The Raw-Body Trap
The signature is computed over the raw request body. If your framework parses the JSON before you see it, the re-serialized string will not match. Different whitespace, different key order, different escaping rules. The HMAC fails even though the payload is genuine.
| Framework | How to read the raw body |
|---|---|
| Bun.serve | await req.text() |
| Next.js App Router | await req.text() (do not call req.json()) |
| Hono | await c.req.text() |
| Express | app.use(express.raw({ type: "application/json" })). Body is a Buffer, decode it |
If you mix middleware that parses JSON globally (express.json(), Fastify’s auto-parsing, etc.) put the webhook route before the JSON parser so it sees the unparsed body.
What constructEvent Does
Internally, in order:
- Parse the
Medblocks-Signatureheader into{ t, v1 }. - Reject if
tis outside the tolerance window (default 5 minutes. See below). - HMAC-SHA256 over
${t}.${rawBody}with the secret, constant-time compare againstv1. - JSON-parse the body; reject if the envelope shape is wrong.
- Return the typed
WebhookEvent.
Any step’s failure throws MedblocksSignatureError with a reason describing which one.
Timestamp Tolerance
Defaults to 300 seconds (5 minutes). Raise it only if your clock drift really is larger than that. Usually a sign of an NTP problem worth fixing rather than papering over.
const event = await Medblocks.webhooks.constructEvent(
rawBody,
signature,
secret,
{ tolerance: 600 }, // 10 minutes - only if you really need it
);The tolerance window protects against replay: an attacker who captures a single delivery cannot replay it forever.
Accepting Bytes Instead Of A String
constructEvent accepts string or Uint8Array. Use the byte form when your runtime exposes the body as bytes already (e.g., Express’s raw middleware).
const rawBytes: Uint8Array = req.body; // from express.raw()
const event = await Medblocks.webhooks.constructEvent(rawBytes, signature, secret);Failure Reasons
MedblocksSignatureError.reason is one of:
reason | What happened | What to do |
|---|---|---|
missing_header | No Medblocks-Signature on the request. | Confirm the request is actually from Medblocks. Return 400. |
malformed_header | Header didn’t match t=<sec>,v1=<hex>. | Something is rewriting the header. Check load balancers and proxies. |
timestamp_expired | t outside the tolerance window. | Fix clock drift on your server, or raise tolerance short-term. |
signature_mismatch | HMAC didn’t match. | Wrong secret, or you’re not reading the raw body. |
malformed_body | Body wasn’t valid JSON in the event envelope. | Confirm nothing between Medblocks and your handler is rewriting the body. |
Always log err.reason. It’s the fastest way to tell what’s wrong.
Rotating The Secret
mb.webhooks.rotateSecret(id) returns a new signing secret and the old one stops verifying immediately. There is no grace period. Sequence to avoid downtime:
- Deploy code that reads the new secret from a configurable secret store.
- Call
mb.webhooks.rotateSecret(endpointId). - Write the returned
secretto that store.
Any delivery in flight during step 2 will fail verification with signature_mismatch. Medblocks retries those, so set up retries to absorb the gap.
Verifying In Other Languages
For non-TypeScript receivers, the algorithm is the same. HMAC-SHA256 over ${t}.${rawBody} keyed by the secret, hex-compare against v1, reject if t is outside the tolerance window. The SDK is the canonical implementation; if you need a port to Python, Go, Ruby, etc., open a support ticket and we’ll publish a reference snippet.
Related
- Quickstart. Register, receive, verify in 5 steps.
- Events Catalog. Payload shape per
event.type. - SDK · Errors · Webhook Signature Errors.
- Managing Endpoints · Rotate Secret.
