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

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": []
  }
}
FieldWhat it means
completedRead successfully and now waiting as a draft.
failedCould not be processed. failures[] gives a per-file reason.
duplicateNot an error. That bill is already posted to your ledger, so it was skipped and cost no credit. Listed in duplicates[].
doneTrue 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

StatusMeaningWhat to do
401UnauthenticatedMissing, invalid or expired token. Re-issue it.
402ai_credit_requiredOut of AI bill credit. Top up the wallet; the body's credit object reports the balance. Not retryable until topped up.
403pro_requiredThe company is not on Luca Pro, or the subscription lapsed. Not a token problem.
403Permission deniedThe token's user lacks create bill / view bill / delete bill.
404Not foundThe batch or draft does not exist in this company. Another company's ids report 404, never 403.
409import_in_progressAn identical import is still being queued. Poll the batch instead of retrying.
422Validation failedNot 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.