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.

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:
| Call | What it gives you | For a signed webhook |
|---|---|---|
c.req.text() | The raw request body, as a string | Verify it, then JSON.parse the same string |
c.req.arrayBuffer() | The request body as an ArrayBuffer | Also works: verifyWebhook accepts an ArrayBuffer |
c.req.json() | A parsed application/json body | Don't verify this: a parsed-and-reserialized object doesn't verify |
c.req.raw | The raw Request object | Pass its headers to verifyWebhook |
cloneRawRequest(c.req), imported from hono/request | A clone of the raw Request, even after validators or HonoRequest methods consumed the body | Use 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?
401when the check fails, before you parse anything.204after you've recorded the event, with the slow work done afterwards. Sume allows 10 seconds per attempt and retries a slow endpoint, so dedupe onrequest_idorjob_id: retries repeat them.204for 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
maxSizeabove 1 MiB. Sume inlines run receipts up to 1 MiB and sendspayload: nullwith aresult_urlabove that. - Without the SDK, the check is the same: HMAC-SHA256 over
<timestamp>.<raw_body>, everysume-v1=entry compared in constant time, stale timestamps and an empty secret refused. Webhook security best practices lists the rules.
Sources
- Verifying webhooks
- TypeScript SDK
- Run webhooks
- Webhooks
- Hono: HonoRequest (read 2026-09-28)
- Hono: Context (read 2026-09-28)
- Hono: Stripe Webhook example (read 2026-09-28)
- Hono: Adapter Helper (read 2026-09-28)
- Hono: Body Limit Middleware (read 2026-09-28)
- Hono: docs home (read 2026-09-28)
- Hono: Node.js (read 2026-09-28)
Related posts
More in Integrations
- IntelliJ GitHub Copilot MCP: add Sume in mcp.json
Add an MCP server to GitHub Copilot in IntelliJ IDEA: Agent mode, Add MCP Tools, and a servers entry for Sume's hosted MCP with an API-key header.
- Fetch timeout in JavaScript: AbortSignal.timeout and retry
fetch() has no timeout option. Pass signal: AbortSignal.timeout(ms), catch the TimeoutError, and retry network errors, 429 and 5xx with backoff.
- Jenkins build periodically: cron syntax and the H symbol
Jenkins' Build periodically takes 5 cron fields plus H, a hash of the job name that spreads start times: H 20 * * * runs once in the 8 p.m. hour.
- Kilo Code MCP server: add Sume in kilo.jsonc
Add Sume's hosted MCP server to Kilo Code under the mcp key in kilo.jsonc: type remote, the Sume URL, and an API key header or an OAuth sign-in.
Written by Sume