Invoices
An invoice is a billing document for a customer — line items, totals, due date, and a status that moves through draft → open → paid (or void). The Go SDK exposes the seven invoice methods behind client.Invoices. Invoices can be created manually (one-off services, professional fees) or are generated automatically each cycle by a subscription. For wire shapes, status transitions, and per-field validation see API → Invoices.
Field on the Client
client.Invoices — type *plugipay.InvoicesResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
Methods
Create
Signature. func (r *InvoicesResource) Create(ctx context.Context, in InvoiceCreateInput) (*Invoice, error)
Creates an invoice in draft (or open if you set Status to "open"). Required: CustomerID, Currency, Lines. Optional: Discount, Tax (both in smallest currency unit), DueAt (ISO timestamp), Memo. Not auto-keyed — if you need idempotency on invoice creation, use client.Do(...) with your own key (e.g. your accounting system's invoice id).
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
due := "2026-06-01T23:59:59Z"
memo := "Net 14 — thank you for your business."
inv, err := client.Invoices.Create(ctx, plugipay.InvoiceCreateInput{
CustomerID: "cus_01HX...",
Currency: plugipay.CurrencyIDR,
Lines: []plugipay.InvoiceCreateLine{
{Description: "Consulting (May)", Quantity: 10, UnitAmount: 1_500_000_00},
{Description: "Travel reimbursement", Quantity: 1, UnitAmount: 850_000_00},
},
Tax: ptr(int64(1_585_000_00)), // 10% PPN
DueAt: &due,
Memo: &memo,
})
if err != nil {
return err
}
log.Printf("invoice %s draft", inv.ID) // "in_01HX..."
Get
Signature. func (r *InvoicesResource) Get(ctx context.Context, id string) (*Invoice, error)
Fetches one invoice with its full line-item list, totals, and current status. Returns *plugipay.Error{Status: 404, Code: "not_found"} if missing. Naturally idempotent.
inv, err := client.Invoices.Get(ctx, "in_01HX...")
if err != nil { return err }
log.Printf("%s due=%v total=%d paid=%d",
inv.Status, deref(inv.DueAt), inv.Total, inv.AmountPaid)
List
Signature. func (r *InvoicesResource) List(ctx context.Context, params InvoiceListParams) (Page[Invoice], error)
Cursor-paginated. Filters: Status (one of "draft", "open", "paid", "void", "uncollectible"), CustomerID.
status := "open"
limit := 100
page, err := client.Invoices.List(ctx, plugipay.InvoiceListParams{
Status: &status,
Limit: &limit,
})
for _, inv := range page.Data {
log.Printf("%s due=%v amountDue=%d", inv.ID, deref(inv.DueAt), inv.AmountDue)
}
Finalize
Signature. func (r *InvoicesResource) Finalize(ctx context.Context, id string) (*Invoice, error)
Moves an invoice from draft to open and assigns it an immutable, human-friendly Number ("INV-2026-001"). After finalization, the line items are locked — you can no longer edit. Not auto-keyed; the server treats double-finalize as 409 conflict.
inv, err := client.Invoices.Finalize(ctx, "in_01HX...")
if err != nil {
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "conflict" {
// already open — fetch it
return client.Invoices.Get(ctx, id)
}
return err
}
log.Printf("finalized as %s", inv.Number)
Pay
Signature. func (r *InvoicesResource) Pay(ctx context.Context, id string) (*Invoice, error)
Marks an open invoice paid via the manual collection path — bank transfer, cash, EDC slip. The server posts a ledger entry and emits an invoice.paid event. Auto-keyed for idempotency. For automatic collection (charging a stored token), don't call Pay; the subscription engine handles it.
inv, err := client.Invoices.Pay(ctx, "in_01HX...")
if err != nil {
return err
}
log.Printf("paid %d of %d", inv.AmountPaid, inv.Total)
Void
Signature. func (r *InvoicesResource) Void(ctx context.Context, id string) (*Invoice, error)
Voids an open (unpaid) invoice. Lines and history are preserved for audit; the invoice still appears in List filtered by Status="void". Cannot void a paid invoice — that requires a refund. Auto-keyed for idempotency.
inv, err := client.Invoices.Void(ctx, "in_01HX...")
SendEmail
Signature. func (r *InvoicesResource) SendEmail(ctx context.Context, id string, to *string) (*InvoiceSendEmailResult, error)
Emails the invoice (PDF attachment + a hosted-link button) to the customer. Pass nil to send to the customer's primary email; pass a non-nil pointer to override the recipient. Returns the address it actually sent to. Not auto-keyed — calling twice sends twice (intentional).
override := "ap@bigcorp.id"
res, err := client.Invoices.SendEmail(ctx, "in_01HX...", &override)
if err != nil { return err }
log.Printf("sent=%v to=%s", res.Sent, res.To)
Types
type Invoice struct {
ID string `json:"id"` // "in_..."
AccountID string `json:"accountId"`
CustomerID string `json:"customerId"`
Status string `json:"status"` // draft|open|paid|void|uncollectible
Number string `json:"number"` // "" until Finalize
Currency CurrencyCode `json:"currency"`
Subtotal int64 `json:"subtotal"`
Discount int64 `json:"discount"`
Tax int64 `json:"tax"`
Total int64 `json:"total"`
AmountPaid int64 `json:"amountPaid"`
AmountDue int64 `json:"amountDue"`
DueAt *string `json:"dueAt"`
IssuedAt *string `json:"issuedAt"` // set by Finalize
PaidAt *string `json:"paidAt"`
HostedInvoiceURL *string `json:"hostedInvoiceUrl"`
Lines []InvoiceLine `json:"lines"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type InvoiceLine struct {
ID, Description string
Quantity, UnitAmount, Amount int64
}
type InvoiceSendEmailResult struct {
Sent bool `json:"sent"`
To string `json:"to"`
}
Field rules and totals math: API → Invoices.
Common patterns
Draft → finalize → send
The canonical "send a freelance invoice" pipeline:
inv, err := c.Invoices.Create(ctx, in)
if err != nil { return err }
if _, err := c.Invoices.Finalize(ctx, inv.ID); err != nil {
return err
}
if _, err := c.Invoices.SendEmail(ctx, inv.ID, nil); err != nil {
return err
}
log.Printf("sent invoice %s", inv.ID)
For multi-recipient sends, call SendEmail once per recipient with to overridden — the server records each delivery.
Idempotent create from your accounting system
Use your accounting invoice number as the idempotency key so a network retry can never double-issue:
func issueFromAccounting(ctx context.Context, c *plugipay.Client, acctInvoiceNo string, in plugipay.InvoiceCreateInput) (*plugipay.Invoice, error) {
var out plugipay.Invoice
err := c.Do(ctx, plugipay.RequestOptions{
Method: "POST",
Path: "/api/v1/invoices",
Body: in,
IdempotencyKey: "ar_" + acctInvoiceNo,
}, &out)
if err != nil { return nil, err }
return &out, nil
}
Walk overdue invoices
status := "open"
limit := 100
var cursor *string
overdue := []plugipay.Invoice{}
now := time.Now()
for {
page, err := c.Invoices.List(ctx, plugipay.InvoiceListParams{
Status: &status, Limit: &limit, Cursor: cursor,
})
if err != nil { return err }
for _, inv := range page.Data {
if inv.DueAt == nil { continue }
due, _ := time.Parse(time.RFC3339, *inv.DueAt)
if now.After(due) { overdue = append(overdue, inv) }
}
if !page.HasMore { break }
cursor = page.Cursor
}
Errors with errors.As
inv, err := c.Invoices.Finalize(ctx, id)
var pe *plugipay.Error
if errors.As(err, &pe) {
switch pe.Code {
case "conflict":
// already finalized — fetch instead
case "validation_error":
log.Printf("can't finalize: %s", pe.Message)
}
}
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Empty Lines, negative amounts, bad DueAt format. |
not_found |
404 | Invoice / customer id missing. |
conflict |
409 | Finalize on open, Pay on paid, Void on paid. |
insufficient_scope |
403 | Key lacks plugipay:invoice:write. |
Full mechanics: Errors.
Next
- Receipts — auto-generated when an invoice transitions to
paid. - Refunds — refund a paid invoice (
SourceType: SourceTypeInvoice). - Subscriptions — auto-creates invoices each cycle.
- API → Invoices — HTTP-level reference.