Invoices

An invoice is a billing document for a customer — line items, totals, due date, and a status that moves through draftopenpaid (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.
Plugipay — Payments that don't tax your success