Import supplier bills with Luca AI
Upload bill PDFs, let Luca read them, review what it extracted, then post the ones you approve to your ledger.
Nothing reaches your ledger until you approve it. An imported PDF becomes a draft, which is Luca's reading of the document and nothing more. That is deliberate: you decide what gets posted.
Before you start
- An active Luca Pro subscription. The whole v1 API requires it; without one every call returns
403 pro_required. - AI bill credit, or an Enston Managed plan. Each bill Luca reads spends one credit.
- A token created in LUCA under Settings → API Tokens, whose user holds
create bill(to import and approve),view bill(to poll and read) anddelete bill(to discard).
The shape of it
Four calls. Reading the PDFs happens in the background, so the upload returns immediately and you poll for the outcome.
1. POST /api/v1/luca-ai/bills/imports upload PDFs -> batch_id
2. GET /api/v1/luca-ai/bills/imports/{batch_id} poll -> done: true
3. GET /api/v1/luca-ai/bills/drafts review -> draft ids
4. POST /api/v1/luca-ai/bills/drafts/{id}/approve post the bill -> bill_id
Step 1: upload the PDFs
Send multipart/form-data, not JSON. One to 25 PDFs per call, 15 MB
each at most.
The Idempotency-Key header is required, and it is what
makes retrying safe. Use your own unique id for the import (a job id, a folder
name, a hash of the filenames) and send the same value on every retry.
A repeat of a key you have already used replays the original batch instead of
importing the files again, so a retry cannot capture the same bill twice or
spend a second AI credit. Keys are remembered for 48 hours, per company.
curl -X POST "https://api.luca.pro/api/v1/luca-ai/bills/imports" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json" \
-H "Idempotency-Key: batch-2026-07-30-001" \
-F "pdfs[]=@invoice-1.pdf" \
-F "pdfs[]=@invoice-2.pdf"
{
"success": "success",
"data": { "batch_id": "9f3c1f7a-1c4d-4a2f-9a0e-2b5d6f7a8c91", "files_accepted": 2, "replayed": false }
}
Repeating that exact call returns the same batch_id with
"replayed": true and imports nothing further. Treat
replayed: true as success: your files are already in that batch, so
carry on to step 2.
Step 2: poll until the batch is done
Poll until done is true.
curl "https://api.luca.pro/api/v1/luca-ai/bills/imports/9f3c1f7a-1c4d-4a2f-9a0e-2b5d6f7a8c91" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"success": "success",
"data": {
"batch_id": "9f3c1f7a-1c4d-4a2f-9a0e-2b5d6f7a8c91",
"total": 2, "queued": 0, "processing": 0, "completed": 1,
"failed": 1, "duplicate": 0, "done": true,
"failures": [ { "filename": "invoice-2.pdf", "error": "AI could not read this bill." } ],
"duplicates": []
}
}
| Field | What it means |
|---|---|
completed | Read successfully and now waiting as a draft. |
failed | Could not be processed. failures[] gives a per-file reason. |
duplicate | Not an error. That bill is already posted to your ledger, so it was skipped and cost no credit. Listed in duplicates[]. |
done | True once nothing is queued or processing. |
Two different kinds of duplicate. The duplicate count is
about bills: that PDF is already in your ledger. Idempotency-Key
is about requests: the same import, sent twice. You need the key because the
bill-level check only covers bills already posted, so a PDF whose draft is still
awaiting review has no such guard.
Step 3: review the drafts
Defaults to the pending_review queue. total is
GST-inclusive, so it matches the figure printed on the supplier's invoice rather
than the pre-GST subtotal.
curl "https://api.luca.pro/api/v1/luca-ai/bills/drafts" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{
"success": "success",
"data": [
{
"id": 4812, "status": "pending_review",
"vendor_name": "Acme Spinning Mills Ltd",
"reference": "INV-2026-0142",
"issue_date": "2026-07-01", "due_date": "2026-07-31",
"total": "663.40",
"file_url": "https://files.luca.pro/bills/inv-2026-0142.pdf",
"posted_bill_id": null, "created_at": "2026-07-01 09:14:22"
}
]
}
Step 4: approve (this is what posts the bill)
Send an empty body to accept Luca's reading as-is. To correct it first, send the fields you want changed and they replace the extracted values. Your corrections are fed back, so later bills from that supplier are read more accurately.
curl -X POST "https://api.luca.pro/api/v1/luca-ai/bills/drafts/4812/approve" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
{ "success": "success", "data": { "bill_id": 5231, "reference": "INV-2026-0142" } }
Approving posts the bill through exactly the same path as creating one by hand, so the accounting entries are identical, and the source PDF is attached to the new bill.
When a draft will not post
Validation matches the manual bill form, so a draft Luca could not fully resolve
returns 422 naming the offending fields rather than failing deep in
the ledger. The two you are most likely to meet:
{
"message": "The additional data.vendor.id field is required.",
"errors": { "additionalData.vendor.id": ["The additional data.vendor.id field is required."] }
}
Resolve the supplier yourself and re-send it on the approve call. Likewise, send
lineItems[].account.id when a line has no account:
# resolve the ids first
curl "https://api.luca.pro/api/v1/vendors?search=Acme" -H "Authorization: Bearer YOUR_TOKEN"
curl "https://api.luca.pro/api/v1/accounts?search=Hosting" -H "Authorization: Bearer YOUR_TOKEN"
# then approve with the corrections applied
curl -X POST "https://api.luca.pro/api/v1/luca-ai/bills/drafts/4812/approve" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"additionalData": { "vendor": { "id": 7 } },
"lineItems": [ { "amount": 608.62, "quantity": 1, "account": { "id": 184 } } ]
}'
Discarding a draft
Removes it from your review queue without posting anything.
curl -X DELETE "https://api.luca.pro/api/v1/luca-ai/bills/drafts/4812" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Accept: application/json"
The AI credit already spent on reading the PDF is not refunded. The record is kept internally for audit and to improve future extractions; it simply stops appearing in your queue.
Errors worth handling
| Status | Meaning | What to do |
|---|---|---|
401 | Unauthenticated | Missing, invalid or expired token. Re-issue it. |
402 | ai_credit_required | Out of AI bill credit. Top up the wallet; the body's credit object reports the balance. Not retryable until topped up. |
403 | pro_required | The company is not on Luca Pro, or the subscription lapsed. Not a token problem. |
403 | Permission denied | The token's user lacks create bill / view bill / delete bill. |
404 | Not found | The batch or draft does not exist in this company. Another company's ids report 404, never 403. |
409 | import_in_progress | An identical import is still being queued. Poll the batch instead of retrying. |
422 | Validation failed | Not a PDF, more than 25 files, a missing Idempotency-Key, or a draft that cannot post yet. |
Full parameter and response detail for every endpoint above lives in the API Reference, and a Postman collection with worked examples is on its Download menu.