Webhooks
Get an HTTPS POST as documents are extracted, exported or archived — no polling.
Setting up
Create an endpoint on the Webhooks page in FinAiQ (/app/webhooks) (or POST /api/v1/webhooks): your
HTTPS URL, the events you want, and all folders or a selection. FinAiQ shows a signing secret (whsec_…)
once — store it.
Events
| Event | When |
|---|---|
EXTRACTION_FINISHED | A document has been read; its data is ready. |
EXTRACTION_FAILED | A document couldn't be read. |
DOCUMENT_EXPORTED | The accounting system accepted the export. |
EXPORT_FAILED | The export was refused; the reason is on the document. |
DOCUMENT_ARCHIVED | A document was archived. |
ENDPOINT_TESTED | You pressed "Send test". |
Payload
POST https://your-app.example.com/finaiq
Content-Type: application/json
X-FinAIQ-Event: DOCUMENT_EXPORTED
X-FinAIQ-Delivery: 3f6c0d6e-… (unique per delivery — use it to de-duplicate)
X-FinAIQ-Timestamp: 1791100000
X-FinAIQ-Signature: sha256=5d41402abc4b2a76…
{
"id": "3f6c0d6e-…",
"type": "DOCUMENT_EXPORTED",
"occurredAt": "2026-10-04T06:05:56Z",
"folder": { "id": 1, "name": "Finaiq Pvt Ltd" },
"document": {
"id": 1, "fileName": "TaxInvoice INV-009464.pdf", "status": "EXPORTED",
"documentType": "SALES_INVOICE", "documentNumber": "INV-009464",
"vendorName": "Mr. Riggs", "totalAmount": 5765.76, "currencyCode": "AUD",
"documentDate": "2026-02-04", "dueDate": "2026-05-29", "queue": null
}
}
Verifying the signature
X-FinAIQ-Signature is sha256= + the hex HMAC-SHA256 of the raw request body,
keyed with your signing secret. Compute it over the exact bytes you received (before parsing JSON) and compare in
constant time. Reject anything that doesn't match.
Node.js
import crypto from 'node:crypto';
export function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(header ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
Java
static boolean verify(byte[] rawBody, String header, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(rawBody));
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
header == null ? new byte[0] : header.getBytes(StandardCharsets.UTF_8));
}
X-FinAIQ-Delivery as an idempotency key —
ignore a delivery id you've already processed — and use occurredAt to discard stale events.Responding and retries
Reply with any 2xx quickly (do the work asynchronously). Anything else, or a timeout, is retried after
1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After 20 consecutive failures the endpoint is switched off;
turn it back on from the Webhooks page once it's fixed. GET /api/v1/webhooks/{id}/deliveries lists recent attempts.