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/receiptsregardless 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 snapshot — footerText, 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
idfor lookups, thenumberfor human display. The ULID-basedidis stable, sortable, and unique across all workspaces; thenumberis 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
- Portal → Receipts — resending, refund receipts, template customisation.
- Templates — layout, numbering, auto-send rules.
- Invoices — the request-for-payment side of the pair.
- Webhooks — the full event catalog.