Authentication

Verify that a delivery came from Onetap before you trust a byte of it.

ProposedNot yet callable

This documents a design under review. Endpoints, payloads and field names may change before release.

Verifying a delivery

The signed payload is the timestamp, a full stop, and the raw request body. Compute the HMAC with your endpoint secret and compare in constant time.

import crypto from "node:crypto";

export function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string]),
  );

  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

Verify against the raw body, before any JSON parsing or middleware reserialises it. Re-encoded JSON produces a different signature and every delivery will fail verification. In Express that means express.raw() on the webhook route, not express.json().

Replay protection

A valid signature stays valid forever, so a captured request could be replayed. Reject deliveries whose timestamp is too old.

const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return reject(); // older than five minutes

Five minutes is generous enough to survive clock skew and a slow retry, and short enough that a captured request is worthless by the time it could be replayed.

Rolling a secret

Rolling issues a second secret while the first keeps working, so you can deploy without dropping deliveries.

POST /v1/webhook_endpoints/whe_8f21c0/roll_secret

{ "expires_old_in": "24h" }

During the overlap, accept a delivery if it verifies against either secret. Remove the old one once the window closes.