Verify & secure deliveries

Use your signing secret to confirm requests come from Ziptax
Available onProEnterprise

Because your endpoint is a public URL, anyone who discovers it could send it POST requests. Ziptax gives each account a signing secret so you can verify that an incoming delivery is genuinely from Ziptax.

Your signing secret

  • Each account has a single signing secret, shared across all of that account’s endpoints.
  • It is prefixed with whsec_.
  • From Develop > Events, you can reveal the current secret and rotate it. Rotating replaces the secret for the whole account, so update every endpoint that uses it at the same time.

Store your signing secret securely and treat it like a password. Keep it out of source control and never expose it in client-side code. If you believe it has leaked, rotate it from Develop > Events.

How signing keys work

A signing key lets you prove that a request really came from the sender and was not tampered with in transit. The pattern is the same across most webhook providers:

The sender signs each delivery

Before sending, the provider computes a hash-based message authentication code (HMAC) over the exact request body using the shared signing secret, then attaches the result as a signature header on the POST.

Your endpoint recomputes the signature

On receipt, you compute an HMAC over the raw request body (the exact bytes you received, before any JSON parsing) using the same signing secret.

You compare the two signatures

If your computed signature matches the one in the header, the request is authentic and unmodified. If it does not match, reject the request. Because you and the sender are the only parties who know the secret, no one else can forge a valid signature.

Verifying a request

Ziptax sends the signature in the X-Signature header. It is a hex-encoded HMAC-SHA256 of the raw request body, keyed with your account’s signing secret. To verify a delivery, read the raw body, recompute the HMAC with your secret, and compare it against X-Signature using a constant-time comparison. Always compare with a timing-safe function so an attacker cannot learn the correct signature byte by byte.

1import express from "express";
2import crypto from "crypto";
3
4const SIGNING_SECRET = process.env.ZIPTAX_SIGNING_SECRET; // "whsec_..."
5
6const app = express();
7
8// Capture the RAW body. Verification must run on the exact bytes received,
9// not on a re-serialized JSON object.
10app.use(express.raw({ type: "application/json" }));
11
12function isValidSignature(rawBody, signature) {
13 const expected = crypto
14 .createHmac("sha256", SIGNING_SECRET)
15 .update(rawBody)
16 .digest("hex");
17
18 const a = Buffer.from(expected);
19 const b = Buffer.from(signature || "");
20 return a.length === b.length && crypto.timingSafeEqual(a, b);
21}
22
23app.post("/webhooks/ziptax", (req, res) => {
24 const signature = req.get("X-Signature"); // Ziptax's signature header
25
26 if (!isValidSignature(req.body, signature)) {
27 return res.sendStatus(401);
28 }
29
30 const { event, data } = JSON.parse(req.body.toString());
31 // ...handle the verified event...
32
33 res.sendStatus(200);
34});
35
36app.listen(3000);

Verify against the raw request body, not a re-serialized object. Frameworks that auto-parse JSON can reorder keys or change whitespace, which changes the HMAC and breaks verification. Capture the raw bytes first, verify, then parse.

Next step