Invoices
An invoice is a billable line-item document tied to a customer. It can be one-off (manual create) or generated automatically by a subscription with collection_method="send_invoice". The Python SDK wraps the seven invoice endpoints behind plugipay.invoices. For the wire format and full lifecycle, see API → Invoices.
Namespace
plug.invoices # _Invoices
Methods
create
plug.invoices.create(
*,
customer_id: str,
currency: str,
lines: list[dict[str, Any]],
discount: int | None = None,
tax: int | None = None,
due_at: str | None = None,
status: str | None = None,
memo: str | None = None,
) -> Invoice
Creates an invoice in draft status by default. lines is a list of {description, amount, quantity?} dicts — amounts in integer minor units. Pass status="open" to skip drafting and emit immediately. No idempotency key is auto-sent on this call — wrap your own retry if needed via plug.request.
invoice = plug.invoices.create(
customer_id="cus_01HX...",
currency="IDR",
lines=[
{"description": "Pro subscription, May 2026", "amount": 149_000_00, "quantity": 1},
{"description": "SMS top-up (1000 msgs)", "amount": 50_000_00, "quantity": 1},
],
tax=21_900_00, # 11% PPN on subtotal
due_at="2026-06-12T00:00:00Z",
memo="Thank you for your business.",
)
print(invoice["id"]) # "inv_01HX..."
get
plug.invoices.get(invoice_id: str) -> Invoice
Retrieves the current state, including computed subtotal, total, and amountDue after payments. Use for reconciliation; subscribe to plugipay.invoice.* webhooks for state transitions in real time.
invoice = plug.invoices.get("inv_01HX...")
print(invoice["status"], invoice["amountDue"])
list
plug.invoices.list(
*,
limit: int | None = None,
cursor: str | None = None,
status: str | None = None,
customer_id: str | None = None,
) -> PageResult[Invoice]
Lists invoices. status is "draft" | "open" | "paid" | "void" | "uncollectible".
# All unpaid invoices older than 7 days
from datetime import datetime, timedelta, timezone
cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
overdue = []
cursor = None
while True:
page = plug.invoices.list(limit=100, status="open", cursor=cursor)
for inv in page.data:
if inv.get("dueAt") and inv["dueAt"] < cutoff:
overdue.append(inv)
if not page.has_more:
break
cursor = page.cursor
finalize
plug.invoices.finalize(invoice_id: str) -> Invoice
Promotes a draft invoice to open and locks the line items. Once finalized, lines and totals are immutable — to correct mistakes, void and re-create. No auto idempotency key.
plug.invoices.finalize("inv_01HX...")
pay
plug.invoices.pay(invoice_id: str) -> Invoice
Charges the customer's default payment token to settle an open invoice. Auto idempotency key. The invoice transitions to paid synchronously on success; on provider-async methods (VA, QRIS) it stays open until the webhook fires.
plug.invoices.pay("inv_01HX...")
void
plug.invoices.void(invoice_id: str) -> Invoice
Marks an open invoice as void — no longer collectible, but kept for audit. Use this for billing mistakes; do not delete (Plugipay doesn't expose delete on purpose). Auto idempotency key.
plug.invoices.void("inv_01HX...")
send_email
plug.invoices.send_email(invoice_id: str, *, to: str | None = None) -> dict
Triggers Plugipay's emailer to send the invoice PDF + hosted-pay link. Without to, sends to the customer's email on file; with to, overrides the recipient (cc/internal copies). Returns a plain dict, not an Invoice — useful fields are {"sent": True, "to": "...", "messageId": "..."}.
plug.invoices.send_email("inv_01HX...")
plug.invoices.send_email("inv_01HX...", to="accounts-payable@buyer.com")
Finalize then pay, in that order. A draft invoice can't be paid. Either pass
status="open"oncreateto skip the draft step, or runfinalizebeforepay. Most subscription-generated invoices arrive asopenalready.
Types
from plugipay import Invoice, PageResult
Invoice is a Resource subclass. Likely-read fields:
inv["id"]—inv_+ ULID.inv["status"]—"draft" | "open" | "paid" | "void" | "uncollectible".inv["currency"],inv["subtotal"],inv["total"],inv["amountDue"].inv["lines"]— list of line dicts.inv["dueAt"],inv["paidAt"]— ISO 8601 UTC, nullable.inv.get("hostedInvoiceUrl")— buyer-facing hosted page.
Full reference at API → Invoices.
Common patterns
Send-invoice flow for one-off bills.
invoice = plug.invoices.create(
customer_id=customer["id"],
currency="IDR",
lines=[{"description": "Setup fee", "amount": 500_000_00, "quantity": 1}],
due_at="2026-06-12T00:00:00Z",
)
plug.invoices.finalize(invoice["id"])
plug.invoices.send_email(invoice["id"])
Auto-charge if a default token exists, else email.
def collect(plug, invoice_id):
inv = plug.invoices.get(invoice_id)
customer = plug.customers.get(inv["customerId"])
if customer.get("defaultPaymentTokenId"):
plug.invoices.pay(invoice_id)
else:
plug.invoices.send_email(invoice_id)
Voiding with audit trail. Capture the reason in metadata via plug.request (the SDK kwargs don't expose metadata on void):
plug.request(
method="POST",
path=f"/api/v1/invoices/{invoice_id}/void",
body={"metadata": {"reason": "duplicate billing"}},
idempotency_key=f"void:{invoice_id}",
)
Reconciliation against your accounting system. Pull paid invoices since last sync:
last_sync = "2026-05-01T00:00:00Z"
cursor = None
while True:
page = plug.invoices.list(limit=100, status="paid", cursor=cursor)
for inv in page.data:
if inv.get("paidAt", "") < last_sync:
cursor = None
break
upsert_in_accounting(inv)
if not page.has_more or cursor is None:
break
cursor = page.cursor
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Empty lines, negative amounts, bad currency. |
404 |
not_found |
Invoice id missing or in another workspace. |
409 |
invalid_state_transition |
pay on a draft, finalize on a paid invoice, void on a paid invoice. |
409 |
idempotency_key_reused |
Same Idempotency-Key against a different body. |
422 |
payment_method_required |
pay called but customer has no default token. |
422 |
insufficient_funds |
Provider rejected the charge — typically a webhook follow-up arrives. |
All raise PlugipayError. See Errors.
Next
- API → Invoices — full field reference, line-item shape, and error catalog.
- Customers — set
default_payment_token_idsopayworks without prompting. - Refunds — refund a paid invoice.
- Receipts — what gets generated on payment.