Receipts

A receipt is the immutable, customer-facing record of a completed payment — a numbered document with line items, totals, and a hosted URL the buyer can return to. Plugipay auto-generates a receipt whenever a checkout session completes or an invoice transitions to paid. You don't create receipts directly — the SDK exposes only List and Get. The Go SDK puts both behind client.Receipts. For wire shapes and per-field details, see API → Receipts.

Field on the Client

client.Receipts — type *plugipay.ReceiptsResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.

Methods

List

Signature. func (r *ReceiptsResource) List(ctx context.Context, params ReceiptListParams) (Page[ReceiptSummary], error)

Cursor-paginated list of receipt summaries — the small projection you'd render in a dashboard table. For the rich, immutable snapshot, follow up with Get. Filters: SourceType ("checkout_session" or "invoice"), CustomerID, IssuedAfter, IssuedBefore (both ISO timestamps).

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

since := "2026-05-01T00:00:00Z"
limit := 100

page, err := client.Receipts.List(ctx, plugipay.ReceiptListParams{
    IssuedAfter: &since,
    Limit:       &limit,
})
if err != nil {
    return err
}
for _, rs := range page.Data {
    log.Printf("%s %s %d %s", rs.Number, rs.SourceType, rs.Amount, rs.Currency)
}

Per-customer history:

custID := "cus_01HX..."
page, err := client.Receipts.List(ctx, plugipay.ReceiptListParams{
    CustomerID: &custID,
})

Get

Signature. func (r *ReceiptsResource) Get(ctx context.Context, id string) (json.RawMessage, error)

Fetches the full receipt payload as json.RawMessage. This is intentional. The receipt is an immutable snapshot — the wire shape is richer than ReceiptSummary (line items, the brand snapshot at issue time, the template rendering used) and intentionally opaque to the SDK so it can evolve without breaking your code. Decode into your own struct if you need fields beyond the summary; otherwise pass the bytes straight to your PDF renderer or REST proxy.

import "encoding/json"

raw, err := client.Receipts.Get(ctx, "rc_01HX...")
if err != nil {
    return err
}

// Pass through to a downstream service:
w.Header().Set("Content-Type", "application/json")
w.Write(raw)

// Or decode into your own shape:
type myReceipt struct {
    ID     string `json:"id"`
    Number string `json:"number"`
    Lines  []struct {
        Description string `json:"description"`
        Amount      int64  `json:"amount"`
    } `json:"lines"`
}
var rcpt myReceipt
if err := json.Unmarshal(raw, &rcpt); err != nil {
    return err
}

Receipts are immutable. Once issued, the line items, totals, brand snapshot, and template are frozen. To "correct" a receipt, refund the source and re-issue.

Types

type ReceiptSummary struct {
    ID         string  `json:"id"`         // "rc_..."
    Number     string  `json:"number"`     // "RC-2026-001"
    SourceType string  `json:"sourceType"` // "checkout_session" | "invoice"
    SourceID   string  `json:"sourceId"`
    CustomerID *string `json:"customerId"`
    Amount     int64   `json:"amount"`     // smallest currency unit
    Currency   string  `json:"currency"`
    Method     *string `json:"method"`     // qris | va | card | ...
    Adapter    *string `json:"adapter"`    // xendit | midtrans | paypal | manual
    IssuedAt   string  `json:"issuedAt"`
    EmailedAt  *string `json:"emailedAt"`
    EmailedTo  *string `json:"emailedTo"`
}

The full receipt returned by Get is json.RawMessage — decode at your own discretion. For the field list at the wire level, see API → Receipts.

Common patterns

Render the buyer's receipt history

func receiptList(ctx context.Context, c *plugipay.Client, custID string) ([]plugipay.ReceiptSummary, error) {
    var out []plugipay.ReceiptSummary
    limit := 50
    var cursor *string
    for {
        page, err := c.Receipts.List(ctx, plugipay.ReceiptListParams{
            CustomerID: &custID,
            Limit:      &limit,
            Cursor:     cursor,
        })
        if err != nil { return out, err }
        out = append(out, page.Data...)
        if !page.HasMore { return out, nil }
        cursor = page.Cursor
    }
}

Proxy the full receipt to the buyer

If you're serving the receipt under your own domain (e.g. /orders/:id/receipt.json), just stream the bytes through:

func receiptHandler(c *plugipay.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        receiptID := r.PathValue("id")

        ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
        defer cancel()

        raw, err := c.Receipts.Get(ctx, receiptID)
        if err != nil {
            var pe *plugipay.Error
            if errors.As(err, &pe) && pe.Code == "not_found" {
                http.Error(w, "not found", http.StatusNotFound)
                return
            }
            http.Error(w, "upstream error", http.StatusBadGateway)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.Header().Set("Cache-Control", "public, max-age=86400, immutable")
        w.Write(raw)
    }
}

Receipts are immutable — you can cache them aggressively.

Match receipts to your accounting export

Receipts have SourceType + SourceID, which let you join back to the originating session or invoice:

type recon struct {
    Receipt    plugipay.ReceiptSummary
    InvoiceID  string
}
out := []recon{}
for _, rs := range receipts {
    if rs.SourceType == "invoice" {
        out = append(out, recon{Receipt: rs, InvoiceID: rs.SourceID})
    }
}

Errors with errors.As

raw, err := c.Receipts.Get(ctx, id)
var pe *plugipay.Error
if errors.As(err, &pe) {
    switch pe.Code {
    case "not_found":
        // bad id
    case "rate_limited":
        time.Sleep(2 * time.Second) // back off and retry
    }
}

Errors

Code Status Cause
not_found 404 Receipt id doesn't exist or is in another workspace.
validation_error 400 Bad timestamp format on IssuedAfter / IssuedBefore.
rate_limited 429 Too many calls. Standard backoff applies.
insufficient_scope 403 Key lacks plugipay:receipt:read.

Receipts have no write endpoints — you'll never see 409 conflict or validation_error outside the filter timestamps.

Full mechanics: Errors.

Next

  • Refunds — receipts can't be edited; refund and re-issue.
  • Webhooks — subscribe to receipt.issued / receipt.emailed.
  • API → Receipts — HTTP-level reference.
Plugipay — Payments that don't tax your success