On this page
Verify every delivery before parsing or acting on its JSON. Myaza signs the exact request bytes with the secret assigned to that endpoint.
Signed headers
| Header | Example | Required handling |
|---|---|---|
X-Myaza-Signature-V2 | t=1786441200,v1=8df1… | Parse the Unix timestamp and every v1 candidate. Multiple v1 values are present during secret rotation. |
X-Myaza-Timestamp | 1786441200 | Same signed timestamp, exposed separately for diagnostics. |
X-Myaza-Replay-Window | 300 | Endpoint replay window in seconds. Reject signatures outside it. |
X-Myaza-Event | risk.signal.created | Event type for routing. The signed JSON remains authoritative. |
X-Myaza-Event-Id | evt_… | Stable business-deduplication key. |
X-Myaza-Delivery | del_… | Endpoint delivery record ID. |
X-Myaza-Correlation-Id | corr_… | Optional end-to-end correlation value when the producer supplied one. |
X-Myaza-Webhook-Version | v2 | Endpoint body contract. |
X-Myaza-Signature | sha256=… | Exact-body compatibility signature. New integrations should verify V2. |
Verification algorithm
- Read the raw HTTP request body as bytes.
- Parse
tand allv1values fromX-Myaza-Signature-V2. - Reject a missing, invalid, too-old or implausibly future timestamp.
- Compute lowercase hex
HMAC-SHA256("<t>.<rawBody>", endpointSecret). - Compare the expected value with every supplied
v1using a constant-time comparison. - Only after a match, parse the JSON, deduplicate on
id, enqueue it durably and return2xx.
Do not re-serialise parsed JSON before verification. Whitespace and property order change the signed bytes.
Node.js example
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/webhooks/myaza", express.raw({ type: "application/json" }), (req, res) => {
const parts = String(req.header("X-Myaza-Signature-V2") || "").split(",");
const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2);
const candidates = parts
.filter((part) => part.startsWith("v1="))
.map((part) => part.slice(3));
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send("Expired signature");
}
const expected = crypto
.createHmac("sha256", process.env.MYAZA_WEBHOOK_SECRET)
.update(`${timestamp}.`)
.update(req.body)
.digest("hex");
const valid = candidates.some((candidate) =>
candidate.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected))
);
if (!valid) return res.status(401).send("Invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// Persist event.id before dispatching asynchronous business work.
res.status(200).send("OK");
});Secret rotation
During the configured grace period Myaza signs with the active secret and every unexpired grace secret. The V2 header therefore carries more than one v1 value. Keep both endpoint secrets available and accept the request if either expected signature matches. Remove the old secret only after its dashboard grace period ends.
Failure responses
Return 401 for missing, stale or invalid signatures. Return 2xx only after the event is durably accepted. Other non-2xx responses and timeouts are retried according to the delivery schedule.