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

EventWhen
EXTRACTION_FINISHEDA document has been read; its data is ready.
EXTRACTION_FAILEDA document couldn't be read.
DOCUMENT_EXPORTEDThe accounting system accepted the export.
EXPORT_FAILEDThe export was refused; the reason is on the document.
DOCUMENT_ARCHIVEDA document was archived.
ENDPOINT_TESTEDYou 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));
}
The signature covers the body only. Treat 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.