Receipts
A receipt is the immutable, customer-facing record of a successful payment — the PDF you'd email, the line in their portal history, the audit document your accountants want. Plugipay generates receipts automatically when a checkout session completes or an invoice is paid; the SDK lets you list and retrieve them. There is no create — receipts are derived, not authored. The Python SDK exposes two methods behind plugipay.receipts. For HTTP detail, see API → Receipts.
Namespace
plug.receipts # _Receipts
Methods
list
plug.receipts.list(
*,
limit: int | None = None,
cursor: str | None = None,
source_type: str | None = None,
customer_id: str | None = None,
issued_after: str | None = None,
issued_before: str | None = None,
) -> PageResult[ReceiptSummary]
Returns a paginated list of receipt summaries — a trimmed shape suitable for tables, audit logs, and dashboards. To see the full receipt body (line items, tax breakdown, hosted PDF URL), call get(receipt_id) on the id returned here.
source_typeis"checkout_session"or"invoice".issued_after/issued_beforeare ISO 8601 UTC strings.customer_idfilters to one buyer's history.
# Receipts for a customer this quarter
page = plug.receipts.list(
customer_id="cus_01HX...",
issued_after="2026-04-01T00:00:00Z",
issued_before="2026-07-01T00:00:00Z",
limit=100,
)
for r in page.data:
print(r["id"], r["amount"], r["currency"], r["issuedAt"])
# Full pagination loop
cursor = None
while True:
page = plug.receipts.list(limit=100, cursor=cursor)
for receipt in page.data:
archive_to_warehouse(receipt.raw)
if not page.has_more:
break
cursor = page.cursor
get
plug.receipts.get(receipt_id: str) -> Any
Returns the full receipt payload as a plain dict — not a Resource dataclass. This is the only method on the namespace that breaks the "everything wraps in a dataclass" pattern, because receipts carry a rich, document-shaped payload (lines, tax, addresses, hosted PDF URL) that's easier to navigate as raw JSON than as obj.raw["…"].
receipt = plug.receipts.get("rcp_01HX...")
print(receipt["hostedReceiptUrl"]) # buyer-facing PDF
print(receipt["lines"]) # list of line dicts
print(receipt["total"], receipt["currency"])
Why no
create? Receipts are an output of a successful payment. Plugipay generates exactly one receipt per settled payment — you couldn't create a second one against the same source if you tried. To customize what a receipt looks like, edit your templates (thekind="receipt"ones), not the receipts themselves.
Types
from plugipay import ReceiptSummary, PageResult
ReceiptSummary is a Resource subclass — the row shape returned by list. Likely-read fields:
summary["id"]—rcp_+ ULID.summary["sourceType"],summary["sourceId"].summary["customerId"].summary["amount"],summary["currency"].summary["issuedAt"]— ISO 8601 UTC.summary.get("number")— human-friendly receipt number (e.g.RCP-2026-001234).
The full receipt returned by get is not typed in the Python SDK — it's a plain dict. The shape is documented at API → Receipts → The receipt object. Notable nested fields:
receipt["lines"]— list of{description, amount, quantity, taxAmount?}dicts.receipt["subtotal"],receipt["taxTotal"],receipt["total"].receipt["billTo"]—{name, email, taxId?, address?}snapshot at issue time.receipt["hostedReceiptUrl"]— public PDF link.receipt["templateId"]— which template rendered it.
Common patterns
Email-an-old-receipt flow. The hosted URL never expires; just resend it:
receipt = plug.receipts.get(receipt_id)
send_email(
to=receipt["billTo"]["email"],
subject=f"Your Plugipay receipt {receipt['number']}",
body=f"Download: {receipt['hostedReceiptUrl']}",
)
CSV export for accountants.
import csv
with open("receipts.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "number", "issuedAt", "amount", "currency", "customer"])
cursor = None
while True:
page = plug.receipts.list(limit=100, cursor=cursor)
for r in page.data:
w.writerow([
r["id"], r.get("number", ""), r["issuedAt"],
r["amount"], r["currency"], r["customerId"],
])
if not page.has_more:
break
cursor = page.cursor
Pre-fetch on demand. Summaries are cheap; full receipts are heavier. Render a table from list, then get only when the user clicks "View":
@app.route("/receipts/<receipt_id>")
def view_receipt(receipt_id: str):
receipt = plug.receipts.get(receipt_id)
return redirect(receipt["hostedReceiptUrl"])
Treat receipts as immutable snapshots. A buyer's name change after the fact does not update old receipts — the billTo block was frozen at issue time. If you need a "corrected" receipt, void the underlying invoice and create a new one (which generates a new receipt).
Bulk download to a local cache. When pulling receipts into your own document store, fetch summaries first then get only the ones you don't have:
import os
import json
import pathlib
cache = pathlib.Path("/var/data/receipts")
cache.mkdir(parents=True, exist_ok=True)
cursor = None
while True:
page = plug.receipts.list(limit=100, cursor=cursor)
for summary in page.data:
target = cache / f"{summary['id']}.json"
if not target.exists():
full = plug.receipts.get(summary["id"])
target.write_text(json.dumps(full))
if not page.has_more:
break
cursor = page.cursor
This pattern keeps your local cache append-only and avoids re-fetching settled receipts on every run.
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
validation_error |
Bad source_type, malformed issued_after / issued_before. |
404 |
not_found |
Receipt id doesn't exist or is in another workspace. |
403 |
insufficient_scope |
Key lacks plugipay:receipt:read. |
There are no state-transition errors on receipts (no mutation endpoints). Network and timeout failures surface as PlugipayNetworkError and PlugipayTimeoutError as usual — see Errors.
Next
- API → Receipts — the full receipt payload shape.
- Templates — customize how receipts render.
- Checkout sessions — the most common receipt source.
- Invoices — the other common receipt source.