Authentication
Every trigger request is authenticated by an HMAC-SHA256 signature computed with your product's shared secret. There is no bearer token — the signature both authenticates you and proves the body wasn't tampered with.
The signature header
Send your signature in the X-Elixpo-Signature header. It carries a timestamp and a v1 HMAC, comma-separated:
X-Elixpo-Signature: t=1718500000,v1=9f8a...e21ct— the unix time in seconds when you signed the request.v1— the hex-encoded HMAC-SHA256.
What you sign
The signed payload string is `${t}.${rawBody}` — the timestamp, a literal dot, then the exact JSON body bytes you send. The HMAC key is the product's shared secret.
import crypto from "node:crypto";
const secret = "YOUR_PRODUCT_SECRET";
// 1. Serialize the body ONCE. These are the exact bytes you will send.
const rawBody = JSON.stringify({ to: "user@example.com", variables: { name: "Ada" } });
// 2. Current unix time in seconds.
const t = Math.floor(Date.now() / 1000);
// 3. HMAC-SHA256 over `${t}.${rawBody}` with the product secret.
const v1 = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
// 4. Send the header alongside the SAME rawBody.
const signature = `t=${t},v1=${v1}`;Sign the bytes you send
401.Timestamp tolerance
Requests whose t is outside a 5-minute tolerance of server time are rejected with 400. Sign each request fresh at send time; don't cache signatures.
Secret rotation
When you roll a product's secret, the previous secret is still accepted for a short grace window. This lets you deploy the new secret without dropping in-flight requests. After the window closes, only the new secret works.
Why a request is rejected
401— bad or missing signature.400— invalid timestamp (outside tolerance) or invalid recipient.403— the webhook/product is disabled, or no signing secret is configured.
