Parsli documentation

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.

SettingNotes
URLYour HTTPS endpoint
MethodPOST or PUT
AuthNone, bearer token, or basic

The payload

json
{
  "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
  }
}
KeyTypeNotes
eventstringdocument.processed
parser_idstringWhich parser produced this
parser_namestringHuman-readable, for logs
document_idstringRe-fetchable via GET /api/v1/documents/{id}
timestampstringISO 8601, UTC
dataobjectThe extracted result, keyed by your field names
metadata.file_namestringOriginal filename
metadata.mime_typestringDetected content type
metadata.source_typestringHow it arrived — upload, email, api
metadata.page_countnumberPages 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.

javascript
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?

WebhookPolling
You needA public HTTPS endpointNothing
LatencyImmediateYour poll interval
Works forEvery document, any sourceDocuments you submitted
Local developmentNeeds a tunnelWorks 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.