Webhook signatures
Verify that each webhook delivery came from Workwae, and rotate an endpoint's signing secret without downtime.
Every webhook delivery is signed with its endpoint's secret. Verify the signature before trusting the body, and rotate the secret with a grace period when you need a new one.
Verify the signature
Signatures follow Standard Webhooks, so any Standard Webhooks library verifies them:
- The algorithm is HMAC-SHA256, keyed with the base64-decoded part of the secret after
whsec_. - The signed content is
{webhook-id}.{webhook-timestamp}.{body}. webhook-signaturemay carry several space-separatedv1,<base64>signatures; one match is enough.
Verify the raw body
Verify the request body exactly as received, before parsing it. Re-serialized JSON does not match the signature.
parseWebhook from the TypeScript SDK checks the signature and the timestamp (5 minutes of tolerance), then parses the body. verifyWebhook only returns a boolean.
import { parseWebhook, type WebhookEvent } from "@workwae/sdk";
export async function POST(request: Request): Promise<Response> {
const body = await request.text();
let event: WebhookEvent;
try {
event = await parseWebhook(process.env.WORKWAE_WEBHOOK_SECRET!, request.headers, body);
} catch {
return new Response("Invalid webhook", { status: 400 });
}
if (await alreadyHandled(event.id)) return new Response(null, { status: 204 });
if (event.known && event.type === "message.created") {
await enqueue(event.id, event.data.message.subject);
}
return new Response(null, { status: 204 });
}What parseWebhook returns:
event.createdAtis aDate.- For a known type, checking
event.typenarrowsevent.datato that type's data. - A type added after your SDK build arrives with
known: falseand uncheckeddata.
Rotate the secret
POST /v1/webhooks/{endpointId}/rotate-secret (or Rotate secret in the console) returns a new secret, which signs every delivery from then on.
curl -X POST "https://api.workwae.com/v1/webhooks/$ENDPOINT_ID/rotate-secret" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "graceSeconds": 86400 }'During the grace period the old secret keeps signing too:
graceSeconds | Effect |
|---|---|
| Omitted | 24 hours of grace. |
| Up to 604800 (7 days) | Deliveries carry two signatures, v1,<new> v1,<old>; a receiver holding either secret accepts them. |
0 | The old secret stops signing at once. |
previousSecretExpiresAt, in the response and on the endpoint, says when the old secret stops signing. Switch your receiver to the new secret before then.verifyWebhook,parseWebhookand every Standard Webhooks library accept a delivery when any of its signatures matches the secret they are given.- A rotation during another rotation's grace ends that grace: only the secret it replaces keeps signing.