Webhooks
A webhook tells your system the moment something happens in TRUED, such as an invoice being
issued or paid, or a month being closed. TRUED sends a signed POST to an address you choose.
Add a webhook
- In TRUED, open Settings, then Integrations, then Webhooks, and choose Add webhook.
- Enter your address. It must start with
https://and be reachable from the internet. - Tick the events to send, and choose Add webhook.
- Copy the signing secret (starts
whsec_). TRUED shows it once. Your system uses it to check that each message really came from TRUED. - Choose Send test to send a
test.pingevent and see the result straight away.
A workspace can have up to 5 webhooks.
What TRUED sends
POST /trued HTTP/1.1
Content-Type: application/json
User-Agent: TRUED-Webhooks/1
TRUED-Signature: t=1790875200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
TRUED-Event-Id: evt_3Kq9xT
TRUED-Delivery-Id: evt_3Kq9xT_wh_7aB2
{
"id": "evt_3Kq9xT",
"type": "invoice.paid",
"created_at": "2026-10-01T15:41:07.000Z",
"data": {
"object_type": "invoice",
"object_id": "inv_7Qa2",
"invoice_id": "inv_7Qa2",
"invoice_number": "INV-2026-09-0047",
"client_id": "cl_19ff",
"contractor_id": "ct_5d0e",
"period": "2026-09"
}
}
The message is thin: it carries ids, never amounts, pay or margins. To get the details, read
the object through the API with your own key, for example GET /v1/invoices/inv_7Qa2. This way a
webhook can never show more than your key is allowed to see, and you always get current figures.
The same events are available to read from GET /v1/events for 30 days. The full list of event
types is in the reference.
Check the signature
TRUED-Signature is t=<unix seconds>,v1=<signature>. The signature is an HMAC-SHA256, in hex, of
the timestamp, a full stop, and the raw request body, using your signing secret. Check it against
the raw body, before any JSON parsing changes it.
Node.js:
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')).filter((p) => p[0] === 't'));
const sigs = header.split(',').filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return sigs.some((s) => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));
}
Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = [p.split('=', 1) for p in header.split(',')]
t = next((int(v) for k, v in parts if k == 't'), 0)
sigs = [v for k, v in parts if k == 'v1']
if not t or abs(time.time() - t) > tolerance_sec:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s, want) for s in sigs)
Refuse a message whose timestamp is more than 5 minutes old: that stops an old message from being replayed at you.
Rotating the secret
Choose Rotate secret on the webhook. For the next 24 hours TRUED signs every message with
both the old and the new secret, so the header carries two v1= values. The code above accepts
either, so update your system's secret any time in those 24 hours.
Answer quickly, handle repeats
- Answer with any 2xx status within 10 seconds. Do the real work after answering (put the event on a queue). A slow answer counts as a failure.
- Expect repeats. TRUED delivers at least once, so the same event can arrive more than once.
Use
TRUED-Event-Idto skip an event you already handled. - Order is not guaranteed. An
invoice.paidcan arrive beforeinvoice.sent. Read the invoice through the API to see its current state rather than relying on order. - TRUED does not follow redirects. Give the final address.
When delivery fails
A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. Each webhook shows its recent deliveries in TRUED, with the status your address answered.
If every delivery to a webhook fails for 3 days in a row, TRUED turns the webhook off, emails your finance team and shows a notice in TRUED. Fix the address, then choose Turn back on.
Deliveries are kept for 30 days. Choose Send again on any finished delivery to send it once more, for example after an outage on your side.