Receipts

A receipt is the customer-facing proof that a specific payment was received. Plugipay issues one automatically the moment a checkout session completes or an invoice is paid — there is no POST /v1/receipts.

Receipts are evidentiary: each row freezes a snapshot of merchant brand, customer details, line items, tax breakdown, currency, and provider reference as they were at capture. Later edits to your business profile or to the customer's record don't rewrite history.

Receipt vs invoice. An invoice requests payment (what's owed). A receipt records payment (what cleared, when, by which method). One paid invoice produces one receipt; a successful checkout session also produces one — no invoice needed. Both share /v1/receipts regardless of source.

For the portal walkthrough — resending, refund receipts, template customisation — see Portal → Receipts.

Indonesian tax context

For workspaces with PPN (Indonesian VAT) configured under Settings → Business → Tax, receipts break out subtotal, taxAmount, and total. The default rate is 0.11 (11%), back-calculated from the gross (taxAmount = round(total × rate / (1 + rate))). merchant.taxId carries your NPWP in the header. For non-Indonesian workspaces or unconfigured tax, the block is suppressed (taxAmount: 0, template.showTax: false).

Plugipay receipts are commercial receipts — not Faktur Pajak. Generate formal VAT documents in your accounting system using the receipt number as the cross-reference.

Endpoints

Method Path Operation
GET /v1/receipts List receipts
GET /v1/receipts/:id Retrieve one
GET /v1/receipts/:id/receipt.pdf Download as PDF
GET /v1/receipts/:id/receipt.html Render as HTML
GET /v1/receipts/:id/receipt.escpos ESC/POS thermal-printer bytes
POST /v1/receipts/:id/email Send (or re-send) by email

There is no create, update, or delete endpoint — issuance is automatic and receipts are immutable. To correct one, void-and-reissue from the portal.

All endpoints require plugipay:checkout:read. The email endpoint additionally requires plugipay:checkout:write.


Retrieve a receipt

GET /v1/receipts/:id

Returns the full receipt object including the frozen snapshot, line items, tax breakdown, and customer.

Response — 200 OK

{
  "data": {
    "receiptId": "rcpt_01HXxxxxxxxxxxxxxxxxxxxxxx",
    "number": "PLGP-RCPT-2026-05-0042",
    "sourceType": "checkout_session",
    "sourceId": "sess_01HYxxxxxxxxxxxxxxxxxxxxxx",
    "status": "completed",
    "paidAt": "2026-05-12T10:42:00.123Z",
    "amount": 250000,
    "currency": "IDR",
    "method": "qris",
    "methodLabel": "QRIS",
    "adapter": "xendit",
    "customer": {
      "name": "Alice Wijaya",
      "email": "alice@example.com",
      "phone": "+62 812 0000 0000"
    },
    "merchant": {
      "name": "Toko Kopi Senja",
      "logoUrl": "https://cdn.plugipay.com/u/...",
      "accentColor": "#A16207",
      "tagline": null,
      "address": "Jl. Senopati 12, Jakarta",
      "taxId": "01.234.567.8-901.000"
    },
    "lineItems": [
      { "name": "Latte 16oz", "qty": 2, "unitPrice": 90000, "subtotal": 180000 },
      { "name": "Croissant", "qty": 1, "unitPrice": 70000, "subtotal":  70000 }
    ],
    "subtotal": 225225,
    "taxAmount": 24775,
    "total": 250000,
    "template": {
      "footerText": null,
      "thankYouText": "Terima kasih — Thank you",
      "showTax": true,
      "taxLabel": "PPN",
      "taxRate": 0.11,
      "cashierLabel": "Budi",
      "showMerchantAddress": true,
      "merchantAddress": "Jl. Senopati 12, Jakarta",
      "merchantTaxId": "01.234.567.8-901.000"
    }
  },
  "error": null,
  "meta": { "requestId": "req_01H...", "timestamp": "..." }
}

Errors

Status Code When
404 not_found No receipt with that ID in this workspace

Samples

// Node
const receipt = await plugipay.receipts.get('rcpt_01HX...');
# Python
receipt = plugipay.receipts.get("rcpt_01HX...")
// Go
receipt, err := client.Receipts.Get(ctx, "rcpt_01HX...")
# curl — using the plugipay_curl helper from Authentication
plugipay_curl GET '/v1/receipts/rcpt_01HX...'

To download the rendered artefact, hit a format suffix — these return raw bytes, not the envelope:

GET /v1/receipts/:id/receipt.pdf       → application/pdf
GET /v1/receipts/:id/receipt.html      → text/html
GET /v1/receipts/:id/receipt.escpos    → application/octet-stream (use ?width=58 for 58mm; default 80mm)

List receipts

GET /v1/receipts

Returns receipts in the active workspace, newest first by issuedAt.

Query parameters

Param Type Notes
limit integer 1–100; default 20
cursor string From meta.page.nextCursor of the prior page
sourceType checkout_session | invoice Filter by what generated the receipt
customerId string cus_... exact match
issuedAfter ISO 8601 or epoch Inclusive lower bound on issuedAt
issuedBefore ISO 8601 or epoch Inclusive upper bound

List items are the summary shape — a flat subset of the full object (no snapshot, no merchant, no line items). Use GET /v1/receipts/:id to inflate.

Response — 200 OK

{
  "data": [
    {
      "id": "rcpt_01HXxxxxxxxxxxxxxxxxxxxxxx",
      "number": "PLGP-RCPT-2026-05-0042",
      "sourceType": "checkout_session",
      "sourceId": "sess_01HY...",
      "customerId": "cus_01HZ...",
      "amount": 250000,
      "currency": "IDR",
      "method": "qris",
      "adapter": "xendit",
      "issuedAt": "2026-05-12T10:42:00.123Z",
      "emailedAt": "2026-05-12T10:42:03.910Z",
      "emailedTo": "alice@example.com"
    }
  ],
  "error": null,
  "meta": {
    "requestId": "req_01H...",
    "timestamp": "...",
    "page": { "limit": 20, "hasMore": true, "nextCursor": "rcpt_01HX..." }
  }
}

Samples

// Node — last 50 invoice-sourced receipts for one customer
const page = await plugipay.receipts.list({
  limit: 50, sourceType: 'invoice', customerId: 'cus_01HZ...',
});
# Python
page = plugipay.receipts.list(limit=50, source_type="invoice", customer_id="cus_01HZ...")
// Go
limit := 50; src := "invoice"; cust := "cus_01HZ..."
page, err := client.Receipts.List(ctx, plugipay.ReceiptListParams{
    Limit: &limit, SourceType: &src, CustomerID: &cust,
})
plugipay_curl GET '/v1/receipts?limit=50&sourceType=invoice&customerId=cus_01HZ...'

Send a receipt by email

POST /v1/receipts/:id/email

Emails the receipt — HTML body plus PDF attachment — to the customer on file, or to an address you supply. Use it when the customer's email was added after checkout, when a buyer asks for a forwarded copy (procurement, accounting), or when auto-send is disabled and you're driving sends manually.

Request body

Field Type Notes
to string | omitted Override recipient. Omit to use customer.email from the receipt.
{ "to": "accounting@buyer.example" }

Response — 200 OK

{
  "data": { "sent": true, "to": "accounting@buyer.example" },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

On success, the receipt's emailedAt and emailedTo fields are updated to the latest send. Earlier sends aren't preserved as history — only the most recent one is recorded.

Errors

Status Code When
404 not_found Receipt doesn't exist in this workspace
409 no_recipient No to was supplied and the receipt has no customer email on file

Samples

// Node — forward to accountant
await plugipay.receipts.email('rcpt_01HX...', { to: 'accounting@buyer.example' });
# Python
plugipay.receipts.email("rcpt_01HX...", to="accounting@buyer.example")
// Go
err := client.Receipts.Email(ctx, "rcpt_01HX...", &plugipay.ReceiptEmailParams{
    To: plugipay.String("accounting@buyer.example"),
})
plugipay_curl POST '/v1/receipts/rcpt_01HX.../email' '{"to":"accounting@buyer.example"}'

Re-sends are rare. Receipts auto-email at issue time when the customer record has an email address. Hitting this endpoint with no good reason just risks spam-flagging your sender domain. Re-send when asked, not as a nudge.

The receipt object

Field reference for GET /v1/receipts/:id.

Field Type Notes
receiptId string rcpt_01H...
number string Human-readable; see Numbering
sourceType checkout_session | invoice What produced this receipt
sourceId string The sess_... or inv_... it was issued from
status string Snapshot of source status at issue (completed, paid, etc.)
paidAt ISO 8601 | null When payment cleared
amount integer Gross total in the smallest currency unit (= total)
currency string ISO 4217
method string | null Method ID (card, qris, va, ...)
methodLabel string | null Display label (e.g. QRIS, Virtual Account)
adapter xendit | midtrans | paypal | manual | null Upstream provider
customer object | null { name, email, phone } snapshot
merchant object { name, logoUrl, accentColor, tagline, address, taxId } snapshot
lineItems[] array { name, qty, unitPrice, subtotal } — amounts in smallest unit
subtotal integer Pre-tax sum of line items
taxAmount integer PPN or other tax; 0 when not configured
total integer subtotal + taxAmount; equals amount
template object Snapshot of the render template (see below)

template snapshotfooterText, thankYouText (defaults to Terima kasih — Thank you), showTax, taxLabel (defaults to PPN), taxRate (fractional, e.g. 0.11), cashierLabel (from session metadata cashierName if present, else template default), showMerchantAddress, merchantAddress, merchantTaxId.

The template block is frozen at issue. Updating the workspace template later does not mutate this row — future receipts get the new template, this one does not.

The list endpoint returns the flat subset documented above; inflate with GET /v1/receipts/:id when you need merchant, line items, or template.

Numbering

Receipt numbers follow PLGP-RCPT-YYYY-MM-NNNN: the fixed prefix, UTC year and zero-padded month of issuedAt, then a per-workspace monotonic sequence zero-padded to 4 digits.

The sequence is assigned at issue. Concurrent issues for the same source are serialised by a unique constraint on (workspace, sourceType, sourceId) — if two writers race, exactly one row is created and the other receives the existing receipt. Failed payments don't consume numbers. The sequence monotonically increases across the workspace's entire history (it is not month-reset in the current implementation). Custom prefixes and reset schemes are roadmap items.

Use the receipt id for lookups, the number for human display. The ULID-based id is stable, sortable, and unique across all workspaces; the number is a per-workspace presentational artefact that may change scheme later.

Events

Plugipay does not currently emit dedicated receipt.* webhook events. Subscribe to the source events instead — checkout_session.completed, payment.succeeded, or invoice.paid — and fetch the linked receipt by filtering GET /v1/receipts?sourceType=<...> on the source ID. Dedicated receipt.generated and receipt.sent events are on the roadmap; watch the changelog.

Next

Plugipay — Payments that don't tax your success