Fintray Extract — integration guide
Send an invoice your customer received, as a photo or PDF, and get its data back as structured JSON.
Every endpoint, field and event is described in the API reference, built from
openapi.json (OpenAPI 3.1). Limits, the versioning policy and the history of changes
are in Limits and versions and the changelog.
1. Keys
You get two keys:
ftk_test_…(sandbox): every document returns the same fictitious invoice, with no model and no cost. Build your integration with it.ftk_live_…(production): documents are actually read, and billed.
We issue keys on request; a sandbox key is yours before any contract. GET /v1/whoami checks a key:
curl https://api.fintray.ro/v1/whoami -H "Authorization: Bearer ftk_test_…"
# {"tenant_id": "ten_…", "key_id": "key_…", "mode": "test"}
The key also works in an X-API-Key header. A wrong, revoked or expired key always gets the same 401.
2. Submitting a document
POST /v1/documents:
curl https://api.fintray.ro/v1/documents \
-H "Authorization: Bearer ftk_live_…" \
-H "Idempotency-Key: invoice-8841" \
-F [email protected] \
-F external_id=invoice-8841 \
-F 'metadata={"client_id": "c-102"}' \
-F buyer_vat=RO12345678 \
-F 'buyer_name=Client SRL'
| Field | What it does |
|---|---|
file |
PDF, JPEG, PNG, GIF or WebP; at most 14 MiB and 50 pages |
external_id |
your reference, at most 128 characters. Not unique; you can filter on it. |
metadata |
up to 20 scalar values, returned unchanged |
buyer_vat |
required. The VAT number of the company that received the invoice, i.e. the customer whose account you upload it to. A Romanian CUI, with or without RO, must pass its check digit; for another EU company send the VAT number with its country prefix. It keeps the model from mistaking the vendor for the buyer, and the response tells you whether the invoice really is this customer's. |
buyer_name |
optional, at most 200 characters. The same company's name. |
mode |
async (the default, answers 202) or sync (waits up to 30 s, then 202) |
retention |
standard or zero (see section 6) |
webhook_url |
https://… for this document. Otherwise the account's URL is used. |
Or a JSON body: {"file_base64": "...", "filename": "invoice.pdf", "buyer_vat": "RO12345678", "external_id": "..."}.
A complete example, in Python: examples/submit.py.
Idempotency-Key. The same key with the same request returns the same document, with an
Idempotent-Replayed: true header. The same key with a different request gets 409. Keys are kept 24 hours.
3. The result
GET /v1/documents/{id}:
curl https://api.fintray.ro/v1/documents/doc_… -H "Authorization: Bearer ftk_live_…"
status goes queued → processing → succeeded or failed. On succeeded the response carries:
document: vendor, customer, number, dates, currency, lines, totals, IBAN;fields: per-field confidence:high(independently verified),medium(nothing contradicts it) orlow(check it before booking);warnings:totals_mismatch,vendor_vat_missing,vendor_vat_unverified,invoice_date_in_future, plus two that check the invoice againstbuyer_vat:buyer_vat_mismatch: another customer is on the invoice. Either it was uploaded to the wrong account, or it is not addressed to this customer.buyer_is_vendor: the number you sent is printed as the vendor, so this is an invoice the customer issued, not one it received.
A high field can be taken as is: on our evaluation corpus it was correct 99.6% of the time. Show a
low field to your user.
GET /v1/documents?external_id=invoice-8841 lists your documents, newest first.
4. Webhooks
When a document finishes, we POST to the document's webhook_url or, if it has none, to the
account's URL (we set it for you, on request). The event is document.succeeded or document.failed:
{"type": "document.succeeded", "created_at": "…",
"data": {"id": "doc_…", "object": "document", "external_id": "invoice-8841",
"status": "succeeded", "url": "/v1/documents/doc_…"}}
The event carries no extraction; fetch it with GET, with your key. On document.failed,
data.error.code says why. Verify the signature in the Fintray-Signature header with the
whsec_… secret we gave you; the example is examples/verify_webhook.py.
Fintray-Event-Id stays the same on every retry of one event, so deduplicate on it.
Anything other than a 2xx is retried for 24 hours. Ask for a lost event again with
POST /v1/documents/{id}/webhook.
5. Errors
Every error is {"error": {"code": "...", "message": "..."}}. The codes are in
Error codes.
6. Retention and deletion
retention |
The file | The extraction |
|---|---|---|
standard |
deleted 24 h after processing | deleted after 30 days |
zero |
deleted as soon as it is processed | deleted on the first successful GET, 24 h at most |
DELETE /v1/documents/{id} erases the document: the file, the extraction and everything describing it.
Only its status, page count and dates remain, for billing.
7. Versions, limits, usage
Fintray-Version: on every response. A key stays on the version current when it was created, and a request may name another. What we guarantee within a version and what happens at a new one: Limits and versions.- Rate limit: 120 submissions per minute per key, in bursts of 20. Over it you get
429withRetry-After. Every limit: Limits and versions. GET /v1/usage?month=2026-10: the month's billable documents and pages.