Skip to main content
Every webhook delivery from OpenTrain is signed with the subscription’s secret (the whsec_... value returned once, when the subscription was created). Verifying the signature proves the delivery came from OpenTrain and wasn’t tampered with — never act on an unverified delivery. This scheme is shared by the Public API and the Platform API: one verifier works for both.

The Headers

The signature header has two comma-separated parts: The signed message is the timestamp, a literal ., and the raw request body bytes — exactly as received, before any JSON parsing.

The Verification Recipe

  1. Read the raw body bytes. Not JSON.parse-then-re-stringify — key ordering and whitespace differences will change the bytes and the verification will fail (or worse, falsely pass a forged body if you re-serialize attacker-controlled JSON).
  2. Parse t and v1 from X-OpenTrain-Signature.
  3. Reject if |now − t| exceeds your tolerance window (5 minutes is a good default) — this bounds replay attacks.
  4. Compute HMAC-SHA256(secret, "<t>.<rawBody>") and hex-encode it.
  5. Compare with v1 using a constant-time comparison.

Node.js / TypeScript

Wired into a minimal HTTP server that preserves the raw body:
In frameworks that parse JSON for you (Express with express.json(), Next.js API routes, Fastify), you must opt into the raw body — e.g. express.raw({ type: 'application/json' }) on the webhook route, or the verify callback that captures req.rawBody. Verifying a re-serialized body is the single most common webhook-verification bug.

Python

For example with Flask (request.get_data() returns the raw bytes):

Operational Rules

  • Respond 2xx within 10 seconds. Acknowledge immediately and do real work asynchronously. A slow handler is indistinguishable from a failing one and burns your retry budget.
  • Return 5xx (or time out) to request a retry. Deliveries retry up to 5 attempts with 1m/5m/30m/120m backoff. Return 2xx for deliveries you choose to skip — a 4xx still counts as a failure toward auto-disable.
  • Dedupe by X-OpenTrain-Delivery. Retries reuse the delivery ID; treat it as an idempotency key. (If you follow the trigger-then-poll pattern, duplicates are naturally harmless.)
  • A 400 on signature failure is fine — but log it. Repeated signature failures usually mean a body-parsing middleware is mangling the raw bytes, or you rotated the subscription (new secret) without updating your handler.
  • Secrets are per-subscription. If you run multiple subscriptions, key your secrets by webhook ID. Deleting and re-creating a subscription issues a new secret.

Testing Your Verifier

You don’t need to wait for a real event. Compute a signature yourself and POST it:
Your handler should accept this, reject a tampered body (change one character), and reject a stale timestamp (T=$(($(date +%s) - 3600))).

Stay in Sync

Subscribing, the event catalog, retries, and the agent loop around deliveries.

Platform Webhooks

The platform-side delivery surface — same signature scheme, plus redelivery.