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.