Verify each render event before your app collects a file or sends it to a customer. A valid signature shows that the body matches a message signed with your webhook secret. Store the event ID as well, so a repeat delivery doesn't repeat the work. thirds.ai uses a 300-second signature freshness window.
An invoice is one useful example. Your app creates the PDF once, then collects it when the signed result arrives. Verification protects the step between creating the file and using it.

How do webhooks handle authentication?
thirds.ai signs each event with HMAC-SHA256. The thirds-signature header carries t=UNIX_SECONDS,v1=HEX_SIGNATURE. The signed input is the timestamp text, a dot, and the exact body bytes. Your receiver uses the signing secret returned when you register the destination. Your account API key is separate. You use it to read the finished job.
Read the raw bytes before you parse JSON. Parsing and encoding the body again can change its spaces and key order. Check the timestamp against your clock, compute the signature, and use a constant-time comparison. The webhook guide has the complete signing contract and maintained Node.js and Python functions.
How do you verify a signature?
Use the same verifier functions as the documentation. This runnable check creates a signed sample, then tries a changed body and an old timestamp. It needs Python 3 and Node.js. It sends no event, starts no render, and needs no account secret.
Make an empty folder. Open the webhook guide and copy the Node.js function into verify-third.mjs. Copy the Python function into verify_third.py in the same folder.
Save this Node.js check as check-signature.mjs:
import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { verifyThirdsSignature } from "./verify-third.mjs";
const body = Buffer.from('{"id":"local-signature-check"}');
const secret = "local-check-only";
const stamp = "1800000000";
const digest = createHmac("sha256", secret)
.update(`${stamp}.`)
.update(body)
.digest("hex");
const header = `t=${stamp},v1=${digest}`;
const cases = [
[body, header, Number(stamp), true],
[Buffer.from("{}"), header, Number(stamp), false],
[body, header, Number(stamp) + 301, false],
[body, "", Number(stamp), false],
];
for (const [raw, value, now, expected] of cases) {
assert.equal(verifyThirdsSignature(raw, value, [secret], now), expected);
}
console.log("signature_checks_passed");
Save this Python check as check_signature.py:
import hashlib
import hmac
from verify_third import verify_thirds_signature
body = b'{"id":"local-signature-check"}'
secret = "local-check-only"
stamp = "1800000000"
digest = hmac.new(secret.encode(), stamp.encode() + b"." + body, hashlib.sha256).hexdigest()
header = f"t={stamp},v1={digest}"
cases = [
(body, header, int(stamp), True),
(b"{}", header, int(stamp), False),
(body, header, int(stamp) + 301, False),
(body, "", int(stamp), False),
]
for raw, value, now, expected in cases:
if verify_thirds_signature(raw, value, [secret], now) != expected:
raise SystemExit("signature_check_failed")
print("signature_checks_passed")
Run both checks from that folder:
node check-signature.mjs
python3 check_signature.py
Both checks must print signature_checks_passed. The fixed clock makes this local check repeatable. In your receiver, omit the now argument so the function uses the current clock. Never use this sample secret for real deliveries. These checks only test signature handling. The small sample body isn't a real render event.
Node supplies HMAC and timingSafeEqual. Python supplies HMAC and compare_digest. Use these library functions rather than writing your own cryptography.
Do webhooks require authentication?
For an event that can cause customer work, verify the sender before you act. HTTPS protects the connection, but the receiver still needs to check the signed request. An event ID alone is not proof of origin.
After verification, parse the event and check its fields. Save the event ID and the work to durable storage before you return a small 2xx response. If the ID already exists, acknowledge it without adding more work. Don't put an API key in a URL, and don't log the body, signature, or secret.
Use the durable receiver recipe to connect this check to file collection. It binds to localhost behind your HTTPS proxy. The receiver saves verified events. A separate worker reads the job from the known API origin and checks the download. Don't send your API key to an unchecked event URL.
What are the downsides of using webhooks?
Your receiver needs a public HTTPS address, a correct clock, and durable storage. Events can repeat or arrive out of order. A slow receiver can cause retries even when your own work is already underway. Keep the response fast and let a worker handle file collection.
thirds.ai gives a receiver ten seconds to answer. A failed event gets up to eight attempts before delivery stops. When you repair the receiver and replay the event, you don't get another PDF or spend another credit. Poll the known job if you need its result while delivery is down. See the delivery and replay rules.
| Failure | Next step |
|---|---|
| Signature check returns false | Check the raw body, active secret, and server clock. |
| A body parser changes the request | Use a route-specific raw body parser before JSON parsing. |
| Delivery repeats | Find the stored event ID and return success without new work. |
| Receiver cannot save the event | Return a failure and restore storage before the next delivery. |
| A secret rotates | Accept both active secrets during the 24-hour overlap. |
| A test event has no downloadable job | Acknowledge the documented nil job ID after verification. |
Next, register your receiver and click Send test event in Webhooks. The test uses no credits. Then follow the file collection recipe with one invoice you intend to create.



