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.