API documentation
Integrate Robodam Scan into your own process: send an invoice PDF and get structured data (JSON) and, when needed, an accounting import file (Rivilė .eip, Centas XML or Finvalda XML). Works with Power Automate, cron, Postman and scripts.
below {BASE} — the API address https://scan-api.robodam.com (NE the app/login address is scan.robodam.com — it serves only the browser interface). For monitoring: GET {BASE}/health (a root path, not under /api/v1) returns {"status":"healthy"}.
Contents
1. Autentifikacija
Every request must carry your personal Bearer key in the header. For automation (Power Automate, cron, scripts) create a permanent API key: Settings → Account → API keys — raktas (rbs_…) is shown once and stays valid until you revoke it. For quick manual tests the “Test key” above also works, but it is temporary (a session key) — do not use it for automated flows.
Authorization: Bearer <your-key>2. Reading an invoice (PDF → JSON)
Synchronous reading: you send the PDF and get the data in the same response (no extra requests or polling).
{BASE}/api/v1/invoices/extract-directRequest
multipart/form-data, the document is passed in the field file. Accepted formats: PDF and photos (JPG / PNG / HEIC) — photos are converted to PDF on the server automatically. Optional parameters:
| Parametras | Numatyta | Value |
|---|---|---|
| save_to_profile | true | Whether to save the reading in your history. |
| allow_duplicate | false | Jei false — sending the same file again returns the previous result free of charge (see section 5). |
| include_export | — | rivile, centas arba finvalda — also return the accounting file (see section 6). |
| include_expense | false | If true — the response also carries the expense block: a suggested expense account and VAT class for every line, from your company's rules and accounting memory. null means expense assignment is off for this document's company (on an accounting-firm account, also when the document is not yet assigned to any company); on a sales document the lines carry no codes. |
| split_documents | false | When true, a file that holds several documents (a fuel-card bundle: one VAT invoice per country) is returned as separate documents in documents[]; each is billed as one invoice. Without this parameter a bundle is never returned as separate documents. No effect on a file that is one document. Works only for accounts where Robodam has enabled fuel-card statement reading (enabled per account); for other accounts the parameter is ignored. |
Limits: a PDF or a JPG / PNG / HEIC photo; file up to 10 MB; up to 20 pages (/extract-direct has its own cap, but by default it equals the /upload cap — 20 each; an administrator can raise the /upload cap for a specific customer, up to 50, and the /extract-direct cap does not change with it; for accounts where Robodam has enabled fuel-card statement reading, a statement whose printed totals reconcile to the cent is exempt from the page cap — up to 200 pages; for every other account the cap is unchanged); 30 requests/min. A PDF with multiple invoices is treated here as a single document (splitting only happens via /upload).
The whole request (the multipart body) — up to 20 MB: anything larger is refused by the gateway before it reaches the API (413 without a JSON body). One file — up to 10 MB unless an administrator set another limit for your account (at most 18 MB). For /batch-upload the limit applies to all files together.
Example (cURL)
curl -X POST "{BASE}/api/v1/invoices/extract-direct" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@saskaita.pdf"Response (200 OK)
{
"success": true,
"filename": "saskaita.pdf",
"confidence_score": 100, // 0–100, AI confidence
"auto_validated": true, // true kai confidence_score >= 85
"validation_warnings": [], // text for a human
"validation_issues": [], // the same for a machine — with a code (see below)
"risk_flags": [], // worth checking before booking
"document_type": "invoice", // invoice / credit_note / receipt / unknown / ...
"from_cache": false,
"duplicate_of": null,
"invoice_id": "3f2b9c1e-...", // ID of the saved invoice in Scan; null when save_to_profile=false
"extracted_data": { ... }, // invoice fields (see section 3)
"export": null // filled in only with include_export
}3. Extracted fields (extracted_data)
All fields are optional — if a field is not on the invoice, it is null or omitted.
{
"invoice_number": "SF Nr. 70",
"invoice_date": "2026-05-29", // YYYY-MM-DD
"due_date": "2026-06-12",
"currency": "EUR",
"subtotal": 1100.0, // suma be PVM
"tax_amount": 231.0, // VAT amount (absent when 0% / no VAT)
"tax_rate": 21, // dokumento PVM %, sveikas sk.
"total_amount": 1331.0, // suma su PVM
"bank_account": "LT74...",
"document_type": "invoice",
"vendor": { // vendor (seller)
"name": "MB Decentrum",
"company_code": "306265414", // company code
"tax_id": "306265414", // VAT code (LT… if a VAT payer)
"address": "Ceikiniu g. 3, Lietuva"
},
"customer": { // buyer
"name": "UAB Robodam",
"company_code": "...",
"tax_id": "LT100017242115",
"address": "Kaunas, Vytauto pr. 27"
},
"line_items": [
{
"description": "Programavimo paslaugos",
"quantity": 1.0,
"unit": "vnt",
"unit_price": 1100.0, // vieneto kaina be PVM
"total": 1100.0, // line total excluding VAT
"tax_rate": 21, // line VAT % (if absent — the document tax_rate)
"item_number": "..." // the vendor's item code, if any
}
],
"notes": "..."
}The response may also contain _validation bei _recovery blokai — tai vidiniai diagnostikos duomenys, juos galite ignoruoti.
4. Quality signals
confidence_score pasako, kiek we are. These two fields tell you kas negerai — and are meant for a program, not a human. Both are always in the response; for a clean document they are empty lists.
validation_issues — what we checked
{
"code": "total_mismatch", // stable — safe to branch on
"field": "total_amount",
"message": "Total amount (100.0) does not match calculated total (121.00)",
"severity": "warning", // warning | error
"kind": "semantic" // format | semantic | extraction
}kind skiria our check (format — format, semantic — amounts and dates) from the model's own note about its reading (extraction). The latter are free text and carry no code — inventing one would promise a contract we would not keep. The most common codes: total_mismatch, line_sum_mismatch, tax_amount_mismatch,date_format, due_before_invoice, iban_checksum.The textual validation_warnings stays — it is meant to be shown to a person.
risk_flags — what is worth checking
Tai faktai, o ne verdiktai. You make the decision, because you know the direction:
own_company_in_vendor_slot— your client's company is in the seller's lauke. On a purchase invoice that means the parties are swapped; on a sales invoice this is normal.same_party_both_sides— the same company on both sides. There is no case where this is correct.duplicate_invoice_number— the same supplier has already sent an invoice with this number. The fieldduplicate_of_invoice_numbergives the previous invoice's id, amount and date — the difference in amounts is exactly what tells you whether the supplier rewrote the document or simply resent it. File-level duplicate checking cannot see this: a rewritten document is different bytes.credit_note_signs_applied— the document was recognised as a credit note, sototal_amount,subtotaland the line totals forced negative. The numbers in the response may not match the signs printed in the PDF.
The list of codes keeps growing. An unfamiliar code should be ignore themrather than treated as an error — that way your integration will not break when we add a new one.
5. Resending
If you resend the same file (the same bytes, the same account), then allow_duplicate yra false (default) — returns the earlier extraction, free of charge(from_cache: true, duplicate_of points to the earlier ID; the AI is not run again). This way a Power Automate flow that reads the same folder every day is not billed again. To force a fresh extraction, pass allow_duplicate=true.
Retrying with your own document ID
Send your own document ID in the Idempotency-Key header or the external_id form field (1–128 characters) — on /upload, /extract-direct and /batch-upload. It is stored as the invoice's external_id, returned in the response and filterable (GET /invoices?external_id=…). A repeat with the same ID answers the FIRST result — even when the file was scanned again and its bytes differ — so no second invoice is created or billed.
curl -X POST "{BASE}/api/v1/invoices/upload" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: DMS-2026-000123" \
-F "file=@saskaita.pdf"For /batch-upload, send one external_id field per file, in the same order; a single Idempotency-Key header for several files is refused (422).
Note: with save_to_profile=false the result is not stored, so a repeated request is a new extraction and a new charge (an ID with this parameter is refused). Automations that retry after an error or a timeout should keep the default save_to_profile=true and send their own ID.
6. Accounting export (Rivilė / Centas / Finvalda)
You can get the file in two ways. Both return the file base64 format inside the JSON (content_base64) — decode it and save it.
A. Together with the reading (one file)
Add the parameter to the section 2 request include_export (rivile, centas arba finvalda) — atsakyme atsiras export field with that invoice's file:
{BASE}/api/v1/invoices/extract-direct?include_export=rivile{BASE}/api/v1/invoices/extract-direct?include_export=centasexport field has the same structure as the batch response (see below).
B. For selected / filtered invoices (batch)
{BASE}/api/v1/invoices/export/{format}{format} = rivile, centas or finvalda. The request body is either specific ids or filters (as in the invoice list, up to 1000):
{ "invoice_ids": ["<id1>", "<id2>"] }
// arba
{ "start_date": "2026-05-01", "end_date": "2026-05-31" }Response — Rivilė (.zip containing .eip files):
{
"format": "rivile",
"filename": "Rivile_eksportas-20260612-061255.zip",
"content_type": "application/zip",
"content_base64": "UEsDBB...", // base64 → bytes → save as .xml
"problems": [], // warnings for the bookkeeper (see below)
"skipped_count": 0,
"purchase_count": 1,
"sale_count": 0
}Response — Centas (.xml, or .zip when there are both purchases and sales):)
{
"format": "centas",
"filename": "Centas_eksportas-20260612-061255.xml",
"content_type": "application/xml", // or application/zip
"content_base64": "PD94bWw...", // base64 → bytes → save as .xml
"problems": [],
"skipped_count": 0,
"purchase_count": 1,
"sale_count": 0
}| Format | File | Contents |
|---|---|---|
| rivile | .zip | Klientai / Prekes / Pirkimai / Pardavimai .eip (ISO-8859-13) |
| centas | .xml or .zip | Pirkimai.xml / Pardavimai.xml (UTF-8) |
| finvalda | .xml or .zip | Pirkimai.xml / Pardavimai.xml (UTF-8) |
problems — warnings meant for a person (skipped invoices, a 0%/reverse-charge class that needs confirming) — show them before importing.
7. Company settings
Some values are not on the invoice — your company code (to tell purchases from sales), VAT class codes. Save them once and every export will use them:
{BASE}/api/v1/export-config?format={format}{BASE}/api/v1/export-config?format={format}{format} = rivile, centas or finvalda. PUT body (send only what you want to set; the rest stay at their defaults). The fields differ per format:
Rivilė:
{ "config": {
"own_company_code": "304827491",
"own_vat_code": "LT100001738313",
"vat_class_map": { "21": "PVM1", "0": "PVM5" },
"partner_group": "PT001",
"product_group": "PR001",
"op_tip_purchase": 1,
"op_tip_sale": 51
} }Centas:
{ "config": {
"own_company_code": "304827491",
"own_vat_code": "LT100001738313",
"vat_class_map": { "21": "PVM1", "0": "PVM5" },
"account_purchase": "SANAUDOS",
"account_sale": "PAJAMOS"
} }Finvalda:
{ "config": {
"own_company_code": "304827491",
"own_vat_code": "LT100001738313",
"op_type_purchase": "1",
"op_type_sale": "51",
"journal_purchase": "PIRK",
"journal_sale": "PARD",
"import_param": "IMP1",
"default_client_code": "KL001",
"account_purchase": "6001",
"account_sale": "5001"
} }Finvalda PVM klasės lauko neturi — klasę parenka imp_param, tad vat_class_map šiam formatui netaikomas.
Use snake_case keys (an unknown key returns 422). Important: the 0% / reverse-charge VAT class ("0") defaults to PVM5 — confirm the correct code with your accountant (a wrong but valid code silently distorts VAT accounting). Set abu — own_company_code ir own_vat_code: purchase/sale is recognised when either one matches (the AI sometimes fails to read the company code, especially on foreign invoices).
8. Errors and limits
| Code | Value |
|---|---|
| 401 | The key is missing or invalid. |
| 403 | The key is valid, but the action is not allowed — this is not an authentication error. Most often it is one of the billing errors (payment_blocked, trial_exhausted, insufficient_credits): do not repeat the request until a human pays the invoice, signs the agreement or tops up the credit. detail.error names the exact reason (see section 10). |
| 400 / 413 / 422 | Invalid file (not a PDF / JPG / PNG / HEIC, > 10 MB, over the page cap) or invalid parameters. |
| 404 | No invoices were found for the export with the given ids/filter. |
| 422 (export) | All selected invoices were skipped (reasons are in the error text) — no file was created. Also: unknown format or an invalid config. |
| 429 | Rate limit exceeded — wait the number of seconds given in Retry-After, then try again. Limits are counted per key and differ per path (under /api/v1/invoices): /upload — 120/min, /extract-direct and /batch-upload — 30/min, /export/{format} and /{id}/retry — 10/min. |
9. The bulk path: upload → poll → fetch → export
When there are many documents, or long ones, use the asynchronous path: upload now and collect the results once they are ready.
- Upload —
POST /invoices/upload(one file) or/batch-upload(up to 100 files)./batch-uploadalways answers 201 with a result per file, even when every file was refused — check eachresults[].status. - Poll —
GET /invoiceswithsort_by=updated_at&sort_order=asc&page_size=100. While the response'snext_cursoris notnull, send it back ascursor. - Fetch —
GET /invoices/{id}returns every extracted field (the list shortens them). - Export —
POST /invoices/export/{format}withinvoice_ids(§6).
GET {BASE}/api/v1/invoices?sort_by=updated_at&sort_order=asc&page_size=100&updated_after=2026-09-27T08:00:00Z
→ { "items": [ … ], "total": 240, "next_cursor": "eyJ1Ijoi…" }
GET {BASE}/api/v1/invoices?sort_by=updated_at&sort_order=asc&page_size=100&updated_after=2026-09-27T08:00:00Z&cursor=eyJ1Ijoi…
→ { "items": [ … ], "next_cursor": null }Recipe: safe incremental sync
Keep the last row's updated_at. Start the next sync with updated_after = that value minus 2 minutes and walk next_cursor. The overlap is required: a record committed later can carry an earlier stamp. You may receive the same invoice twice — recognise it by id. Paging by page can skip a row while the data changes — do not use it to sync.
Status lifecycle (status)
uploaded → processing → a final status. A multi-page PDF first goes through detection_pending; when it holds several invoices, the parent row becomes split and each invoice is processed as its own row.
| Status | Value | Final? |
|---|---|---|
| uploaded | Uploaded, waiting in the queue. | no |
| detection_pending | Multi-page PDF — finding where one invoice ends and the next begins. | no |
| processing | Being read. | no |
| processed | Read — the result is ready. | yes |
| unprocessed | Unreadable: required fields are missing. Not billed. | yes |
| failed | Reading failed. Not billed; can be retried with POST /invoices/{id}/retry. | yes |
| rejected | Rejected at review. | yes |
| split | The parent row of a multi-invoice PDF — not read itself; the invoices are separate rows. | yes |
| skipped_trial_exhausted | Skipped: the trial allowance ran out. Not billed. | yes |
Fields in the list and in the detail
In the list extracted_data is shortened to five fields — vendor.name, total_amount, subtotal, tax_amount, currency — with the same names as in the detail (§3). Lines, the buyer, dates and the number are only in GET /invoices/{id}. Both carry updated_at and external_id. Field names never change — new ones are only added.
10. The contract: OpenAPI, version and error codes
The machine-readable contract — openapi-public.json (OpenAPI 3.1, without the administration routes). Every response carries the X-Contract-Version header (a date): within one version changes are only additions; a removal or a change of meaning means a new date. Ignore fields and codes you do not know.
Every response carries X-Request-ID, and every error body also request_id: quote it when you write to us. The error code is detail.error (branch on it); the text for a person is detail.message.
HTTP/1.1 403 Forbidden
X-Request-ID: 5f0c…
X-Contract-Version: 2026-09-30
{ "detail": { "error": "trial_exhausted", "message": "…", "retryable": false },
"request_id": "5f0c…" }Rate limits are counted per API key (or signed-in user), not per IP address — only the sign-in and registration routes are limited per IP. Limited responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 also Retry-After (seconds).
Error codes
| Code | Value | Retry? |
|---|---|---|
| invalid_token | The session is no longer valid — sign in again. | no — a person must act |
| user_inactive | The account is inactive. Contact the administration. | no — a person must act |
| api_key_requires_session | A new API key can only be created while signed in: Settings → Account → API keys. | no — a person must act |
| api_key_limit_reached | The active API key limit was reached. | no — a person must act |
| payment_blocked | Your account is blocked over unpaid invoices. Please contact us. | no — a person must act |
| trial_exhausted | The free trial is used up. | no — a person must act |
| insufficient_credits | Your credit limit is used up. Contact us to raise it. | no — a person must act |
| pdf_only | Only PDF files can be uploaded | no — a person must act |
| invalid_content_type | Invalid content type — PDF only. | no — a person must act |
| file_too_large | The file is too large. | no — a person must act |
| invalid_pdf_content | Invalid PDF: the content does not match the PDF format. | no — a person must act |
| invalid_pdf_mime | Invalid PDF: a different file type was detected. | no — a person must act |
| pdf_too_complex | The PDF cannot be processed safely: its page dimensions are too large or its page count is invalid. | no — a person must act |
| too_many_pages | The PDF has too many pages ({page_count}). The maximum is {max_pages}. | no — a person must act |
| too_many_pages_for_sync | The document is too long for synchronous extraction ({page_count} pages; limit {max_pages}). Use the asynchronous path. | no — a person must act |
| too_many_files | Too many files. | no — a person must act |
| duplicate_invoice | This invoice has already been uploaded. | no — a person must act |
| file_empty | The file is empty. | no — a person must act |
| image_unreadable | Could not read the photo — upload another one. | no — a person must act |
| image_too_large | The photo is too large to process safely. | no — a person must act |
| heic_unsupported | HEIC is not supported yet — upload a JPG or PNG. | no — a person must act |
| extraction_failed | Could not extract data from the PDF. | no — a person must act |
| extraction_too_large | The document is too long to read in one pass — retrying will not help. Read the PDF locally or send it in smaller parts. Not billed. | no — a person must act |
| invalid_external_id | Invalid document ID — 1–128 visible characters, one per file. | no — a person must act |
| external_id_conflict | This document ID already belongs to an invoice whose result is not ready yet — wait, or use another ID. | no — a person must act |
| invalid_filter_value | Invalid filter value — choose one from the list. | no — a person must act |
| invalid_cursor | The list position is no longer valid — start again from the first page. | no — a person must act |
| unknown_company | The company “{company_code}” is not registered — choose a company from the list. | no — a person must act |
| invoice_not_found | Invoice not found. | no — a person must act |
| pdf_not_found | The PDF was not found on the server — it may have been deleted. | no — a person must act |
| export_no_invoices | No invoices match the export criteria. | no — a person must act |
| export_all_skipped | Every selected invoice was skipped — no file was created. | no — a person must act |
| invalid_export_config | Invalid export configuration. | no — a person must act |
| key_unsendable | Key not saved: it contains characters that cannot be sent — copy the key again, without quotes or spaces. | no — a person must act |
| invoice_save_failed | Could not save the invoice — try again. | yes, after a pause |
| internal_error | Server error — try again in a minute. If it keeps happening, write to us. | yes, after a pause |
| service_unavailable | The service is temporarily unavailable. Try again in a minute. | yes, after a pause |
| extraction_internal_error | An internal extraction error occurred. | yes, after a pause |
Treat an unknown code as “do not retry” for now — try again in a later run, but never treat it as permanent. A response without retryable does not mean false. Billing errors (payment_blocked, trial_exhausted, insufficient_credits) are always 403 with retryable: false: stop the whole run and tell a human, do not skip the file.
Questions about the integration? Get in touch — kontaktai.