OdyDocs

Verifying signatures

Verify the Standard Webhooks HMAC-SHA256 signature on every Ody delivery, in Node, Python, or any language.

Your webhook URL is public, so anyone can POST to it. Every Ody delivery is signed with the endpoint's secret. Verify the signature before you trust the payload.

#The headers

HeaderExampleMeaning
webhook-idevt_4f9b2c0d…The event ID (same as the body's id)
webhook-timestamp1792087542Unix seconds when this attempt was signed
webhook-signaturev1,K5oZfz…=v1, followed by the base64 HMAC-SHA256 signature

Deliveries also carry content-type: application/json and user-agent: Ody-Webhooks/1.0.

#The algorithm

Text
key       = base64_decode( secret without the "whsec_" prefix )
message   = webhook-id + "." + webhook-timestamp + "." + raw_request_body
signature = "v1," + base64( HMAC_SHA256(key, message) )

Then:

  1. Compare your signature with the header value using a constant-time comparison. The header may hold several space-separated signatures; accept if any one matches.
  2. Reject the request if webhook-timestamp is more than 5 minutes from your clock. This stops replays.
Use the raw body

Sign the bytes exactly as received. If a framework parses the JSON and you re-serialise it, whitespace and key order change and the signature won't match. In Express, use express.raw() on this route. In Next.js route handlers, use await req.text().

#Code

import express from "express";
import { verifyWebhookSignature } from "@ody/sdk";

const app = express();

app.post("/ody/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWebhookSignature(process.env.ODY_WEBHOOK_SECRET!, req.headers, req.body)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.status(200).end();           // acknowledge fast…
  void handleEvent(event);         // …then do the work
});

Because Ody follows Standard Webhooks, the official standardwebhooks libraries (JavaScript, Python, Go, Ruby, PHP, Java, Rust, C#…) verify Ody deliveries unchanged.

#Test it

Send yourself a signed ping:

Shell
curl -X POST https://api.ody.co/v1/api/webhooks/$WEBHOOK_ID/test \
  -H "Authorization: Bearer $ODY_API_KEY"

"delivered": true means your endpoint returned 2xx. If you get 401 from your own verifier, check that you're using the raw body and the right endpoint's secret.

#Rotating secrets

POST /webhooks/:id/rotate issues a new secret, and the old one stops working immediately. To rotate without dropping events:

  1. Rotate, and put the new secret in your config next to the old one.
  2. Make your verifier accept a signature from either secret.
  3. Once the new secret is deployed everywhere, remove the old one.

Rotate straight away if a secret might have leaked.

Last updated Wed Oct 14 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us