Invoices
An Invoice represents a billing statement — a set of line items for a billing period, with a total amount, currency, and lifecycle state.
Invoice Lifecycle
Draft → Open → Paid
→ Void
- Draft — Being assembled. Line items can be added. Not visible to customers yet.
- Open — Finalized with a sequential number (
INV-XXXXXX). Payment collection begins. - Paid — Payment received (via Stripe or manual record).
- Void — Cancelled before payment. A reason is required.
Invoice Types
| Type | Description |
|---|---|
Standard | Regular billing invoice |
CreditNote | Negative invoice offsetting a prior charge |
ProForma | Estimate/quote (not legally binding) |
Key Fields
| Field | Type | Description |
|---|---|---|
id | string (UUIDv7) | Invoice ID |
invoice_number | string? | Sequential number assigned on finalization (e.g., INV-000042) |
status | enum | Draft / Open / Paid / Void |
invoice_type | enum | Standard / CreditNote / ProForma |
customer_id | string | Owning customer |
subscription_id | string | Source subscription |
total_nanos | string | Total in pico-units (i128 as string) |
currency | string | ISO 4217 |
due_date | date | Payment due date |
period_start / period_end | datetime | Billing period |
line_items | array | Charges detail |
applies_to_invoice_id | string? | For credit notes: which invoice this offsets |
voided_reason | string? | Why the invoice was voided |
finalized_at | datetime? | When the invoice was finalized |
Get an Invoice
# [curl]
curl https://api.bill.sh/v1/invoices/01944b1f-0000-7000-8000-000000000004 \
-H "Authorization: Bearer $TOKEN"
# [Python]
import requests
resp = requests.get(
f"https://api.bill.sh/v1/invoices/{invoice_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
invoice = resp.json()
total_usd = int(invoice["total_nanos"]) / 1e12
print(f"Invoice {invoice.get('invoice_number', 'Draft')}: ${total_usd:.2f} {invoice['currency']}")
for line in invoice.get("line_items", []):
line_usd = int(line["amount_nanos"]) / 1e12
print(f" {line['description']}: ${line_usd:.2f}")
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/v1/invoices/${invoiceId}`,
{ headers: { "Authorization": `Bearer ${TOKEN}` } }
);
const invoice = await resp.json();
const totalUsd = (BigInt(invoice.total_nanos) / BigInt(1e12)).toString();
console.log(`Invoice ${invoice.invoice_number ?? "Draft"}: $${totalUsd} ${invoice.currency}`);
// [Go]
req, _ := http.NewRequest("GET",
"https://api.bill.sh/v1/invoices/"+invoiceID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var invoice map[string]interface{}
json.NewDecoder(resp.Body).Decode(&invoice)
fmt.Printf("Invoice %v: %v %v\n",
invoice["invoice_number"], invoice["total_nanos"], invoice["currency"])
Response:
{
"id": "01944b1f-0000-7000-8000-000000000004",
"invoice_number": "INV-000001",
"status": "Open",
"invoice_type": "Standard",
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"subscription_id": "01944b1f-0000-7000-8000-000000000003",
"total_nanos": "9990000000000",
"currency": "USD",
"due_date": "2026-03-28",
"period_start": "2026-02-28T00:00:00Z",
"period_end": "2026-03-28T00:00:00Z",
"line_items": [
{
"id": "01944b1f-0000-7000-8000-000000000010",
"description": "Startup Plan — Monthly",
"quantity": "1",
"unit_amount_nanos": "9990000000000",
"amount_nanos": "9990000000000",
"currency": "USD",
"is_tax": false
}
],
"finalized_at": "2026-02-28T10:00:00Z",
"created_at": "2026-02-28T00:00:00Z"
}
Finalize an Invoice
Assigns a sequential invoice number and transitions from Draft → Open:
# [curl]
curl -X POST https://api.bill.sh/admin/v1/invoices/$INV_ID/finalize \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Idempotency-Key: finalize-$INV_ID"
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/invoices/{inv_id}/finalize",
headers={
"Authorization": f"Bearer {ADMIN_TOKEN}",
"Idempotency-Key": f"finalize-{inv_id}",
},
)
invoice = resp.json()
print(f"Finalized: {invoice['invoice_number']} — {invoice['status']}")
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/invoices/${invId}/finalize`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Idempotency-Key": `finalize-${invId}`,
},
}
);
const invoice = await resp.json();
console.log(`Finalized: ${invoice.invoice_number} — ${invoice.status}`);
// [Go]
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/invoices/"+invID+"/finalize", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Idempotency-Key", "finalize-"+invID)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var invoice map[string]interface{}
json.NewDecoder(resp.Body).Decode(&invoice)
fmt.Printf("Finalized: %v — %v\n", invoice["invoice_number"], invoice["status"])
Void an Invoice
# [curl]
curl -X POST https://api.bill.sh/admin/v1/invoices/$INV_ID/void \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Duplicate invoice — customer billed twice in error",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith"
}'
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/invoices/{inv_id}/void",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={
"reason": "Duplicate invoice — customer billed twice in error",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith",
},
)
print(resp.json()) # {"status": "Void", ...}
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/invoices/${invId}/void`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reason: "Duplicate invoice — customer billed twice in error",
actor_id: "support-agent-001",
actor_name: "Jane Smith",
}),
}
);
const result = await resp.json();
console.log("Voided:", result.status);
// [Go]
body, _ := json.Marshal(map[string]string{
"reason": "Duplicate invoice — customer billed twice in error",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith",
})
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/invoices/"+invID+"/void",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
Voiding requires a reason of at least 10 characters. Only Open invoices can be voided.
Issue a Credit Note
Creates a negative invoice offsetting some or all charges from a finalized invoice:
# [curl]
curl -X POST https://api.bill.sh/admin/v1/invoices/$INV_ID/credit-note \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Service incident credit — 4-hour outage on 2026-02-28",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith",
"idempotency_key": "cn-ticket-1234"
}'
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/invoices/{inv_id}/credit-note",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={
"reason": "Service incident credit — 4-hour outage on 2026-02-28",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith",
"idempotency_key": "cn-ticket-1234",
},
)
credit_note = resp.json()
print(f"Credit note {credit_note['id']} — {credit_note.get('total_nanos')} nanos")
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/invoices/${invId}/credit-note`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reason: "Service incident credit — 4-hour outage on 2026-02-28",
actor_id: "support-agent-001",
actor_name: "Jane Smith",
idempotency_key: "cn-ticket-1234",
}),
}
);
const creditNote = await resp.json();
console.log("Credit note:", creditNote.id);
// [Go]
body, _ := json.Marshal(map[string]string{
"reason": "Service incident credit — 4-hour outage on 2026-02-28",
"actor_id": "support-agent-001",
"actor_name": "Jane Smith",
"idempotency_key": "cn-ticket-1234",
})
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/invoices/"+invID+"/credit-note",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
Pass line_item_ids to credit specific charges only. Empty array credits the full invoice.
Mark as Paid
For payments collected outside Stripe (wire transfer, check):
# [curl]
curl -X POST https://api.bill.sh/admin/v1/invoices/$INV_ID/pay \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"actor_id": "finance-001",
"actor_name": "Alex Finance"
}'
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/invoices/{inv_id}/pay",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={"actor_id": "finance-001", "actor_name": "Alex Finance"},
)
print(resp.json()) # {"status": "Paid", ...}
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/invoices/${invId}/pay`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ actor_id: "finance-001", actor_name: "Alex Finance" }),
}
);
const result = await resp.json();
console.log("Marked paid:", result.status);
// [Go]
body, _ := json.Marshal(map[string]string{
"actor_id": "finance-001",
"actor_name": "Alex Finance",
})
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/invoices/"+invID+"/pay",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()