Automate your invoicing
Create renewal invoices on a schedule, check their status, and pull the PDF for delivery — the full flow, run against the mock sandbox.
1. What you can automate
Generate a customer's monthly renewal invoice — in one call from last period's
invoice, or from scratch — look it up to confirm it posted, and download the PDF
to email, all from a script or cron job. Every call below runs
against the sandbox at developer.luca.pro; point the same requests at
the live base URL https://api.luca.pro with a real token when you're
ready for live data. Creating or renewing an invoice requires the token's
create sale invoice permission; looking one up or downloading its PDF
requires view sale invoice.
2. Renew last period's invoice in one call
When the new invoice is “the same as last period's”, skip the id
mapping below entirely — give POST /api/v1/invoices/renew the old
invoice's number and the new dates, and the server copies the customer and line
items from the stored invoice:
curl -X POST "https://developer.luca.pro/api/v1/invoices/renew" \
-H "Authorization: Bearer sandbox-token" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-2026-09-INV-2026-0101" \
-d '{
"source_invoice_number": "INV-2026-0101",
"issue_date": "2026-09-01",
"service_from": "2026-09-01",
"service_to": "2026-09-30"
}'
The renewal posts immediately with a fresh invoice number, the same lines, and tax
recalculated at each tax code's current rate. due_date
defaults to issue_date plus the source's own issue→due span, so
payment terms carry over; service_from/service_to replace
the service-period text in the copied line descriptions (“From 1 September
2026 to 30 September 2026”) — leave them out to copy descriptions verbatim.
A source the copy can't reproduce faithfully (an invoice-level discount, inventory
lines, or a deleted customer/product/tax code) returns 422 with a
message saying why — create those from scratch as below. Renew shares the
create-invoice idempotency namespace, so replays return 409 with the
original invoice's id.
3. Look up your building blocks
Creating from scratch instead? Resolve the customer, product/service, and tax code ids you'll need before creating the invoice:
curl "https://developer.luca.pro/api/v1/customers?search=Acme" \
-H "Authorization: Bearer sandbox-token"
Take data[].id from the response — that's the customer_id.
curl "https://developer.luca.pro/api/v1/product-services?search=registered%20office" \
-H "Authorization: Bearer sandbox-token"
Take data[].id — that's the line item's
product_service_id — and sale_price as the default
unit_price. Search matches the name or SKU; omit it to list everything.
curl "https://developer.luca.pro/api/v1/tax-codes" \
-H "Authorization: Bearer sandbox-token"
Take a code's id for any taxed line item's tax_code_id — omit it on lines that aren't taxed.
Rebuilding from an existing invoice instead? The invoice lookups
(GET /api/v1/invoices/detail and GET /api/v1/invoices/{id})
return each line's product_service_id and — for taxed lines — its
tax_code_id, so you can copy an old invoice's ids straight into a new
payload when /invoices/renew's exact-copy behavior isn't what you want.
4. Create the invoice from scratch
Post the invoice with one line item per charge and an Idempotency-Key:
curl -X POST "https://developer.luca.pro/api/v1/invoices" \
-H "Authorization: Bearer sandbox-token" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-2026-08-cust12" \
-d '{
"customer_id": 12,
"issue_date": "2026-08-01",
"due_date": "2026-08-31",
"description": "Monthly renewal",
"line_items": [{
"product_service_id": 7,
"quantity": 1,
"unit_price": 150.00,
"description": "Registered office address fee",
"service_from": "2026-08-01",
"service_to": "2026-08-31"
}]
}'
The service period is appended to the line description
(“From 1 August 2026 to 31 August 2026”). Use one
Idempotency-Key per logical invoice — replays return 409
with the original invoice's id instead of a duplicate, safe for cron retries. If
creation returns a 5xx, check List invoices for a created-but-unposted
invoice before retrying with a new key — reusing the old key is
only remembered after full success.
5. Check and deliver it
Confirm the invoice, then fetch its PDF to email to the customer:
curl "https://developer.luca.pro/api/v1/invoices/9120" \
-H "Authorization: Bearer sandbox-token"
Or list by customer instead of id: GET /api/v1/invoices?customer_id=12.
curl "https://developer.luca.pro/api/v1/invoices/pdf?invoice_number=INV-2026-0101" \
-H "Authorization: Bearer sandbox-token"
Returns the rendered invoice as a PDF attachment (Content-Type: application/pdf) — the same document a user downloads from the app.
6. Send it automatically
Skip the manual PDF-and-email step above: POST /api/v1/invoices/{id}/send posts the
invoice if it isn't posted yet, renders it with the company's own invoice template, and queues it to
the customer's email address on file. Unlike the lookups above, this call takes the invoice's
id, not its invoice number — take it from the Create-invoice response's
data.id or from GET /api/v1/invoices. Requires the
send sale invoice permission.
curl -X POST "https://developer.luca.pro/api/v1/invoices/9120/send" \
-H "Authorization: Bearer sandbox-token"
A 200 means the email was accepted for delivery — the same guarantee you get sending it
from the app, not proof it reached the customer's inbox. There's no Idempotency-Key here:
calling it twice queues two emails, which is deliberate, so a genuine re-send (lost mail, corrected
address) needs no special handling. A customer with no email address on file, or an invoice with no
stored transaction yet (still a draft), returns 422; a company that sends invoices from
its own connected Gmail account returns 409 — that flow needs an interactive sign-in the
API can't do, so send those from the app instead.
7. Correct a mistake before money moves
Caught an error before anything's been paid? Amend it in place with
PUT /api/v1/invoices/{id} — line_items is a full replacement, not a patch,
so send the complete set of lines the invoice should have; the invoice keeps its existing number.
Prefer to start over instead? DELETE /api/v1/invoices/{id} removes the invoice, its lines
and its ledger transaction outright. Requires edit sale invoice or
delete sale invoice respectively.
curl -X PUT "https://developer.luca.pro/api/v1/invoices/9120" \
-H "Authorization: Bearer sandbox-token" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 12,
"issue_date": "2026-08-01",
"due_date": "2026-08-31",
"line_items": [{
"product_service_id": 7,
"quantity": 1,
"unit_price": 175.00,
"description": "Registered office address fee (corrected)"
}]
}'
Both calls are for straightforward, per-item invoices, and both are refused with 422 once
a payment or a credit note has been recorded against the invoice — at that point see the next section
for what a credit note can and can't do. PUT is refused further for anything the app
itself treats as a special case: an inventory invoice, an invoice whose tax is computed on the total
rather than per line item, or an invoice with no stored transaction to rebuild from — amend those in
the LUCA app instead. DELETE only refuses the payment/credit-note and no-transaction
cases; an inventory or tax-on-total invoice can still be deleted outright, since removing rows doesn't
need to reinterpret their tax the way an amend does.
Treat deleting as final — LUCA has no void or cancelled state, and there's no endpoint that brings a deleted invoice back. The invoice is gone from the company's books as far as the API is concerned.
8. Credit an invoice in full
POST /api/v1/credit-notes takes the invoice's number and a date, copies every
line from the invoice server-side, numbers the credit note from your own credit-note sequence, and
posts it — the same result as raising the credit note in the app. It always credits the invoice
in full; there's no partial-amount field. Requires create credit note.
One limit to design around: the credit is applied against the invoice, and LUCA won't clear more
against an invoice than it still owes. So a full-value credit only goes through while the invoice is
untouched — nothing paid, nothing already credited. An invoice that's been paid or
part-paid comes back 422 carrying the ledger's own wording ("… amount remaining to be
cleared is less than …"). That's not an API restriction; the app's own credit-note screen refuses
the same full-value credit, which is why it lets you type a smaller amount. Credit those in the LUCA
app. Check GET /api/v1/invoices/payment-status first if you're not sure — it reports
amount_paid.
Which leaves a gap worth naming: an invoice with money against it can't be amended, can't be deleted, and can't be credited in full through the API. That one is a trip to the app.
curl -X POST "https://developer.luca.pro/api/v1/credit-notes" \
-H "Authorization: Bearer sandbox-token" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cn-2026-08-07-INV-2026-0101" \
-d '{
"invoice_number": "INV-2026-0101",
"date": "2026-08-07",
"description": "Billed in error"
}'
Send an Idempotency-Key: reusing one returns 409 with the original credit
note's id instead of crediting the invoice twice. A 422 means the invoice can't be
credited through this endpoint: money has already been cleared against it (above), it has no stored
transaction or line items, it's an inventory invoice, its tax is computed on the total rather than
per line item, or its customer has since been deleted — raise the credit note from the LUCA app for
those.
9. Next steps
Browse the full API reference for every endpoint, or walk through reconciling a payment against open invoices to record it once it arrives.