Hono webhook: verify the signature on the raw body

Read the raw body with c.req.text(), verify the HMAC signature, then JSON.parse that string and answer 204. Works on Workers, Bun, Deno and Node.

5 min readSume
All posts

To handle a webhook in Hono, read the raw body with await c.req.text() before anything parses it, verify the signature over that exact string, then JSON.parse the same string and answer quickly. For Sume's webhooks, verifyWebhook from @sume-com/sdk does the check with WebCrypto, so one Hono route runs on Cloudflare Workers, Bun, Deno and Node.js.

Hono facts come from its HonoRequest, Context, Adapter Helper and Body Limit docs and its Stripe Webhook example; Sume facts come from Verifying webhooks, the TypeScript SDK page and Run webhooks. All were read on 2026-09-28. Sume ships no Hono middleware: the route below is plain Hono calling the SDK. Runtime-specific receivers without a framework are in Cloudflare Workers webhook to a Queue and Supabase Edge Function webhook.

How do I get the raw request body in Hono?

Call c.req.text(). Hono's Stripe example says signature verification needs the raw request body, unmodified, and that in Hono you get it with context.req.text(). The request object has other readers too, and only some of them keep the bytes you need:

From Hono's HonoRequest docs and Stripe Webhook example and Sume's Verifying webhooks page, read 2026-09-28.
CallWhat it gives youFor a signed webhook
c.req.text()The raw request body, as a stringVerify it, then JSON.parse the same string
c.req.arrayBuffer()The request body as an ArrayBufferAlso works: verifyWebhook accepts an ArrayBuffer
c.req.json()A parsed application/json bodyDon't verify this: a parsed-and-reserialized object doesn't verify
c.req.rawThe raw Request objectPass its headers to verifyWebhook
cloneRawRequest(c.req), imported from hono/requestA clone of the raw Request, even after validators or HonoRequest methods consumed the bodyUse it when middleware read the body first

How do I verify a Sume webhook in a Hono route?

Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends sume-v1=<hex> in x-sume-webhook-signature. verifyWebhook takes the raw body, the headers and your signing secret, and returns false rather than throwing on a malformed delivery. It compares in constant time and enforces the replay window, 300 seconds by default, before it computes the HMAC. It's async, so await it.

Read the secret with Hono's env(c), then route on event and dedupe: on request_id for run webhooks, on job_id for job webhooks.

import { Hono } from "hono";
import { env } from "hono/adapter";
import { verifyWebhook } from "@sume-com/sdk";

type Env = { SUME_COM_WEBHOOK_SIGNING_SECRET: string };
const app = new Hono();

app.post("/hooks/sume", async (c) => {
  const body = await c.req.text(); // raw, before any JSON.parse
  const ok = await verifyWebhook({
    body,
    headers: c.req.raw.headers,
    secret: env<Env>(c).SUME_COM_WEBHOOK_SIGNING_SECRET,
  });
  if (!ok) return c.text("bad signature", 401);

  const event = JSON.parse(body); // the verified string, never re-serialized
  if (event.event === "format.run.terminal") await recordOnce(event.request_id, event);
  else if (event.event?.startsWith("job.")) await recordOnce(event.job_id, event);
  return c.body(null, 204); // fast 2xx, unknown events included
});

export default app;

Does the same route run on Workers, Bun, Deno and Node.js?

Yes. Hono says it works on any JavaScript runtime, including Cloudflare Workers, Deno, Bun and Node.js, and that the same code runs on all platforms. On Node.js, Hono's guide runs the app through its Node.js adapter: serve(app) from @hono/node-server. @sume-com/sdk needs fetch and WebCrypto: Node 18+, Bun, Deno or Cloudflare Workers. verifyWebhook uses WebCrypto rather than node:crypto, which is what keeps it importable from Workers and Deno. Hono's env(c) reads process.env on Node.js and Bun, Deno.env on Deno, and the Worker's bindings, which include secrets, on Cloudflare.

What should the route answer, and when?

  • 401 when the check fails, before you parse anything.
  • 204 after you've recorded the event, with the slow work done afterwards. Sume allows 10 seconds per attempt and retries a slow endpoint, so dedupe on request_id or job_id: retries repeat them.
  • 204 for event types you don't recognize. Sume's docs say that stops a newly added event type from becoming a 500 and a retry storm.
  • If you add Hono's Body Limit middleware to the route, set maxSize above 1 MiB. Sume inlines run receipts up to 1 MiB and sends payload: null with a result_url above that.
  • Without the SDK, the check is the same: HMAC-SHA256 over <timestamp>.<raw_body>, every sume-v1= entry compared in constant time, stale timestamps and an empty secret refused. Webhook security best practices lists the rules.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume