Build with the API
Receiving results
Have Parsli call you when a document finishes, instead of asking repeatedly. The webhook payload in full.
Polling works, but it means asking a question whose answer is usually "not yet". A Webhook inverts that: Parsli calls you the moment a document is done.
This fires for every completed document on the parser, whatever route it arrived by — API, upload, email, or the inbound webhook. Which makes it the one integration point that catches everything.
Setting one up
Add a Webhook integration from the parser's Outbound, give it your endpoint, and use the test action to send a sample payload before you rely on it.
| Setting | Notes |
|---|---|
| URL | Your HTTPS endpoint |
| Method | POST or PUT |
| Auth | None, bearer token, or basic |
The payload
{
"event": "document.processed",
"parser_id": "6f3a1c88-...",
"parser_name": "Supplier invoices",
"document_id": "b71c9d02-...",
"timestamp": "2026-08-07T09:14:22.481Z",
"data": {
"invoice_number": "INV-2026-0417",
"vendor_name": "Northgate Supplies",
"total_amount": 1284.5,
"line_items": [
{ "description": "Steel brackets", "quantity": 40, "unit_price": 12.5 }
]
},
"metadata": {
"file_name": "northgate-0417.pdf",
"mime_type": "application/pdf",
"source_type": "email",
"page_count": 2
}
}
| Key | Type | Notes |
|---|---|---|
event | string | document.processed |
parser_id | string | Which parser produced this |
parser_name | string | Human-readable, for logs |
document_id | string | Re-fetchable via GET /api/v1/documents/{id} |
timestamp | string | ISO 8601, UTC |
data | object | The extracted result, keyed by your field names |
metadata.file_name | string | Original filename |
metadata.mime_type | string | Detected content type |
metadata.source_type | string | How it arrived — upload, email, api |
metadata.page_count | number | Pages charged |
data is shaped identically to results on the inline route. Same field names, same types, same null for anything the engine could not ground in the document. One shape to write a handler for, not two.
Tip: metadata.source_type is the useful one for routing. It lets a single endpoint treat an emailed invoice differently from one a colleague uploaded, without a second parser or a second webhook.
Writing a handler that survives contact with production
Respond fast, work later. Acknowledge with 200 immediately and push the work onto a queue. A handler that does its processing inline will eventually be slow enough to look like a failure.
Be idempotent on document_id. Delivery is at-least-once by design — a network blip between our send and your acknowledgement means a retry. If the same document_id arrives twice, the second one must be a no-op. This is the single most common webhook bug, and it is silent: you get duplicate rows, not an error.
Verify it is us. Set a bearer token on the integration and check it. The endpoint is otherwise open to anyone who learns the URL.
Handle null. A field the engine could not find is null, not absent and not zero. Route it to a human rather than writing it to a ledger.
app.post("/parsli", async (req, res) => {
if (req.headers.authorization !== `Bearer ${process.env.PARSLI_WEBHOOK_SECRET}`) {
return res.sendStatus(401)
}
const { document_id, data, metadata } = req.body
// Acknowledge before doing any real work.
res.sendStatus(200)
if (await alreadyProcessed(document_id)) return // at-least-once delivery
if (data.total_amount == null) return flagForReview(document_id, metadata)
await recordInvoice({ documentId: document_id, ...data })
})
Webhook or polling?
| Webhook | Polling | |
|---|---|---|
| You need | A public HTTPS endpoint | Nothing |
| Latency | Immediate | Your poll interval |
| Works for | Every document, any source | Documents you submitted |
| Local development | Needs a tunnel | Works as-is |
Use a webhook in production. Poll while developing, or when you genuinely only care about documents your own code submitted.
Nothing stops you doing both — the webhook for the general case, polling for a specific submission you are waiting on.
Something here wrong or missing? Tell us — we treat it as a bug.