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...
  • t is the unix-seconds timestamp when Medblocks signed the request.
  • v1 is 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.

server/webhook.ts
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.

FrameworkHow to read the raw body
Bun.serveawait req.text()
Next.js App Routerawait req.text() (do not call req.json())
Honoawait c.req.text()
Expressapp.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:

  1. Parse the Medblocks-Signature header into { t, v1 }.
  2. Reject if t is outside the tolerance window (default 5 minutes. See below).
  3. HMAC-SHA256 over ${t}.${rawBody} with the secret, constant-time compare against v1.
  4. JSON-parse the body; reject if the envelope shape is wrong.
  5. 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:

reasonWhat happenedWhat to do
missing_headerNo Medblocks-Signature on the request.Confirm the request is actually from Medblocks. Return 400.
malformed_headerHeader didn’t match t=<sec>,v1=<hex>.Something is rewriting the header. Check load balancers and proxies.
timestamp_expiredt outside the tolerance window.Fix clock drift on your server, or raise tolerance short-term.
signature_mismatchHMAC didn’t match.Wrong secret, or you’re not reading the raw body.
malformed_bodyBody 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:

  1. Deploy code that reads the new secret from a configurable secret store.
  2. Call mb.webhooks.rotateSecret(endpointId).
  3. Write the returned secret to 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.