# Parsli — complete documentation > Every documentation page, concatenated. Generated from source; do not edit. > Canonical HTML: https://parsli.co/docs > Index: https://parsli.co/llms.txt Terms written as [[term-id]] are defined in the Dictionary, available as structured data at https://parsli.co/docs/dictionary.json =============================================================================== ## Parsli documentation URL: https://parsli.co/docs Section: start-here | Intent: explanation Parsli reads documents — invoices, receipts, statements, forms, scans, photos, handwriting — and gives you back clean, structured data you can actually use. These docs are written for two people at once: someone setting Parsli up without writing any code, and someone wiring it into their own system. Pages stay in plain English. Where a word has a precise technical meaning, it links to the [Dictionary](/docs/dictionary), so you get the detail only if you want it. ## Start where you are **Never used Parsli.** Read [what Parsli does](/docs/start-here/what-parsli-does), then follow [your first extraction](/docs/start-here/your-first-extraction). About five minutes, and you will have real data out of a real document. **Setting it up for your team.** Do the walkthrough first, then [how Parsli works](/docs/start-here/how-parsli-works) so the pieces make sense before you build something everyone depends on. **Writing code against it.** The object model is in [how Parsli works](/docs/start-here/how-parsli-works) and the [Dictionary](/docs/dictionary) — both are short. Every term carries the payload keys and types alongside the plain description. **Working out what it costs.** [How pages are counted](/docs/billing/how-pages-are-counted) explains the unit. [Free pages and plans](/docs/billing/free-pages-and-plans) covers what you get for nothing and what happens after. ## The one idea worth knowing up front Everything in Parsli hangs off a [[parser]]. A parser is a reusable setup for one kind of document — you might have one for supplier invoices and another for bank statements. You tell it what to pull out once, and then every document you send it comes back the same shape. That is the whole model. Documents go in, structured data comes out, and a parser is the thing in the middle that knows what you wanted. ## Reading these docs as a machine If you are an AI agent or you are building one, there are cheaper ways to read this than parsing HTML: - Add `.md` to any docs URL for the raw markdown — `/docs/start-here/how-parsli-works.md` - Request any docs page with `Accept: text/markdown` for the same thing - [`/llms.txt`](/llms.txt) is a compact index of every page - [`/llms-full.txt`](/llms-full.txt) is the entire documentation in one file - [`/docs/dictionary.json`](/docs/dictionary.json) is the full object model as structured data — the fastest way to learn the vocabulary in one request ------------------------------------------------------------------------------- ## What Parsli does URL: https://parsli.co/docs/start-here/what-parsli-does Section: start-here | Intent: explanation You have documents arriving — invoices from forty different suppliers, bank statements, delivery notes, expense receipts photographed in a car park. The information you need is in them. Getting it out means someone typing it into a spreadsheet. Parsli does that part. ## What actually happens You send a document. [[parsing-engine]] reads it the way a person would — it looks at the whole page, understands that the number in the bottom-right is the total and the table in the middle is line items, and returns that as data. ```json { "invoice_number": "INV-2026-0417", "vendor_name": "Northgate Supplies", "total_amount": 1284.50, "due_date": "2026-09-01", "line_items": [ { "description": "Steel brackets", "quantity": 40, "unit_price": 12.5, "total": 500.0 }, { "description": "Delivery", "quantity": 1, "unit_price": 784.5, "total": 784.5 } ] } ``` From there it goes wherever you need it — a Google Sheet, QuickBooks, Xero, Zoho Books, a Zap, or your own system over the API. ## What makes this different from OCR Traditional OCR tells you what characters are on a page. It has no idea which of those characters is the invoice total, so you end up writing rules: "the total is the number 40 pixels below the word TOTAL". Then a supplier changes their template and the rule breaks silently. Parsli works from meaning rather than position. You say you want the total including tax; it finds the total including tax, whether that sits top-right on one invoice and mid-page on another. The practical consequence: **you do not need one setup per supplier.** One [[parser]] handles invoices in whatever shape they arrive. ## What it reads Digital PDFs, scanned PDFs, photographs, screenshots, Word files, spreadsheets, and plain text. Scans and photos are handled directly — there is no separate OCR step to run first. Handwriting works, with the usual caveat that legibility matters. ## What it will not do Worth knowing before you build on it: - **It does not silently guess.** If a value is not in the document, the engine is built to leave it empty rather than invent something plausible. A blank field is information; a fabricated one is a liability. - **It is not a matching engine.** Parsli extracts what is in a document. Reconciling that against a purchase order or a ledger is your system's job, or a job for a [premium workspace](/docs/premium). - **It does not need a fixed layout, but it does need the information to be there.** If the due date is not printed on the invoice, nothing can extract it. > Tip: The single biggest driver of accuracy is being specific about what you want. "Amount" is ambiguous on a document with a subtotal, a tax line, and a total. "Total including tax" is not. See [defining what to extract](/docs/define-what-to-extract). ## Where to go next Do [your first extraction](/docs/start-here/your-first-extraction) — it takes about five minutes and uses a document you already have. Then read [how Parsli works](/docs/start-here/how-parsli-works) when you want to understand the pieces properly. ------------------------------------------------------------------------------- ## Your first extraction URL: https://parsli.co/docs/start-here/your-first-extraction Section: start-here | Intent: tutorial By the end of this you will have pulled real data out of a real document. Use something you actually work with — an invoice, a receipt, a statement. A real document tells you far more than a sample one. You get **10 free pages** when you sign up, and no card is needed to complete this. ## 1. Create a parser From the dashboard, create a new parser. There are three things to set. **Name it** after the kind of document, not the sender. "Supplier invoices" is right; "Northgate invoices" means you will end up with forty parsers. **Choose a parser type:** | Type | What you get | Choose it when | |---|---|---| | **Field Extraction** | Only the values you asked for, the same shape every time | You want specific data — invoice numbers, totals, line items | | **Full Document Extraction** | The whole document as text, layout preserved | You want everything, or documents vary too much to pin down | Most people want Field Extraction. Pick that unless you specifically want a full text conversion. **Choose how to define fields.** Field Extraction needs to know what to look for. The fastest route is **AI detect fields**: upload one sample and Parsli proposes a set of fields from what it sees. You can also start from a ready-made template, or add fields by hand. > Tip: Use AI detect fields for your first parser even if you know exactly what you want. Seeing what Parsli proposes tells you how it reads your document, which is useful before you start writing your own. ## 2. Send it a document Drag your document onto the upload area. Parsli processes it and shows you what it found. A single-page document costs one page from your balance. A five-page PDF costs five. See [how pages are counted](/docs/billing/how-pages-are-counted). ## 3. Read the result You will get each field with the value found. Some things worth noticing: - **An empty field means the engine could not find that value.** That is deliberate. It abstains rather than guessing, so an empty field is a fact about your document, not a failure. - **[[confidence]] flags uncertainty.** A low-confidence value is one to check yourself before trusting it. - **Tables come back as rows**, one object per line item, not as a flat blob you have to unpick. ## 4. Fix what is wrong Almost nobody gets a perfect result first time, and the fix is usually small. In order of how often it works: 1. **Rename the field to say what you mean.** `amount` is ambiguous on a document with a subtotal, tax, and total. `total_including_tax` is not. 2. **Add a description to the field.** One sentence: "The final amount payable, after tax and any discount." 3. **Add [[extraction-instructions]].** For genuine edge cases — "if no PO number is printed, use the invoice number instead". 4. **Check the [[field-type]] matches.** A currency amount typed as text will come back as text, punctuation and all. Change one thing, run the document again, see what moved. Changing four things at once tells you nothing about which one worked. ## 5. Point it somewhere Once the data is right, connect the parser to wherever it should land — a Google Sheet, your accounting system, an automation tool, or your own application. See [send data out](/docs/send-data-out). ## What you have now A parser that handles this kind of document from any sender, in any layout. Send it fifty invoices and you get fifty consistent records. Next: [how Parsli works](/docs/start-here/how-parsli-works) for the pieces underneath, or [defining what to extract](/docs/define-what-to-extract) to sharpen your fields. ------------------------------------------------------------------------------- ## How Parsli works URL: https://parsli.co/docs/start-here/how-parsli-works Section: start-here | Intent: explanation Five objects. Once these make sense, everything else in Parsli is a detail. ## The model A [[parser]] is a reusable setup for one kind of document. It owns everything below it. Inside a parser: - **[[field-definition]]** — what you want pulled out. Shown in the app as **Fields & Tables**. - **[[inbound]]** — how documents get in. - **[[outbound]]** — where extracted data goes. - **[[document]]** — one file you sent, plus the result that came back. That is the whole structure. A parser for supplier invoices has one field definition, a few ways for invoices to arrive, one or two places the data should land, and a growing pile of documents. ## The journey of a document 1. **It arrives** — uploaded, emailed to the parser's own address, or posted in over the API. All routes are equal; the rest of the pipeline does not care which one was used. 2. **Pages are counted.** This is what you are charged: one page, one [[credit]]. 3. **[[parsing-engine]] reads it** against your field definition. 4. **A result is produced**, with a [[confidence]] signal per field. Values that cannot be grounded in the document are left empty rather than guessed. 5. **Outbound integrations fire**, each one getting the same result. 6. **The document is stored** with its result, so you can look at it later or reprocess it. If extraction fails, the credits reserved in step 2 are returned. You are not charged for failures. ## Why a parser is not a template The word "template" implies a fixed layout — draw a box around where the total lives, and hope the next invoice puts it in the same place. Parsli does not work that way. The field definition describes **meaning**, not position. "Total including tax" is a description of what you want; where it sits on the page is the engine's problem. This is why one parser handles invoices from forty suppliers with forty different designs. The practical rule that follows: **make one parser per kind of document, not per sender.** ## Fields, tables, and nesting Most fields are simple — a [[field-type]] like text, a number, a date. Two shapes matter more: - **[[table-field]]** for anything that repeats. Invoice line items, statement transactions. You define the columns once, and get back one row per line found. - **[[object-field]]** for things that group. A vendor with a name, an address, and a tax number. Tables are the common case people miss. If your document has a list of things, that is a table field, not five separate fields with numbers on the end. ## In and out are separate [[inbound]] and [[outbound]] are independent, which is what makes the useful patterns possible: - Invoices arrive by email, land in QuickBooks. Nobody logs in. - Documents arrive over the API, results come back over a webhook. - A colleague drops files into the [[inbox]] and the data appears in a Google Sheet. You can add or remove either side without touching the other. ## What this means if you are writing code The same model, in API terms: - A parser is addressed by `parser_id`. An [[api-key]] is scoped to one parser. - A document is addressed by `document_id` and fetched with `GET /api/v1/documents/{id}`. - Extraction runs either [[synchronous-extraction|synchronously]] or [[asynchronous-extraction|asynchronously]]. Small files can go inline; large or slow ones must use the async route, or you will hit request timeouts. - Outbound webhooks carry the same result object the app shows you. There is no second, different shape to learn. Details in [build with the API](/docs/api). ## Next - [Defining what to extract](/docs/define-what-to-extract) — getting fields right is most of the work - [How pages are counted](/docs/billing/how-pages-are-counted) — what a document actually costs - [Dictionary](/docs/dictionary) — every term, in plain and precise form ------------------------------------------------------------------------------- ## Get documents in URL: https://parsli.co/docs/get-documents-in Section: get-documents-in | Intent: explanation A document can reach a [[parser]] four ways. They are equal — once a document is in, nothing downstream cares how it arrived. | Route | Good for | Anyone need an account? | |---|---|---| | **Upload** | Files already on your computer | Yes | | **Email** | Documents that arrive as email anyway | No — just send to an address | | **[[inbox]]** | Colleagues who should not need a login | No | | **API or webhook** | Another system sending automatically | No — it authenticates with a key | Most teams end up using two: email for what suppliers send, and upload for what someone has on their desktop. ## Uploading Drag files onto the parser's **Documents** page, or click to browse. ![The Documents page with the drop area and a batch of files processing](docs-image:documents-upload) You are not limited to one at a time: - **Select many files** at once - **Drag a whole folder** — Parsli walks it and takes every document inside - **Drop a ZIP** — it is unpacked and the contents processed Large batches process a few at a time rather than all at once, so a folder of five hundred files works without anything falling over. Files up to **300MB** are accepted. > Tip: While you are still tuning a [[field-definition]], test with one representative document rather than the whole batch. Each extraction costs [[credit\|credits]], and one document tells you almost everything a hundred would. ## By email Every parser has its own [[inbound-email-address]]. Anything sent there is processed automatically. This is usually the highest-leverage route, because most documents are already arriving by email. Forward them, or give the address to a supplier and skip the forwarding. Parsli treats **one email as one document** — the body, the sender, and any attachments are combined into a single record rather than split into separate ones. An invoice PDF with the context of its covering note stays together, and a signature logo does not become a document of its own. You can also connect a [[gmail-inbox]] so Parsli watches a real mailbox and picks up attachments from a specific sender. Full detail: [by email](/docs/get-documents-in/by-email). ## From another system Two options, depending on what is calling you. **The [API](/docs/api)** — for code you control. Authenticated with a bearer token, returns extracted data, handles files of any size via the [upload route](/docs/api/large-and-slow-files). **The [[inbound-webhook]]** — a per-parser URL that accepts a document with no headers at all. The route for no-code tools that will only let you paste a URL. Full detail: [from another system](/docs/get-documents-in/from-another-system). ## What Parsli accepts | Format | Extensions | How it is read | |---|---|---| | PDF | `.pdf` | Text and layout together; scanned pages read visually | | Images | `.png` `.jpg` `.jpeg` `.webp` `.gif` `.bmp` | Read visually — OCR is built in, no separate step | | Word | `.docx` `.doc` | Text extraction | | Spreadsheets | `.xlsx` `.xls` | Converted to tabular text | | Plain text | `.txt` `.csv` `.json` `.xml` `.md` | Read directly | Scans and photographs need no preprocessing. A photo of a receipt taken on a phone is a first-class input. ## What it costs One [[credit]] per [[page]], whatever the route. A three-page PDF costs three whether you uploaded it, emailed it, or posted it over the API. See [how pages are counted](/docs/billing/how-pages-are-counted). ------------------------------------------------------------------------------- ## By email URL: https://parsli.co/docs/get-documents-in/by-email Section: get-documents-in | Intent: how-to Most documents worth extracting already arrive by email. This route removes the step where a human downloads an attachment and re-uploads it. ## The parser's own address Every [[parser]] has a private [[inbound-email-address]]. Find it in the parser's **Settings**, under **Email Forwarding** — and it is repeated on the **Documents** page, next to the upload area, so you can copy it without leaving the screen you are already on. ![Settings, Email Forwarding — the parser's inbound address with a copy action](docs-image:email-forwarding) Anything sent there is processed against that parser. Two ways to use it: **Forward manually.** Fine while you are trying things out, and fine forever for low volume. **Set up an automatic rule.** In your mail client, forward anything matching a filter — from a supplier, or with an attachment and "invoice" in the subject — to the parser address. Now nobody touches it again. **Give the address out.** Ask a supplier to send invoices there directly, or use it as the billing contact on an account. Documents arrive already extracted. > Important: Treat the address as a credential. Anyone who has it can send documents to that parser and consume your [[credit\|credits]]. Do not publish it on a website or in a public form. ## One email is one document This matters more than it sounds. Parsli combines the **email body, the sender, and every attachment** into a single [[document]]. It does not create one record per attachment. The reason is that context lives in the covering note. "Please see attached, revised — ignore the version I sent Tuesday" is information about the PDF, and splitting them apart throws it away. It also avoids a failure that used to be common: signature logos and footer images are attachments too, and treating each as a document produces a pile of records extracted from a company logo. ## Connecting a Gmail mailbox Instead of forwarding, Parsli can watch a real mailbox. 1. From the parser's **Outbound**, add a **Gmail Inbox** integration. 2. Authorise your Google account. 3. Set a **from** filter — only mail from this sender is processed, for example `invoices@supplier.com`. 4. Activate it. Parsli checks periodically and processes matching messages as they arrive. > Important: Parsli requests **read-only** Gmail access. It cannot send, delete, or modify your mail. The sender filter is required, not optional — an unfiltered mailbox would extract every email you receive and bill you for it. ### Forwarding or Gmail connection? | | Forwarding rule | Gmail connection | |---|---|---| | Set up in | Your mail client | Parsli | | Google access | None | Read-only OAuth | | Filtering | Whatever your client supports | By sender | | Speed | On arrival | On the next check | | Works with | Any mail provider | Gmail | Forwarding is more flexible and needs no account access. The Gmail connection is easier to reason about because the whole setup lives in one place. ## Letting colleagues send documents The [[inbox]] gives a parser a shared drop-off point with its own access rules — for people who should be able to send documents in without you provisioning them a Parsli account. Set it up under **Inbox** on the parser. ## Checking what arrived Emailed documents appear on the parser's **Documents** page like any other, with `source_type` recorded as email. That field is also in the [webhook payload](/docs/api/receiving-results), so downstream systems can treat emailed documents differently from uploaded ones without a second parser. ## When something does not arrive In order of likelihood: 1. **Sent to the wrong address.** Each parser has a different one; check you copied the right parser's. 2. **The sender filter excluded it.** On a Gmail connection, only the configured sender is processed. 3. **Out of credits.** Extraction stops at zero. Check Settings. 4. **The parser is paused.** A paused parser rejects new documents. ------------------------------------------------------------------------------- ## From another system URL: https://parsli.co/docs/get-documents-in/from-another-system Section: get-documents-in | Intent: how-to Two ways for another system to push documents into a [[parser]]. The right one depends on whether that system can set an HTTP header. ## The inbound webhook Every parser has a unique URL that accepts documents with no authentication headers at all. The token is in the URL. Find it in the parser's **Settings**, under **API Keys & Webhooks** → **Inbound Webhook**. ```bash curl -X POST https://parsli.co/api/inbound/webhook/YOUR_TOKEN \ -F "file=@invoice.pdf" ``` It also accepts JSON with base64 content, for tools that cannot send multipart: ```bash curl -X POST https://parsli.co/api/inbound/webhook/YOUR_TOKEN \ -H "Content-Type: application/json" \ -d '{ "file": { "name": "invoice.pdf", "type": "application/pdf", "data": "JVBERi0..." } }' ``` **Use it when** the sending tool only lets you paste a URL — Zapier, Make, Power Automate, a form builder, a scanner appliance, a legacy system whose HTTP client you cannot change. **It accepts documents; it does not return data.** The response tells you the document was received, not what was extracted. To get results back, add an outbound [[webhook]] or poll the API. See [receiving results](/docs/api/receiving-results). > Important: The token is in the URL, so it will be captured by server logs, browser history, and referrer headers in ways an `Authorization` header will not. Treat the whole URL as a secret. If it leaks, regenerate it — anyone holding it can send documents and consume your [[credit\|credits]]. ## The API For code you control, use the [API](/docs/api) instead. It is better in every way that matters: - Authenticates with a header rather than a URL, so the credential stays out of logs - Returns extracted data directly - Supports many keys per parser, so you can revoke one system without breaking the others - Handles files of any size via [upload-and-poll](/docs/api/large-and-slow-files) ```bash curl -X POST https://parsli.co/api/v1/extract \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file": { "name": "invoice.pdf", "type": "application/pdf", "data": "JVBERi0..." } }' ``` ## Choosing | | Inbound webhook | API | |---|---|---| | Credential | In the URL | `Authorization` header | | Returns extracted data | No | Yes | | Multiple revocable credentials | No | Yes | | Large files | Limited | Yes, via upload-and-poll | | Needs code | No | Yes | **If you can set a header, use the API.** Use the inbound webhook when you genuinely cannot. ## Connecting through an automation tool Zapier, Make, and Power Automate can do either side — send documents in, or receive results out. To **send in**, point the tool's HTTP action at the inbound webhook URL. To **get results out**, add the matching integration under the parser's **Outbound** and paste the tool's catch-hook URL. See [send data out](/docs/send-data-out/automation-tools). Many setups use both: a Zap picks a file out of Dropbox and posts it to the inbound webhook, and an outbound webhook returns the extracted data to a different Zap that files it. ------------------------------------------------------------------------------- ## Define what to extract URL: https://parsli.co/docs/define-what-to-extract Section: define-what-to-extract | Intent: explanation A [[field-definition]] is the list of things you want pulled out of every document. In the app it is **Fields & Tables**. ![The Fields & Tables page showing a field list with types and a table field expanded](docs-image:fields-and-tables) ## The one thing that matters most **Name fields for what you mean, not for what is printed.** Parsli works from meaning, so the name of a field is not a label — it is an instruction. This is where nearly all accuracy problems begin and end. | Instead of | Use | Why | |---|---|---| | `amount` | `total_including_tax` | An invoice has a subtotal, a tax line, and a total | | `date` | `due_date` | There are usually three dates on an invoice | | `number` | `invoice_number` | There is also a PO number and an account number | | `name` | `vendor_name` | The customer name is on there too | If a colleague could misread the field name, so can the engine. Every minute spent here saves ten spent debugging results. ## Adding fields Open **Fields & Tables** and add a field with: 1. **A name** — precise, as above 2. **A [[field-type]]** — picked from the dropdown beside the name 3. **[[extraction-instructions]]** *(optional)* — the box labelled **AI instructions** beneath each field, for anything the name cannot carry on its own You can also let Parsli propose a starting set with [[field-detection]]: upload one sample and edit what it suggests. Faster than starting from an empty list, and it shows you how the engine reads your document. ## Field types Pick the type that matches the data, not the type that always works. A currency amount typed as text comes back as text — currency symbol, thousands separators and all — and you will be parsing strings downstream forever. The dropdown shows friendly names; the API returns the value in the right-hand column. They are the same thing. | In the app | In the API | Use for | |---|---|---| | Text, Rich text | `string`, `richtext` | Names, descriptions, notes | | Email, URL, Phone, Address | `email`, `url`, `phone`, `address` | Contact details | | Number, Decimal | `number`, `decimal` | Quantities, amounts, prices | | Date, Boolean | `date`, `boolean` | Dates, yes/no | | Single select, Multi select | `single_select`, `multi_select` | Categories that must be one of a known set | | Object, List, Table | `object`, `list`, `table` | Grouped, repeating, or nested data | Two worth knowing about specifically: **`decimal` for money.** `number` is for counts. Using `decimal` for amounts gets you a JSON number you can total, not a string you have to clean. **[[select-field\|Select fields]] for categories.** If a field should only ever be Hardware, Software, or Services, constrain it. You get consistent values instead of a long tail of near-synonyms that break every grouping downstream. ## Repeating and nested data Line items, transactions, anything that appears more than once per document — that is a [[table-field]], not five numbered fields. Grouped data — a vendor with a name, an address, and a tax number — is an [[object-field]]. This is the most common structural mistake. Full detail: [tables and nested data](/docs/define-what-to-extract/tables-and-nested-data). ## When the name is not enough Some things cannot be expressed in a field name. That is what descriptions and [[extraction-instructions]] are for — "use the total after any early-payment discount", "if no PO number is printed, use the invoice number". Full detail: [extraction instructions](/docs/define-what-to-extract/extraction-instructions). ## Fields cost nothing Fields are not metered. A definition with forty fields costs exactly the same as one with four — you are billed by [[page]], never by field. So there is no reason to be sparing. If a value might be useful later, extract it now; adding it in six months means reprocessing everything. > Tip: Change one thing at a time and re-run the same document. Changing four things at once tells you nothing about which one worked. ## Next - [Tables and nested data](/docs/define-what-to-extract/tables-and-nested-data) - [Extraction instructions](/docs/define-what-to-extract/extraction-instructions) - [Check the results](/docs/check-the-results) — reading output and fixing what is wrong ------------------------------------------------------------------------------- ## Tables and nested data URL: https://parsli.co/docs/define-what-to-extract/tables-and-nested-data Section: define-what-to-extract | Intent: how-to Most documents worth extracting have something that repeats — invoice line items, statement transactions, packing list contents. Modelling that correctly is the difference between data you can use and data you have to clean. ## The mistake to avoid If a document has a list of things, do **not** create numbered fields: ``` item_1_description, item_1_quantity, item_1_price item_2_description, item_2_quantity, item_2_price item_3_description, item_3_quantity, item_3_price ``` This breaks the moment an invoice has four lines. You get empty fields on short documents, truncation on long ones, and a schema nobody can maintain. Use a [[table-field]] instead. Define the columns once and get one row per line actually found — three rows, or forty. ## Table fields A [[table-field]] holds repeating rows. You define the columns; Parsli finds the rows. For invoice line items: | Column | Type | |---|---| | `description` | `string` | | `quantity` | `number` | | `unit_price` | `decimal` | | `line_total` | `decimal` | You get back an array of objects, one per row: ```json { "invoice_number": "INV-2026-0417", "line_items": [ { "description": "Steel brackets", "quantity": 40, "unit_price": 12.5, "line_total": 500.0 }, { "description": "Delivery", "quantity": 1, "unit_price": 784.5, "line_total": 784.5 } ] } ``` Keys match your column names, values are typed. No cell coordinates, no reconstruction step — iterate the array and insert. > Tip: Give columns the same care as top-level fields. `line_total` is unambiguous; `total` in a table sitting on an invoice that also has an overall total is not. ## Object fields An [[object-field]] groups related values that appear once. A vendor: | Child | Type | |---|---| | `name` | `string` | | `address` | `address` | | `tax_number` | `string` | ```json { "vendor": { "name": "Northgate Supplies", "address": "12 Mill Lane, Leeds LS1 4AB", "tax_number": "GB123456789" } } ``` Objects are organisational. `vendor.name` and `vendor_name` extract equally well — the object version is easier to read and maps more cleanly onto a nested record downstream. ## List fields A [[list-field]] holds several simple values with no internal structure — a set of reference numbers, a list of tags. ```json { "reference_numbers": ["REF-001", "REF-002", "REF-003"] } ``` If each entry has more than one property, it is a table, not a list. ## Choosing between them | Your data | Use | |---|---| | Appears once, single value | A plain field | | Appears once, several related values | [[object-field]] | | Repeats, one value each | [[list-field]] | | Repeats, several values each | [[table-field]] | The test: **does it repeat, and does each entry have more than one property?** Repeating with multiple properties is always a table. ## Tables inside tables Nesting a table within a table row is possible but rarely a good idea. It is hard to describe unambiguously, harder to consume, and usually a sign the document should be modelled as two parsers. If you find yourself reaching for it, consider whether the inner detail actually needs extracting, or whether one flattened table with an extra column expresses the same thing. ## What you get when nothing is found An empty table comes back as an empty array, not `null` and not a missing key: ```json { "line_items": [] } ``` So iterating is always safe. A `null` on a *column inside a row* means that specific cell could not be grounded in the document — the row exists, one value in it did not. ## Pages, not rows A table with two hundred rows costs the same as one with two. Billing counts [[page\|pages]], never rows, fields, or columns. Extract the whole table. ------------------------------------------------------------------------------- ## Extraction instructions URL: https://parsli.co/docs/define-what-to-extract/extraction-instructions Section: define-what-to-extract | Intent: how-to Sometimes what you want cannot be said in a field name. That is what descriptions and instructions are for. Reach for them in this order — the cheapest fix first. ## 1. Fix the name Most "the extraction is wrong" problems are a field called `amount` on a document with three amounts. Renaming it `total_including_tax` fixes more than any instruction will. Try this before anything else. See [defining what to extract](/docs/define-what-to-extract). ## 2. Add a description A [[field-description]] is one sentence saying what you mean. It travels with the field and applies every time. | Field | Description | |---|---| | `due_date` | "The date payment is due, not the invoice date or the delivery date." | | `total_including_tax` | "The final amount payable after tax and any discount." | | `vendor_name` | "The company issuing the invoice, not the customer being billed." | Descriptions handle ambiguity. Use them whenever a reasonable person could pick the wrong value. ## 3. Add extraction instructions [[extraction-instructions]] handle **rules and edge cases** — conditional logic, formatting, fallbacks. They come in two scopes. ### Per field The box labelled **AI instructions** under each field in Fields & Tables. - "If the invoice shows an early-payment discount, use the discounted total." - "Strip any currency symbol and return the number only." - "If there is no PO number, use the invoice number instead." ### Parser-wide In the parser's **Settings**, applying to every extraction that parser runs — including [[full-document-extraction]]. - "Normalise all dates to YYYY-MM-DD." - "Amounts are in GBP unless the document says otherwise." - "Ignore anything in the footer or the terms and conditions." - "If a value appears twice and disagrees, prefer the one in the summary table." Use parser-wide for conventions that hold across the whole document type. Use per-field for a rule about one value. ## Writing instructions that work **Be specific about the failure you are fixing.** "Extract the total correctly" says nothing. "Use the total after discount, not the gross total" is actionable. **Say what to do when the thing is absent.** "If no delivery date is shown, leave it empty" is better than silence — it confirms empty is the right answer rather than something to hunt for. **Prefer constraints to prose.** If a field should be one of three values, make it a [[select-field]]. A constrained type is more reliable than an instruction asking for the same thing. **One rule per instruction.** A paragraph containing four rules is harder to satisfy than four instructions. ## What instructions cannot do Worth knowing before you spend an afternoon on it: - **They cannot conjure absent information.** If the due date is not printed, no instruction produces one. Parsli will leave it empty, which is the correct behaviour. - **They cannot reliably do arithmetic across documents.** Matching an invoice to a purchase order is not extraction. That is a job for your system, or a [premium workspace](/docs/premium). - **They do not fix a bad field type.** If a currency amount is typed `string`, it comes back as a string no matter what the instruction says. Fix the type. ## A worked example An invoice parser producing a total that is sometimes gross, sometimes net. **Wrong first move** — instruction: "always get the right total". Nothing changes; it was already trying. **What actually works:** 1. Rename `total` → `total_payable` 2. Description: "The final amount the customer must pay." 3. Instruction: "If an early-payment discount is shown, use the discounted amount. Otherwise use the gross total including tax." Three changes, from most to least general. Re-run the same document after each so you know which one moved the result. > Tip: Keep one document that used to fail and re-run it after each change. Without a fixed test case you are guessing, and each guess costs [[credit\|credits]]. ------------------------------------------------------------------------------- ## Check the results URL: https://parsli.co/docs/check-the-results Section: check-the-results | Intent: how-to Extracted documents appear on the parser's **Documents** page, one row each, with the extracted values shown as columns. ![The Documents page listing processed documents, one still extracting and one complete](docs-image:documents-processed) A document is **Extracting** while it is being read — its value columns show as placeholders — and **Complete** when it is done. The filter row above the table narrows to **Processed**, **Processing**, **Preview**, or **Failed**. Open a row to see the source document alongside what was pulled out of it. ## Reading a result Three things are worth attention. **Empty values.** A field the engine could not ground in the document comes back empty — not guessed. This is deliberate, and it is a feature: a blank is a fact about your document, whereas a confident wrong number is a liability. An empty `due_date` usually means no due date was printed. **[[confidence]].** Values the engine is less sure about are flagged. Low confidence is not an error; it is a request that a human look before the number goes into a ledger. **Tables.** [[table-field\|Table fields]] come back as rows. If a table is missing rows, that is a field-definition problem rather than a reading problem — see below. ## Diagnosing a wrong extraction Work in this order. It is roughly the order of how often each one is the actual cause. ### 1. Is the field name ambiguous? By far the most common cause. `amount` on a document with a subtotal, a tax line, and a total is a coin toss. `total_including_tax` is not. Rename it. This fixes more than any other single change. ### 2. Is the type wrong? A currency amount typed `string` comes back as a string — symbol, separators and all. A date typed `string` comes back however it was printed. Set `decimal` for money, `date` for dates, `number` for counts. ### 3. Would a description help? One sentence: "The final amount payable, after tax and any discount." Descriptions resolve ambiguity that a name alone cannot carry. ### 4. Is this a genuine edge case? Conditional rules belong in [[extraction-instructions]] — "if there is an early-payment discount, use the discounted total". ### 5. Is the value actually in the document? Worth checking before spending an afternoon on the first four. If the due date is not printed anywhere, no configuration produces one. Empty is the correct answer. > Tip: Change one thing, re-run the same document, see what moved. Changing four things at once tells you nothing about which one worked — and each run costs [[credit\|credits]]. ## Reprocessing Once you have changed a field definition, run documents again to pick up the change. Reprocessing charges [[page\|pages]] again, at the same rate. Two habits that keep this cheap: - **Iterate on one representative document**, not the whole batch. One tells you nearly everything a hundred would. - **Use a short document while tuning.** A one-page sample costs one page; the forty-page statement costs forty each time you try something. ## Missing table rows If a table returns fewer rows than the document shows, the usual causes are: 1. **Ambiguous column names.** `total` inside a table on an invoice that also has an overall total. Rename to `line_total`. 2. **The table was modelled as numbered fields.** `item_1_price`, `item_2_price` cannot grow. Use a [[table-field]]. See [tables and nested data](/docs/define-what-to-extract/tables-and-nested-data). 3. **Rows continue on a page you did not send.** Check the source document is complete. ## Documents that fail entirely A failed extraction is **refunded** — credits are reserved beforehand and returned on failure, so a failure costs nothing. If the same document fails twice, the document is usually the problem: a corrupt PDF, an encrypted file, or a scan too degraded to read. Try opening it yourself; if you cannot read it, neither can the engine. ## What to do about low-confidence values Do not ignore them, and do not treat them as errors. Route them. - **Flag required fields** so their absence is visible rather than silent. - **Check before posting to a ledger.** See [accounting systems](/docs/send-data-out/accounting-systems). - **Handle empties in code.** A `null` total should reach a person, not become a zero. See [webhooks](/docs/send-data-out/webhooks). The point is not to eliminate uncertainty — some documents genuinely are ambiguous — but to make sure uncertainty reaches a human instead of a database. ------------------------------------------------------------------------------- ## Send data out URL: https://parsli.co/docs/send-data-out Section: send-data-out | Intent: explanation Once a [[document]] is extracted, its data goes wherever you need it. Connections live under the parser's **Outbound**. ![The Outbound page listing configured integrations with their status](docs-image:add-integration) Every [[integration]] fires on every completed document, independently. Add as many as you like; one failing does not stop the others. ## What you can connect | Where | Integration | Good for | |---|---|---| | **Spreadsheets** | Google Sheets | A live sheet that fills itself | | **Documents** | Google Docs | A formatted document per extraction | | **Accounting** | QuickBooks Online, Xero, Zoho Books | Filing bills and invoices into your books | | **Automation** | Zapier, Make, Power Automate | Reaching anything else | | **Your system** | Webhook | Full control | | **Templates** | Filled Spreadsheet → Drive | Populating an XLSX template you already use | | **Alerts** | Email notification | Telling a person something arrived | | **Research** | InfoQuest Project | Turning briefs into research projects | ## Picking one **You want the data in a spreadsheet.** Google Sheets. A formula in one cell, and the sheet keeps itself current. See [spreadsheets and documents](/docs/send-data-out/spreadsheets-and-documents). **You want bills in your accounting system.** Connect QuickBooks, Xero, or Zoho Books directly. Fewer moving parts than routing through an automation tool, and the field mapping is built in. See [accounting systems](/docs/send-data-out/accounting-systems). **You want to reach something else entirely.** Zapier, Make, or Power Automate connect to thousands of applications. See [automation tools](/docs/send-data-out/automation-tools). **You are writing the receiving code.** A [[webhook]] is the direct route — the full result, the moment it is ready. See [webhooks](/docs/send-data-out/webhooks). **Someone just needs to know.** An email notification tells a person a document arrived, without giving them a Parsli account. ## Combining them Integrations are independent, so the useful patterns compose: - **QuickBooks + Google Sheets** — bills go into the books, and a sheet keeps a running log the finance team can filter. - **Webhook + email notification** — your system records it, and a person gets told when something needs attention. - **Zapier + accounting** — the direct connection handles the standard case, and a Zap handles the exception. ## Testing before you trust it Every integration has a test action. Use it — a webhook pointing at a typo is invisible until documents start disappearing. For accounting connections especially, run one real document through and check what landed in the ledger before switching on a supplier's mailbox. ## When one fails A failing integration does not fail the extraction. The document is still processed, the result is still stored, and the other integrations still fire. The failure is recorded against that integration. Common causes, in order: 1. **The endpoint moved.** A webhook URL that 404s. 2. **Authorisation expired.** Accounting connections need re-authorising when the upstream token is revoked. 3. **The target rejected the data.** Usually a required field arriving `null` — see [check the results](/docs/check-the-results). ## Getting data out without an integration Not everything needs a connection: - **Download Data**, at the top of the Outbound page, exports every processed document as **CSV**, **JSON**, or **TXT** in one click. There is also an **Export Data** action on the Documents page. - **[Poll the API](/docs/api/large-and-slow-files)** with `GET /api/v1/documents/{id}` - **The Google Sheets feed** is a plain CSV URL, readable by anything that speaks HTTP For a one-off — a month-end export, a hand-off to an accountant — the download is usually the right answer. Integrations are for the recurring case. ------------------------------------------------------------------------------- ## Spreadsheets and documents URL: https://parsli.co/docs/send-data-out/spreadsheets-and-documents Section: send-data-out | Intent: how-to ## Google Sheets The fastest route from documents to something a finance team will actually use. One formula, and the sheet keeps itself current. 1. Under **Outbound**, add a **Google Sheets** integration. 2. Copy the generated formula. 3. Paste it into any cell of your sheet. ```spreadsheet =IMPORTDATA("https://parsli.co/api/parsers/{id}/feed?token={token}&format=csv") ``` Every extracted document appears as a row. New documents appear on their own — the sheet refreshes whenever Google Sheets recalculates, with no webhook, no script, and no manual export. **No Google authorisation is required.** The formula reads a [[data-feed]] — a plain CSV URL. Nothing is granted access to your Drive. > Important: The token in that URL is the credential. Anyone with the formula can read every result from that parser. Share the sheet, not the formula, and do not paste it into a public document. Because the feed is ordinary CSV over HTTP, anything that can read a URL can consume it — Excel's web query, a Python script, a BI tool. Google Sheets is just the easiest client. ### When Sheets is the wrong choice Very high volume. `IMPORTDATA` re-reads the whole feed each refresh, so a sheet backed by tens of thousands of documents gets slow. At that point use a [[webhook]] into a database and point the BI tool at that. ## Google Docs Creates a formatted Google Doc for each extracted document, with fields, tables, and metadata laid out in readable sections. 1. Under **Outbound**, add a **Google Docs** integration. 2. Connect Google Drive. 3. Optionally give a folder to file them in. Useful when the output is meant to be **read** rather than analysed — summaries, reports, case notes. For anything you intend to filter or total, use Sheets. > Important: Parsli requests the `drive.file` scope, which grants access **only to files it creates**. It cannot read, modify, or delete anything else in your Drive. ## Filled Spreadsheet → Drive For when you have an XLSX template that must be filled in — a form, a return, an internal report with a fixed layout. Rather than giving you rows to copy across, Parsli fills your actual template from the extracted data and uploads the completed file to your Drive. 1. Under **Outbound**, add **Filled Spreadsheet → Drive**. 2. Upload the template. 3. Connect Drive. Every processed document produces a filled copy. This suits the case where the *shape of the output* is fixed by someone else — a regulator, a client, a system that only accepts one layout. If you just want the data, Sheets is simpler. ## Choosing | | Google Sheets | Google Docs | Filled Spreadsheet | |---|---|---|---| | Output | Rows in one sheet | One document each | One filled file each | | Drive access | None | `drive.file` | `drive.file` | | Good for | Analysis, totals, filtering | Reading | A layout you must match | | Volume | Moderate | Any | Any | ------------------------------------------------------------------------------- ## Accounting systems URL: https://parsli.co/docs/send-data-out/accounting-systems Section: send-data-out | Intent: how-to Parsli connects directly to QuickBooks Online, Xero, and Zoho Books. Extracted bills, invoices, and expenses are created as real records in your ledger — no CSV import, no automation tool in between. ## Setting one up 1. Under the parser's **Outbound**, add **QuickBooks Online**, **Xero**, or **Zoho Books**. 2. Authorise the connection. You are sent to their site to sign in and approve. 3. Configure the mapping — which account, which contact, which record type. 4. Run one real document and check what landed. ![The accounting integration setup showing account and contact mapping](docs-image:quickbooks-setup) > Important: Do step 4 before pointing a supplier mailbox at the parser. A mapping mistake discovered after two hundred bills have posted is a reconciliation job; discovered after one, it is a dropdown. ## Getting the extraction right first An accounting connection is only as good as the fields feeding it. Before connecting: - **`decimal` for every amount.** A `string` amount arrives with its currency symbol attached and will be rejected or, worse, misread. - **`date` for every date.** Same reasoning. - **Be explicit about which total.** `total_including_tax` posts a different number than the subtotal. See [defining what to extract](/docs/define-what-to-extract). - **Extract the vendor precisely.** `vendor_name` must be the company issuing the invoice, not the customer. Contact matching depends on it. ## Handling missing values If [[parsing-engine]] cannot ground a value in the document, it comes back empty rather than guessed. That is the right behaviour — but a bill with no amount cannot post. Mark the fields your ledger requires as **required** so their absence is visible, and check the Documents page for anything flagged before it reaches the books. See [check the results](/docs/check-the-results). ## Reauthorising Accounting connections use OAuth, and those tokens expire or get revoked — someone changes a password, an admin audits app access, the subscription lapses. When that happens the integration starts failing while extraction carries on working normally. Documents process, data does not reach the ledger. Reauthorise from the integration's settings. > Tip: This is the failure worth building a habit around. Extraction succeeding while delivery quietly fails looks like nothing is wrong. Check the integration status if bills stop appearing. ## Multiple ledgers Each connection belongs to one parser, so different parsers can post to different places — one for each entity, or one per client. ## Which system | | QuickBooks Online | Xero | Zoho Books | |---|---|---|---| | Bills and invoices | Yes | Yes | Yes | | Expenses | Yes | Spend money | Yes | | Setup | OAuth | OAuth | OAuth | Use whichever you already keep your books in. There is no functional reason to prefer one within Parsli. ## If your system is not listed Two routes: - **An [automation tool](/docs/send-data-out/automation-tools)** — Zapier, Make, and Power Automate reach most accounting products. - **A [[webhook]]** into whatever API your system exposes. See [webhooks](/docs/send-data-out/webhooks). For an in-house or industry-specific ledger, a bespoke [[connector]] can be built with you. See [premium workspaces](/docs/premium). ------------------------------------------------------------------------------- ## Automation tools URL: https://parsli.co/docs/send-data-out/automation-tools Section: send-data-out | Intent: how-to Zapier, Make, and Power Automate all work the same way: they give you a URL, you paste it into Parsli, and extracted data flows into your automation. Between them they reach thousands of applications, which makes this the answer whenever there is no direct integration. ## Zapier 1. In Zapier, create a Zap with the **Webhooks by Zapier** trigger. 2. Choose **Catch Hook** and copy the URL. 3. In Parsli, under **Outbound**, add a **Zapier** integration and paste it. 4. Use the test action to send a sample payload. 5. Back in Zapier, build the rest of the Zap against the fields that arrive. Step 4 matters — Zapier learns the payload shape from a real sample. Without it you are mapping fields blind. ## Make Identical shape: 1. Create a scenario with a **Custom webhook** module. 2. Copy the URL. 3. Add a **Make** integration in Parsli and paste it. 4. Send a test so Make can determine the data structure. ## Power Automate 1. Create a flow with the **When an HTTP request is received** trigger. 2. Save the flow to generate the URL. 3. Add a **Power Automate** integration in Parsli and paste it. 4. Send a test. ## What arrives All three receive the same payload as a [[webhook]]: ```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", "total_amount": 1284.5, "line_items": [ { "description": "Steel brackets", "quantity": 40, "unit_price": 12.5 } ] }, "metadata": { "file_name": "northgate-0417.pdf", "source_type": "email", "page_count": 2 } } ``` Your extracted values are under `data`, keyed by your field names. ### Tables need a loop A [[table-field]] arrives as an **array**. Automation tools do not flatten arrays for you — to create one row per line item you need the tool's iterator: a Looping step in Zapier, an Iterator module in Make, **Apply to each** in Power Automate. This is the most common thing to get stuck on. If only the first line item is landing, a loop is what is missing. ### Empty values A field the engine could not ground in the document arrives empty rather than guessed. Add a filter step for anything that must not proceed without a value — otherwise a blank total silently becomes a zero-value record downstream. ## Sending documents the other way These tools can also feed documents **into** Parsli. Point an HTTP action at the parser's [[inbound-webhook]]. A common complete loop: 1. A Zap watches a Dropbox folder 2. It posts new files to the inbound webhook 3. Parsli extracts them 4. The outbound Zapier integration returns the data 5. A second Zap files it wherever it belongs See [from another system](/docs/get-documents-in/from-another-system). ## Automation tool or direct integration? If Parsli connects to your target directly — QuickBooks, Xero, Zoho Books, Google Sheets — use the direct connection. Fewer moving parts, no per-task cost, and field mapping is handled for you. Use an automation tool for everything else, or when you need logic in the middle: routing on a value, enriching from another source, notifying someone conditionally. ------------------------------------------------------------------------------- ## Webhooks URL: https://parsli.co/docs/send-data-out/webhooks Section: send-data-out | Intent: how-to A [[webhook]] posts the full result to an endpoint you control, the moment a document finishes. It is the general-purpose route: whatever Parsli does not connect to directly, a webhook reaches. ## Setting one up Under the parser's **Outbound**, add a **Webhook** integration. | Setting | Notes | |---|---| | URL | Your HTTPS endpoint | | Method | `POST` or `PUT` | | Auth type | None, bearer token, or basic | | Auth token | Sent in the `Authorization` header | Use the test action before relying on it. A webhook pointing at a typo fails silently — extraction keeps working and the data simply never arrives. ## 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 } } ``` Field-by-field reference: [receiving results](/docs/api/receiving-results). `data` is keyed by your field names and typed to your field types. It is the same shape the API returns, so one handler covers both. ## It fires for everything A webhook catches **every** completed document on the parser, however it arrived — uploaded, emailed, sent through the inbound webhook, or submitted over the API. That makes it the single most useful integration point. One endpoint sees everything. `metadata.source_type` tells you which route a document took, so one handler can treat an emailed invoice differently from an uploaded one without a second parser. ## Authentication Set an auth type. Without one the endpoint accepts anything that finds the URL. **Bearer token** — Parsli sends `Authorization: Bearer `. Compare it against your secret and reject mismatches. **Basic** — standard HTTP basic auth. ```javascript if (req.headers.authorization !== `Bearer ${process.env.PARSLI_WEBHOOK_SECRET}`) { return res.sendStatus(401) } ``` ## The two failure modes that matter **Duplicates.** Delivery is at-least-once: a network problem between our send and your acknowledgement causes a retry, and your handler sees the same document twice. Deduplicate on `document_id`. This bug is silent — you get duplicate rows, never an error. **Empty values treated as zero.** A field the engine could not ground in the document is `null`. If a total arrives `null` and your code does `amount || 0`, you have just recorded a zero-value invoice. Check explicitly and route it to a human. ```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 res.sendStatus(200) // acknowledge first 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 }) }) ``` **Acknowledge before you work.** Return `200` immediately and queue the processing. A handler that does its work inline will eventually be slow enough to look like a failure and trigger a retry. ## Developing against it A webhook needs a publicly reachable URL, which localhost is not. Either use a tunnel, or [poll the API](/docs/api/large-and-slow-files) while developing and switch to the webhook in production. ## When it fails A failing webhook does not fail the extraction — the document still processes, the result is still stored, and other integrations still fire. The failure is recorded against the integration. To recover missed results, fetch them with `GET /api/v1/documents/{id}`. Nothing is lost. ------------------------------------------------------------------------------- ## Build with the API URL: https://parsli.co/docs/api Section: api | Intent: explanation Everything the interface does is available over HTTP. Same [[parsing-engine]], same field definitions, same results. ## Base URL and authentication ``` https://parsli.co/api/v1 ``` Every request carries an [[api-key]] as a bearer token. A key is scoped to **one [[parser]]** — which parser is inferred from the key, so no request needs a parser id. ```bash Authorization: Bearer ext_YOUR_API_KEY ``` Create keys in the parser's **Settings**. See [authentication](/docs/api/authentication). ## Choose the right route This is the decision that matters, and getting it wrong is the most common integration bug. | | Inline | Upload and poll | |---|---|---| | How | Base64 the file into the request | Get an upload URL, PUT the bytes, submit the path | | Response | The result, in the same response | A `document_id` to poll | | Requests | 1 | 3 | | Use for | Small files that extract quickly | Large files, multi-page scans, anything slow | **Inline is simpler, and it is a trap for anything big.** The whole file has to fit in a JSON request body, and the extraction has to finish inside one HTTP request. A forty-page scan will not. **Upload-and-poll has no such ceiling.** The bytes go straight to storage without passing through the API, and a worker does the extraction, so nothing is bounded by request wall-clock. Files over 300MB must use this route. If you are building something that accepts documents from users, use upload-and-poll. You cannot predict what they will send you. ## The shortest possible integration ```bash curl -X POST https://parsli.co/api/v1/extract \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file": { "name": "invoice.pdf", "type": "application/pdf", "data": "'"$(base64 -i invoice.pdf)"'" } }' ``` ```json { "success": true, "parser_id": "6f3a…", "document_id": "b71c…", "results": { "invoice_number": "INV-2026-0417", "total_amount": 1284.5, "line_items": [ { "description": "Steel brackets", "quantity": 40, "unit_price": 12.5 } ] } } ``` `results` keys are your field names. Whatever you called a field is the key you get back. ## Getting results without polling Polling is not the only option. A parser can push results to you instead — configure an outbound [[webhook]] and Parsli calls your endpoint the moment a document finishes, whatever route it came in by. That is usually the better shape for a queue: submit and forget, handle results as they arrive. See [receiving results](/docs/api/receiving-results). ## Read this next - [Authentication](/docs/api/authentication) — creating, scoping, and rotating keys - [Extract a document](/docs/api/extract-a-document) — the inline route in full - [Large and slow files](/docs/api/large-and-slow-files) — upload-and-poll in full - [Receiving results](/docs/api/receiving-results) — webhooks and polling - [Errors](/docs/api/errors) — every status code and what to do about it > Tip: The whole vocabulary — every object, field, and payload key — is available as structured data at [`/docs/dictionary.json`](/docs/dictionary.json). If you are pointing a coding agent at this API, give it that first. ------------------------------------------------------------------------------- ## Authentication URL: https://parsli.co/docs/api/authentication Section: api | Intent: how-to ## Creating a key Open the [[parser]] you want to call, go to **Settings**, and create a key. ![The API keys panel in parser Settings, with the create-key action and the one-time key reveal](docs-image:api-keys) > Important: The key is shown **once**, at the moment you create it. It is stored as a hash, so it cannot be shown again or recovered. Copy it into your secret store before closing the dialog. If you lose it, revoke it and create another — that is the only path. ## Using a key ```bash curl https://parsli.co/api/v1/extract \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file": { "name": "invoice.pdf", "type": "application/pdf", "data": "..." } }' ``` Keys are prefixed `ext_`. That prefix is deliberate: it makes keys greppable in logs and recognisable to secret scanners. ## Keys are scoped to one parser A key authenticates **and** selects. The parser is inferred from the key, which is why no endpoint takes a parser id. The consequences are worth stating plainly: - **One key cannot reach two parsers.** Calling five parsers means five keys. - **A leaked key exposes one parser**, not your account. This is the main reason for the design. - **Revoking a key breaks only what used it.** No blast radius across your integration. Give each system its own key — your production backend, your staging environment, and that one Zap should not share credentials. ## Rotating a key There is no in-place rotation, and that is intentional — an atomic swap would give you a window where you cannot tell which system is using which secret. 1. Create a new key alongside the old one. 2. Deploy the new key. 3. Confirm traffic has moved. 4. Revoke the old key. Both keys work during the overlap, so there is no downtime. ## API keys versus the inbound webhook Two ways in, for genuinely different situations. | | API key | [[inbound-webhook]] | |---|---|---| | Credential | `Authorization` header | A token inside the URL | | Create and revoke many | Yes | One per parser | | Returns extracted data | Yes | No — it accepts a document | | Best for | Code you control | No-code tools that only send a URL | Use an API key wherever you can set a header. Use the inbound webhook when a tool will only accept a URL and cannot be told to send headers. > Important: The inbound webhook's token is in the URL, so it will appear in logs, browser history, and referrer headers in a way a header-based credential will not. Treat that URL as a secret, and prefer an API key when you have the choice. ## Errors A missing, malformed, or revoked key returns `401`. See [errors](/docs/api/errors). ------------------------------------------------------------------------------- ## Extract a document URL: https://parsli.co/docs/api/extract-a-document Section: api | Intent: reference ## `POST /api/v1/extract` Sends one document and returns the result in the same response. **Headers** | Header | Value | |---|---| | `Authorization` | `Bearer ext_...` (required) | | `Content-Type` | `application/json` (required) | **Body** | Field | Type | Required | Notes | |---|---|---|---| | `file.data` | string | Yes | The file, base64-encoded | | `file.name` | string | Yes | Original filename, including extension | | `file.type` | string | Recommended | MIME type, e.g. `application/pdf` | `file.data` and `storage_path` are alternatives — send one. `storage_path` is the [large-file route](/docs/api/large-and-slow-files). ## Request ```bash curl -X POST https://parsli.co/api/v1/extract \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file": { "name": "invoice.pdf", "type": "application/pdf", "data": "JVBERi0xLjQKJcfs..." } }' ``` ```javascript const data = await fs.readFile("invoice.pdf", { encoding: "base64" }) const response = await fetch("https://parsli.co/api/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.PARSLI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ file: { name: "invoice.pdf", type: "application/pdf", data }, }), }) const { results } = await response.json() ``` ```python import base64, os, requests with open("invoice.pdf", "rb") as f: data = base64.b64encode(f.read()).decode() response = requests.post( "https://parsli.co/api/v1/extract", headers={"Authorization": f"Bearer {os.environ['PARSLI_API_KEY']}"}, json={"file": {"name": "invoice.pdf", "type": "application/pdf", "data": data}}, ) results = response.json()["results"] ``` ## Response ```json { "success": true, "parser_id": "6f3a1c88-...", "document_id": "b71c9d02-...", "results": { "invoice_number": "INV-2026-0417", "vendor_name": "Northgate Supplies", "total_amount": 1284.5, "due_date": "2026-09-01", "line_items": [ { "description": "Steel brackets", "quantity": 40, "unit_price": 12.5, "total": 500.0 }, { "description": "Delivery", "quantity": 1, "unit_price": 784.5, "total": 784.5 } ] } } ``` | Key | Type | Notes | |---|---|---| | `success` | boolean | Whether extraction completed | | `parser_id` | string | The parser the key is scoped to | | `document_id` | string | Use it to re-fetch this result later | | `results` | object | Keyed by **your field names** | ### How results are shaped `results` mirrors your [[field-definition]] exactly: - A leaf field is a scalar of its [[field-type]] — a `decimal` comes back as a JSON number, not a string. - An [[object-field]] is a nested object. - A [[table-field]] is an **array of objects**, one per row, keys matching your column names. - A field the engine could not ground in the document is **`null`**, never a guess. That last point is the one to design around. Check for `null` and route it to a human; do not treat absence as zero. ## Limits **Inline requests are bounded twice over** — by how much base64 fits in a request body, and by how long one HTTP request may run. Both are real ceilings, and a big scan will hit them. Base64 inflates a file by roughly a third, so a file comfortably under the cap on disk may not be once encoded. Anything large, multi-page, or slow belongs on the [upload-and-poll route](/docs/api/large-and-slow-files). Files over 300MB must use it. ## Cost One [[credit]] per [[page]], charged on the document's real page count. Credits are reserved before processing and **refunded if extraction fails** — a `500` costs you nothing. See [how pages are counted](/docs/billing/how-pages-are-counted). ## Errors See [errors](/docs/api/errors) for every status code. The two worth handling explicitly: `402` when you are out of credits, and `503` when a sync extraction times out — the second is a signal to move that file to the async route. ------------------------------------------------------------------------------- ## Large and slow files URL: https://parsli.co/docs/api/large-and-slow-files Section: api | Intent: how-to The [inline route](/docs/api/extract-a-document) is bounded by request body size and request wall-clock. This route is bounded by neither: bytes go straight to storage without passing through the API, and a worker does the extraction. Use it for anything large, multi-page, or slow — and for **anything a user chose**, because you cannot predict what they will upload. ## The three steps ### 1. Ask for an upload URL ```bash curl -X POST https://parsli.co/api/v1/upload-url \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file_name": "statement.pdf", "file_type": "application/pdf" }' ``` ```json { "upload_url": "https://...storage.../object/upload/sign/...", "storage_path": "8c1f.../6f3a.../api/9b2e.../statement.pdf", "expires_in": 7200 } ``` `expires_in` is seconds. Upload within that window or request a fresh URL. ### 2. PUT the bytes to that URL ```bash curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/pdf" \ --data-binary @statement.pdf ``` This request does **not** carry your API key — the signed URL is the credential, and it is single-purpose and short-lived. The bytes never touch the Parsli API, which is precisely why there is no size ceiling here. ### 3. Submit the path for extraction ```bash curl -X POST https://parsli.co/api/v1/extract \ -H "Authorization: Bearer ext_YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "storage_path": "8c1f.../6f3a.../api/9b2e.../statement.pdf", "file_name": "statement.pdf", "file_type": "application/pdf", "file_size": 48291043 }' ``` ```json { "success": true, "document_id": "b71c9d02-...", "status": "processing", "poll_url": "/api/v1/documents/b71c9d02-..." } ``` You get a `document_id` immediately. Extraction happens in the background. > Important: A `storage_path` submission is **always** processed asynchronously, even for a small file. You will get `status: "processing"` and never an inline result on this route. Do not write a client that expects `results` here. ## Collecting the result ### `GET /api/v1/documents/{document_id}` ```bash curl https://parsli.co/api/v1/documents/b71c9d02-... \ -H "Authorization: Bearer ext_YOUR_API_KEY" ``` ```json { "document_id": "b71c9d02-...", "status": "completed", "file_name": "statement.pdf", "page_count": 42, "results": { "account_number": "...", "transactions": [] }, "error": null } ``` | `status` | Meaning | |---|---| | `processing` | Still working. Poll again. | | `completed` | Done. `results` is populated. | | `error` | Failed. `error` explains why; credits were refunded. | ### Polling politely ```javascript async function waitForResult(documentId, { timeoutMs = 15 * 60_000 } = {}) { const started = Date.now() let delay = 2_000 while (Date.now() - started < timeoutMs) { const res = await fetch(`https://parsli.co/api/v1/documents/${documentId}`, { headers: { Authorization: `Bearer ${process.env.PARSLI_API_KEY}` }, }) const doc = await res.json() if (doc.status === "completed") return doc.results if (doc.status === "error") throw new Error(doc.error ?? "Extraction failed") await new Promise((r) => setTimeout(r, delay)) delay = Math.min(delay * 1.5, 30_000) // back off; large scans take minutes } throw new Error("Timed out waiting for extraction") } ``` Start around two seconds and back off. A forty-page scan takes minutes, and polling it every second just burns both our budgets. **Better still: do not poll at all.** Configure an outbound [[webhook]] on the parser and Parsli calls you when the document finishes. See [receiving results](/docs/api/receiving-results). ## Choosing a route programmatically ```javascript const INLINE_LIMIT = 4 * 1024 * 1024 // conservative: base64 adds ~33% async function extract(buffer, name, type) { return buffer.byteLength > INLINE_LIMIT ? extractViaUpload(buffer, name, type) : extractInline(buffer, name, type) } ``` Size is the easy signal. The one it misses is a small file that is *slow* — a dense two-page scan can outlast an inline request. If you see `503` on inline extractions, move that class of document to this route. ## Cost Identical to the inline route: one [[credit]] per [[page]], refunded on failure. The route you choose does not change the price. ------------------------------------------------------------------------------- ## Receiving results URL: https://parsli.co/docs/api/receiving-results Section: api | Intent: how-to 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 ```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 } } ``` | 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](/docs/api/extract-a-document). 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? | | 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. ------------------------------------------------------------------------------- ## Errors URL: https://parsli.co/docs/api/errors Section: api | Intent: reference Errors return a JSON body with a single `error` key: ```json { "error": "Parser not found or inactive" } ``` ## Status codes | Code | Meaning | Retry? | What to do | |---|---|---|---| | `200` | Success | — | — | | `400` | Malformed request | No | Fix the request. See below. | | `401` | Bad or missing [[api-key]] | No | Check the `Authorization` header and that the key is not revoked | | `402` | Out of [[credit\|credits]] | Not until topped up | Add credits or subscribe | | `403` | `storage_path` outside your namespace | No | Use a path returned by `/api/v1/upload-url` | | `404` | Parser not found, inactive, or document not found | No | Check the parser is `active` | | `500` | Extraction failed | Yes, once | Credits were refunded | | `503` | Timed out or temporarily unavailable | Yes, with backoff | Move the file to the [async route](/docs/api/large-and-slow-files) | ## The ones worth handling explicitly ### `400` — malformed request Three distinct causes, and the message tells you which: - **`Invalid JSON`** — the body did not parse. - **`Provide either file.data (base64) or storage_path`** — neither was supplied. - **`Parser has no fields`** — a Field Extraction parser with an empty [[field-definition]]. Add fields before calling it. None are retryable. The request is wrong. ### `402` — out of credits Extraction stops. Nothing is lost — parsers, field definitions, integrations, and past results are all intact, and everything resumes when credits are added. Retrying will not help until the balance changes. Alert rather than backing off, or you will retry silently for hours. See [free pages and plans](/docs/billing/free-pages-and-plans). ### `500` — extraction failed Something went wrong processing the document. **Credits are refunded**, so a failure costs you nothing. Retry once. If it fails twice on the same document, the document itself is likely the problem — a corrupt PDF, an encrypted file, an unreadable scan. Route it to a human rather than looping. ### `503` — timed out Most often the sync route running out of wall-clock on a document that needed longer. This is a routing signal, not a transient blip. Retrying the same file on the same route will usually time out again. Send it via [upload-and-poll](/docs/api/large-and-slow-files), which has no such ceiling. ## Errors that are not HTTP errors The most important failure mode returns `200`. If [[parsing-engine]] cannot ground a value in the document, that field comes back **`null`**. The request succeeded; the value is not there. That is a deliberate design choice — the engine abstains rather than inventing something plausible, because a confident wrong number in your ledger is far worse than a gap. ```javascript const { results } = await response.json() // A 200 does not mean every field was found. const missing = REQUIRED_FIELDS.filter((f) => results[f] == null) if (missing.length) return flagForReview(documentId, missing) ``` Treat `null` as "a human needs to look at this", not as zero and not as an error. ## Retry policy ```javascript const RETRYABLE = new Set([500, 503]) async function extractWithRetry(body, attempt = 0) { const res = await fetch("https://parsli.co/api/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.PARSLI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }) if (res.ok) return res.json() if (!RETRYABLE.has(res.status) || attempt >= 2) { throw new Error(`Parsli ${res.status}: ${(await res.json()).error}`) } await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)) return extractWithRetry(body, attempt + 1) } ``` Never retry `400`, `401`, `403`, or `404` — the request will not become valid. Never blind-retry `402`; it is a balance problem a human has to solve. ------------------------------------------------------------------------------- ## How pages are counted URL: https://parsli.co/docs/billing/how-pages-are-counted Section: billing | Intent: reference Parsli meters one thing: **pages**. Not documents, not parsers, not fields, not integrations. One [[page]] costs one [[credit]]. That is the whole pricing model. ## What counts as a page | You send | Pages charged | |---|---| | A one-page invoice PDF | 1 | | A five-page contract PDF | 5 | | A photograph of a receipt | 1 | | A twelve-page scanned statement | 12 | | A spreadsheet or text file | Counted by content length | Page count is resolved from the document itself at processing time — not estimated from file size, and not affected by how many fields you asked for. ## Things that are free Worth being explicit, because these are the usual worries: - **Parsers are unlimited** on every plan, free included. Make one per document type; there is no reason to economise. - **Fields are not metered.** A schema with forty fields costs the same as one with four. - **Integrations are not metered**, and every integration is available on every plan. - **Failed extractions are refunded.** Credits are reserved before processing and returned if extraction fails, so a document that errors does not cost you anything. ## Things that do cost - **Every extraction of every document**, including the first one you run while testing a new parser. There is no free test mode — a test extraction is a real extraction against real infrastructure. - **Reprocessing.** Running a document again through a changed field definition charges its pages again. > Tip: When you are iterating on a field definition, work with a one-page sample rather than the full forty-page statement. Same feedback, one fortieth of the cost. ## Predicting a bill Multiply your monthly document volume by average pages per document. Two hundred invoices a month at an average of 1.4 pages is 280 pages. Fifty statements a month at 12 pages is 600. Add them and pick the plan above the total. If the number lands awkwardly between two plans, take the lower one — you can buy [[overage]] for the difference, and you will have a real number after the first month. ## Where to check usage Your current balance and consumption are shown in the app under Settings, and on the dashboard. ## Next - [Free pages and plans](/docs/billing/free-pages-and-plans) — what you get free, and what happens after ------------------------------------------------------------------------------- ## Free pages and plans URL: https://parsli.co/docs/billing/free-pages-and-plans Section: billing | Intent: reference ## What you get for free **10 pages when you sign up.** No card, no trial to cancel. Every feature is included — all integrations, the API, unlimited parsers. The free grant is not a reduced version of Parsli; it is Parsli, for 10 pages. ## The expiry clock, precisely This is the part people get wrong, so it is worth being exact. Your free pages expire **7 days after your first successful extraction** — not 7 days after you sign up. The consequence is a good one: signing up and coming back in three weeks costs you nothing. The clock has not started. It starts the moment you first get a successful result. > Important: The clock starts on your first *successful* extraction. Signing up and doing nothing leaves your free pages intact indefinitely. Once you extract something successfully, you have 7 days to use the rest. This is deliberate. Free pages exist to let you find out whether Parsli works on **your** documents. That is a question you answer in an afternoon, not over a quarter. Pages you buy are different — those stay valid for 12 months. ## After the free pages To keep extracting, you subscribe to a monthly plan. Plans differ in one dimension only: **how many pages you get per month.** Every feature is on every plan. There is no tier where the API is locked, no tier where integrations cost extra, and no per-seat charge. The monthly allowance resets each billing cycle. Current plans and prices are on the [pricing page](/pricing) — that is the live source, and quoting numbers here would be one more thing to go stale. ## Going over your allowance If you use your monthly pages before the cycle ends, subscribers can buy extra pages at **$0.40 per page**. That is deliberately more than the per-page rate bundled into any plan. It is priced as a signal, not a trap: if you are buying overage every month, moving up a plan is cheaper, and we would rather you did that than quietly pay more. Overage is available to active subscribers. ## What happens when you run out Extraction stops. Nothing is deleted — your parsers, field definitions, integrations, documents, and past results are all still there. Add pages or move up a plan and everything resumes where it was. ## Quick reference | | Free pages | Purchased pages | Plan allowance | |---|---|---|---| | How you get them | Signing up | Buying [[overage]] | Monthly subscription | | How many | 10 | As many as you buy | Set by your plan | | Expiry | 7 days after first successful extraction | 12 months | Resets each billing cycle | | Card required | No | Yes | Yes | ## Next - [How pages are counted](/docs/billing/how-pages-are-counted) — what a document actually costs - [Teams](/docs/teams) — sharing one balance across a group ------------------------------------------------------------------------------- ## Teams URL: https://parsli.co/docs/teams Section: teams | Intent: explanation An [[organization]] is a shared Parsli account. Parsers and [[credit\|credits]] belong to the organization rather than to one person — so work does not disappear when someone leaves, and billing stays in one place. ## What is shared **Parsers.** Members see the organization's parsers, subject to their role. One person sets up a supplier-invoice parser and the whole finance team uses it. **Credits.** One balance, one invoice. Anyone's extraction draws from the same pot. See [[shared-credit-pool]]. **Documents and results.** Everything extracted belongs to the organization. ## Roles Four roles, strictly hierarchical — each includes everything below it. | Role | Can | |---|---| | **Owner** | Everything, including billing and transferring ownership | | **Admin** | Manage members and parsers | | **Member** | Create parsers, process documents, configure integrations | | **Viewer** | See parsers and results, change nothing | **Member** is the right default for people doing the work. **Viewer** suits an accountant or auditor who needs to see output without the ability to alter a field definition — a changed definition changes what future documents produce, which is not a change you want made casually. Only an owner can transfer ownership. Do that before the current owner leaves, not after. ## Adding people Invite by email from **Settings**. They receive an invitation and join with the role you assigned. For a company domain, domain-based joining lets people with matching email addresses join without an individual invitation — convenient for a team of thirty, and worth thinking about before enabling: anyone who can get an address on that domain can join. ## Letting people send documents without accounts Not everyone who has documents needs an account. Two lighter options: - **The parser's [[inbound-email-address]]** — anyone who can send email can send documents. - **The [[inbox]]** — a shared drop-off point with its own access rules. Both mean colleagues, suppliers, or clients can get documents in without occupying a seat or gaining access to your results. ## Watching the shared balance A shared pool means one person's bulk import can consume the month for everyone. - Check usage in **Settings**. - Iterate on field definitions with a **one-page sample**, not the full batch. - Remember that reprocessing charges again — a definition change re-run across a large archive is a real cost. ## Departments, budgets, and approvals If your account is an organization, a **Budget** page appears in the sidebar for admins and department heads. Each parser's **Settings** then has a **Budget & approval** section, where you nominate the field holding the document's amount, track it against a department budget, and optionally require a department head to approve before the document proceeds. That covers attributing spend and gating documents on a person. The full process built around it — vendors, bills, routing, and the screens to run them — is a [premium workspace](/docs/premium). ------------------------------------------------------------------------------- ## Premium workspaces URL: https://parsli.co/docs/premium Section: premium | Intent: explanation Everything else in these docs describes extraction: documents in, structured data out, and you decide what happens next. A [[workspace]] is the next step up. It is a complete product for one business process — with its own screens, its own objects, and its own rules — where extraction is a component rather than the point. ## The difference, concretely Parsli's general app answers *"what does this document say?"* A workspace answers *"is this bill approved, is the department within budget, has it been posted, and who is waiting on it?"* That second question needs things extraction alone does not have: a record of clients or vendors, [[department\|departments]] with people responsible for them, [[budget\|budgets]] that spend is measured against, [[approval\|approvals]] that stop a document until a person signs off, and [[routine\|routines]] that run on their own. ## What exists today **Bookkeeping** — built around clients rather than documents. Each client is a row with its own intake, its own rules, and its own state, for firms whose problem is thirty clients rather than one pile of paperwork. **Accounts payable** — bills, vendors, departments, budgets, and approval routing. A bill arrives, is attributed to a department, checked against that department's monthly budget, routed to whoever must approve it, and posted once they do. **[[routine\|Routines]] and [[connector\|connectors]]** — scheduled or triggered automation, and bespoke links to systems with no off-the-shelf integration. This is where in-house and industry-specific software gets connected. > Note: **[[department\|Departments]] and [[budget\|budgets]] are not premium.** If your account is set up as an [[organization]] and you are an admin or a department head, the **Budget** page is already in your sidebar, and each parser's Settings has a **Budget & approval** section for tracking a document's amount against a department budget. What the workspaces add is the process built around it — vendors, bills, routing, and the screens to run it. ## Why these are not self-serve A workspace is shaped around how *your* organization actually works — your departments, your approval thresholds, your chart of accounts, your client list, your systems. There is no configuration screen that could ask all of that usefully. So they are set up with you. We map the process, provision the workspace, connect what needs connecting, and onboard the people who will use it. What you get is a product fitted to your process, not a generic one you have to bend. That also means they are **premium**, and the conversation starts with a call rather than a signup form. ## Worth a conversation if - Documents currently stop and wait for a person, and nobody can see where they are - You are reconciling extracted data against budgets or limits by hand - You run the same process for many clients and the overhead is per-client, not per-document - A key system has no integration and you have been quoted a bespoke build elsewhere - Approvals happen over email, and the audit trail is somebody's inbox If instead you want documents read and the data delivered somewhere, the general app already does that — start with [your first extraction](/docs/start-here/your-first-extraction). ## Talk to us Bring the process you want to fix and, if you can, a handful of real documents. Most of the useful part of the call is working out whether a workspace is the right answer at all — sometimes a parser and a [[webhook]] is. **[Book a call](https://cal.com/talal-bazerbachi-jb8a7d/parsli-discovery-call)** Or email [support@parsli.co](mailto:support@parsli.co) describing the process, and we will tell you honestly whether this fits. ------------------------------------------------------------------------------- ## Dictionary URL: https://parsli.co/docs/dictionary Section: dictionary | Intent: reference Every term below is defined twice. The first definition is for someone running a business. The second is for someone writing code — types, payload keys, constraints. That is on purpose. It means the rest of these docs can stay in plain English without a developer ever being left guessing what a word maps to. **Each entry also lists what it is called elsewhere.** Parsli has renamed things over the years, and other tools in this category use different words for the same idea. If you learned a screen as "Integrations" and it is now "Outbound", searching the old word will still find it. Nobody should be stranded on outdated vocabulary. Search matches the plain definition, the precise definition, and every alias. -------------------------------------------------------------------------------