Developers › Get paid without a website
Invoicing
Verified 6 November 2026Explain this to your team →
An invoice is a bill you raise against a customer, with a number, a due date and a hosted page where they pay by card. You build it as a draft, finalize it when it is right, and send it. If it goes unpaid we email reminders on a schedule until it is paid, voided or written off.
total of 135000 is $1,350.00. The one exception is a tax rate’s percentage, which is a percentage and not an amount: 12.5 means 12.5%.The lifecycle
| Status | What it means |
|---|---|
draft | Being written. No number, nothing owed, invisible to the customer. Lines can be added, edited and removed; the totals are computed from them on every read. |
open | Finalized. It has a number from your own consecutive sequence and its totals are frozen. This is the state money is taken in. |
paid | What has been received covers the total. Final. |
void | Raised in error. Keeps its number and its lines, so the copy in the customer’s inbox still matches something. Final. |
uncollectible | Owed and written off. Not the same as void: the debt was real. Final. |
DELETE /v1/invoices/:id exists so that code written against Stripe does not break, but it voids the draft rather than deleting it and the response says "status": "void". A bill that was raised stays on the record, so “was this customer ever invoiced?” is always answerable.Create, add lines, finalize, send
# 1. A draft. Its money comes from the lines you add next, not from this call.
curl https://api.secureprocessing.app/v1/invoices \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"customer": "cus_9aB1cD3eF5gH7iJ9kL1mN3oP",
"currency": "usd",
"days_until_due": 14,
"memo": "Thanks for diving with us."
}'
# 2. A line. Quote a price, or write the line yourself in minor units.
curl https://api.secureprocessing.app/v1/invoice_items \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"invoice": "in_1aB3cD5eF7gH9jK1lM3nP5qR",
"description": "Blue Hole Two-Tank Dive",
"unit_amount": 50000,
"quantity": 2,
"tax_rate": "txr_5jK7lM9nP1qR3sT5uV7wX9yZ"
}'
# 3. Finalize. Assigns the number, freezes the totals, and moves it to open.
curl -X POST https://api.secureprocessing.app/v1/invoices/in_.../finalize \
-H "Authorization: Bearer sk_test_..."
# 4. Send. Emails the customer a link to the hosted page. A 503 or 400 means it did not go.
curl -X POST https://api.secureprocessing.app/v1/invoices/in_.../send \
-H "Authorization: Bearer sk_test_..."const invoice = await sp.invoices.create({
customer: customer.id,
currency: "usd",
days_until_due: 14,
});
await sp.invoiceItems.create({
invoice: invoice.id,
description: "Blue Hole Two-Tank Dive",
unit_amount: 50000, // minor units — $500.00
quantity: 2,
tax_rate: gst.id,
});
const open = await sp.invoices.finalize(invoice.id);
console.log(open.number); // "INV-0007"
console.log(open.total); // 112500 — minor units, $1,125.00
console.log(open.hosted_invoice_url); // send the customer here
await sp.invoices.send(invoice.id);days_until_due is counted from the moment you finalize, not from when you created the draft — a draft that sat for a fortnight is not already overdue the day you send it. Give due_date instead if you want a fixed date. Not both.
send answers 200 only once the email has gone to our email provider. That is when sent_at is set and invoice.sent fires. If the email did not go, sent_at stays as it was, no event fires, and the answer is one of these two:
| Answer | What it means | What to do |
|---|---|---|
| 503 email_not_sent | The email could not be sent right now. It may not have reached the customer. | Retry. The same Idempotency-Key is fine: a 503 is never stored against it, and the SDK retries it for you. |
| 400 email_recipient_rejected | Our email provider refused the customer's address. param is customer.email. | Retrying will not help, and the 400 is stored against your Idempotency-Key. Send the customer the hosted_invoice_url another way. |
[TEST] in the subject, and the same goes for reminders and receipts. If the customer's address is not a member of your team, nothing is sent. The answer is still a 200, with test_email_suppressed: true, sent_at stays as it was and no event fires. sent_at may already hold an earlier send's time, so read test_email_suppressed to know what that request did. To see the email and invoice.sent, put a team member's address on the test customer.A failed send changes nothing else. The invoice is still open, and the customer can still pay at its
hosted_invoice_url, which you can send them yourself. Within 24 hours, a retry with the same Idempotency-Key is built not to email the customer twice: when an earlier attempt may have reached them and only the answer was lost, the retry reuses our email provider’s idempotency key for it. A new key is a new email.Invoice numbers
Numbers come from a counter of your own, per account and per mode, and they are consecutive with no gaps — INV-0001, INV-0002, and so on. Five finalizes at the same instant get five consecutive numbers. A finalize that fails puts its number back rather than burning it, which is why we do not use a database sequence: a gap is a question an auditor asks, and “a transaction rolled back” has never been an answer anybody accepts.
Test mode has its own counter, so rehearsing does not advance your real numbering.
Tax
A tax rate is a row you own. Belize GST — 12.5%, added on top of the line — is created for you when your account is. Put a rate on a line and the tax is computed per line, rounded half-up to the minor unit, and added into the invoice’s tax.
curl https://api.secureprocessing.app/v1/tax_rates \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"display_name": "GST", "percentage": 12.5, "inclusive": false, "jurisdiction": "Belize"}'| Line | Rate | Tax | Why |
|---|---|---|---|
| 1 × 1000 | 12.5% exclusive | 125 | Added on top. The line is 1000 net, 1125 gross. |
| 3 × 333 = 999 | 12.5% exclusive | 125 | 124.875 rounds half-up to 125, which is what a calculator gives. |
| 1 × 1000 | 12.5% inclusive | 111 | Backed OUT: 1000 ÷ 1.125 is 889 net, so the tax is 111 — not 125. |
active: false on the old one — the same rule, for the same reason, as a price’s amount.The hosted page
hosted_invoice_url is where you send the customer. It shows the line items, the totals, anything you put in memo and footer, and a button that takes them to a secure page hosted by the bank. You never see a card number and neither do we.
in_ id goes in API responses, webhook bodies and your own logs; the hosted URL opens a page naming your customer and what they owe. They are deliberately different strings so the id is safe to paste into a support ticket. Treat the URL as the credential it is.invoice_pdf is the same document as an A4 PDF, generated on request. It needs your API key, so it is for your records and your emails rather than something to link a customer to.
Partial payments
Set allow_partial: true and the hosted page shows an amount box. The customer can pay any part of what is outstanding; the balance stays owed and keeps being chased. Each payment fires invoice.payment_succeeded, and the one that settles the bill also fires invoice.paid, once, alongside its own invoice.payment_succeeded.
amount_paid then exceeds total and amount_due goes negative. Refund the difference — the dashboard flags it, and POST /v1/invoices/:id/refund_overpayment refunds it from the most recent payment. Like any partial refund, one made before the bank has settled that payment is refused with 400 refund_not_yet_possible; see refunds. The overpaying payment fires invoice.payment_succeeded and nothing else: the invoice was already paid, so invoice.paid does not fire again.Reminders
An open invoice with a due date is chased automatically, at most one email a day, on this schedule:
| When | What it says |
|---|---|
| 3 days before the due date | Due in 3 days. |
| On the due date | Due today. |
| 3 days late | 3 days overdue. |
| 7 days late | 7 days overdue. |
| 14 days late | 14 days overdue. The last one. |
Reminders stop the moment the invoice is paid, voided or marked uncollectible, and they stop for good after the last one — an invoice still unpaid a fortnight after it was due needs a person, not a sixth email. An invoice you finalize the day before it is due does not get the earlier reminders retroactively; it starts at the stage that actually applies. Each one fires invoice.reminder_sent.
Charging a saved card
POST /v1/invoices/:id/pay charges a card the customer has already saved with you, with nobody present. It works in the bank’s test environment, but the bank has not yet confirmed it for our account, and a merchant-initiated charge has never been proved in production. Treat it as unproven until we say otherwise.await sp.invoices.pay(invoice.id, { payment_method: "pm_2xY4zA6bC8dE0fG2hI4jK6lM" });There is no way to charge a card the customer has not already saved. Send the invoice and let them pay on the hosted page — which is what send is for.
A 202 — an invoice whose amount_paid has not moved — means the charge was sent and the bank’s answer has not reached us yet. The card may already have been charged, so do not charge it again: the payment settles on its own within minutes, and invoice.payment_succeeded follows if it went through — with invoice.paid alongside it if that payment settled the invoice.
Events
Subscribe on the webhooks page. The one to fulfil on is invoice.paid; the one to reconcile a bank statement against is invoice.payment_succeeded, which fires once for every payment. Fulfil idempotently, keyed on the invoice id: delivery is at-least-once, so the same event can reach you more than once.
These match Stripe. invoice.payment_succeeded fires for every payment attached to the invoice, including the one that settles it and any overpayment after it. invoice.paid fires exactly once, when the invoice changes to paid, and never for a later overpayment. A payment that settles the invoice therefore fires both, alongside each other, each carrying the invoice as it stood after that payment. They share the same created and no order between them is guaranteed — in the events list or in delivery — so do not make one handler wait for the other.
| Event | When |
|---|---|
| invoice.created | A draft exists. |
| invoice.finalized | It has a number and its totals are fixed. |
| invoice.sent | It was emailed to the customer. Never fires for a send that failed. |
| invoice.payment_succeeded | A payment landed. Fires for every payment, including the settling one and an overpayment. |
| invoice.paid | It became paid. Fires once, never for a later overpayment. |
| invoice.voided | Cancelled. |
| invoice.marked_uncollectible | Written off. |
| invoice.reminder_sent | An automatic reminder went out. |
| invoice.updated | A draft’s details were edited with POST /v1/invoices/:id, or an open invoice’s memo, footer or metadata changed. Adding or editing a line does not fire it. |