verifyWebhook async: forget await and you get a promise

verifyWebhook in @sume-com/sdk is async. Without await, a Promise is truthy, so your signature check never rejects. The await, raw body and replay rules.

4 min readSume
All posts

verifyWebhook in @sume-com/sdk is async, so you have to await it. Without the await you hold a Promise, and a Promise is always truthy in JavaScript, so if (!ok) never fires and a forged delivery passes your check. It is one of four rules the docs say decide whether verification works.

Why is verifyWebhook async?

The implementation uses WebCrypto rather than node:crypto. That keeps the package importable from Workers, Deno and bundlers that refuse node: specifiers. The cost is that the check cannot be synchronous.

What does the correct handler look like?

This is the docs shape: read the raw text first, await the check, and reject on false.

import { verifyWebhook } from "@sume-com/sdk";

export async function POST(request: Request) {
  const body = await request.text(); // raw, before any JSON.parse

  // Without await, ok is a Promise and `!ok` is always false.
  const ok = await verifyWebhook({
    body,
    headers: request.headers,
    secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET!,
  });
  if (!ok) return new Response("bad signature", { status: 401 });

  return new Response(null, { status: 204 });
}

What does it return on a bad delivery?

It returns false rather than throwing. A missing header, a garbage timestamp and a wrong signature are all just failed verification, so there is one thing to branch on and no try/catch. That is also why the missing await is silent: nothing throws to tell you.

What else has to be right?

Set the secret from SUME_COM_WEBHOOK_SIGNING_SECRET, and refuse to start if it is empty.

Verification rules from the Sume SDK docs, read 2026-09-29.
RuleDetail
Raw bodyA parsed-and-reserialized object does not verify
Replay windowtoleranceSeconds defaults to 300; 0 skips the timestamp check
ComparisonConstant-time, with the replay window enforced before the HMAC is computed
No clientverifyWebhook takes no client and makes no request

How do I catch a missing await in testing?

Send your route a request with a deliberately wrong signature and check that it answers 401. With the await missing, that request would pass. Then use Send test, which posts a dummy signed webhook.test payload, to confirm a genuine signature is accepted.

Do the framework details change anything?

The raw body rule is where frameworks bite. In Express, mount express.raw({ type: "application/json" }) on the webhook route only. In the Next.js App Router, call await request.text() before anything else. During a secret rotation the signature header can carry two sume-v1= entries, and verifyWebhook in @sume-com/sdk 0.2.0 already handles that.

Is a missing await ever caught by a linter?

That depends on your setup, and the Sume docs do not cover lint rules. A no-floating-promises style check in TypeScript flags an unawaited promise as a statement, but const ok = verifyWebhook(...) followed by if (!ok) is a use of the value, so it can slip through. The reliable guard is a test with a bad signature that expects 401, plus reading verifyWebhook as the one call in the handler that must always carry an await.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume