Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

TypeDescription
StandardRegular billing invoice
CreditNoteNegative invoice offsetting a prior charge
ProFormaEstimate/quote (not legally binding)

Key Fields

FieldTypeDescription
idstring (UUIDv7)Invoice ID
invoice_numberstring?Sequential number assigned on finalization (e.g., INV-000042)
statusenumDraft / Open / Paid / Void
invoice_typeenumStandard / CreditNote / ProForma
customer_idstringOwning customer
subscription_idstringSource subscription
total_nanosstringTotal in pico-units (i128 as string)
currencystringISO 4217
due_datedatePayment due date
period_start / period_enddatetimeBilling period
line_itemsarrayCharges detail
applies_to_invoice_idstring?For credit notes: which invoice this offsets
voided_reasonstring?Why the invoice was voided
finalized_atdatetime?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()