Check your Sume webhook secret at boot: 64 hex and a match

A Node startup check for a Sume webhook receiver: the secret must be 64 hex characters, non-empty, and equal to GET /v1/webhooks/signing-secret.

4 min readSume
All posts

Make the receiver refuse to start unless SUME_COM_WEBHOOK_SIGNING_SECRET is 64 hex characters, and, if you want a stronger check, equals the secret that GET /v1/webhooks/signing-secret returns. A missing or placeholder secret is the common cause of 401 bad signature on every delivery, and an empty one is worse: an HMAC with an empty key is trivial to forge. Failing at boot costs seconds. Failing in production costs retried deliveries.

What the endpoint returns

The signing-secret read needs an API key with the account:read scope. The OpenAPI spec describes the data object.

GET /v1/webhooks/signing-secret, from the OpenAPI spec read 2026-10-08
FieldMeaning
scopeworkspace:{id} for a team key, user:{id} for a personal key
versionDerivation version of the secret
fingerprintNon-reversible fingerprint, equal to x-sume-webhook-secret-fingerprint on deliveries
secretThe signing secret, 64 hex characters

The boot check

Save this as check-secret.mjs and run it before the server starts, for example in a container entrypoint. It exits with 1 for a bad format, 2 for a failed read and 3 for a mismatch. On a mismatch it prints only the current fingerprint, never a secret, which makes it safe for CI logs.

import { timingSafeEqual } from "node:crypto";

const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!/^[0-9a-f]{64}$/i.test(secret)) {
  console.error("SUME_COM_WEBHOOK_SIGNING_SECRET must be 64 hex characters");
  process.exit(1);
}
const res = await fetch("https://api.sume.com/v1/webhooks/signing-secret", {
  headers: { "x-api-key": process.env.SUME_API_KEY ?? "" }, // needs account:read
});
if (!res.ok) {
  console.error("could not read the signing secret:", res.status);
  process.exit(2);
}
const { secret: current, fingerprint } = (await res.json()).data;
const a = Buffer.from(secret);
const b = Buffer.from(current);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
  console.error(`env secret is not the current one; current fingerprint ${fingerprint}`);
  process.exit(3);
}
console.log(`secret ok, fingerprint ${fingerprint}`);

Notes

If the check passes and signatures still fail, the usual cause is the body: verify against the raw bytes, not a re-serialized object.

  • During a rotation, the fingerprint on deliveries names the new secret. Install the new secret on your receiver before the 24-hour overlap ends.
  • Two members of the same team share one verifier, because a team key uses a workspace-scoped secret.
  • Keep the account:read key out of the receiver itself. Run this check in the deploy pipeline, and give the running service only the signing secret.

When to run it

Run it at deploy time and again after every rotation. A rotation produces a new secret, and the fingerprint printed on a mismatch tells you whether the environment still holds the old value. Because the old secret stays valid for 24 hours, a mismatch right after a rotation is a reminder to update the environment, not an outage.

The check compares bytes with timingSafeEqual only out of habit: this runs once, outside the request path, so timing is not a real concern here. The request path is another matter, and verifyWebhook already does a constant-time comparison for each delivery.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume