Stripe already lets customers download receipts as PDFs. Use that route when its receipt meets your needs. If you need your own branded layout, map a successful payment to your approved order data and fill a saved receipt template. This recipe starts with a local sample event, checks its amount, and creates one PDF. Each successful PDF costs one credit.
Want to try a branded receipt first? Open the receipt generator and pick one of the five designs.
How can I PDF a receipt?
Open the receipt in Stripe and use its PDF download. Stripe's receipt guide confirms that customers can view receipts in a browser and download PDFs. A custom template helps when the document needs your layout or extra order text.
Here is the Payment receipt gallery design. It shows sample goods, names, and amounts. The recipe below uses a separate workshop order, so its finished PDF has different data.

How do I create a receipt in Stripe?
Stripe creates receipts for successful payments and refunds. Its receipt settings can send them to customers. Read the Stripe receipt steps before you build a separate document flow.
For your own layout, process a verified payment_intent.succeeded snapshot event. That event carries a PaymentIntent in data.object. The event reference defines the event, and the PaymentIntent reference defines amount_received and currency.
Your webhook endpoint must verify the raw body, Stripe-Signature, and endpoint secret with Stripe's official library. It must check the expected account and test/live mode. Then it saves accepted events and queues the work before it returns. Stripe's webhook guide explains signature checks and repeated delivery. Don't feed unverified JSON from a request straight into this recipe.
How do I download a payment receipt?
For Stripe's own receipt, use the Charge object's receipt_url as described in its receipt URL guide. For a thirds.ai render, wait for succeeded, then download the file. The commands below do that and check its SHA-256.
thirds.ai keeps your files for 30 days. Its signed download links last 15 minutes, so store the finished PDF with your own order record. See download handling.
How to make a receipt of payment PDF?
Copy the Payment receipt template into your account and publish its source and schema. Set THIRDS_TEMPLATE_ID and THIRDS_TEMPLATE_VERSION to your copy's ID and version. Set THIRDS_API_KEY through your secret store. Keep your key and work files out of source control.
Use Python 3 and curl on macOS or Linux. The mapping code uses only the standard library.
Start with a sample event
Save this JSON as receipt-input.json in a private work folder. It's a small, invented Stripe event and order record, with no signature. The sample only tests the mapping and the PDF. For real use, your verified event worker supplies the event and looks up the order from its own records.
{
"event": {
"id": "evt_sample_receipt",
"type": "payment_intent.succeeded",
"created": 1788652800,
"livemode": false,
"data": {
"object": {
"id": "pi_sample_receipt",
"object": "payment_intent",
"status": "succeeded",
"currency": "usd",
"amount_received": 12000
}
}
},
"order": {
"payment_intent_id": "pi_sample_receipt",
"currency": "usd",
"merchant": {
"name": "Fieldnote Studio",
"address": "Sample business address",
"email": "hello@fieldnote.example"
},
"customer": {
"name": "Alex Morgan",
"email": "alex@example.com"
},
"receipt_number": "R-SAMPLE-1042",
"payment_method": "Sample card payment",
"items": [
{
"description": "Workshop booking",
"quantity": 1,
"amount_cents": 10000
}
],
"tax_cents": 2000,
"tax_label": "Sample tax",
"note": "Sample receipt for a layout check. No payment takes place.",
"footer": "Fieldnote Studio sample receipt. Amounts are in USD."
}
}
The example accepts only USD amounts in whole cents. The order's tax amount is already calculated. Each item's amount_cents is its full line total, including its quantity. Change the adapter and template together for another currency or payment flow.
Map the fields
Save this code as prepare-receipt.py. It checks payment state, PaymentIntent identity, currency, and the final amount before it writes a render request. The paid date uses the successful event's UTC date. The merchant, customer, items, and receipt number come from the approved order.
"""Map a local successful-payment fixture to a saved receipt template."""
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
fixture_path, template_id, version, output_path = sys.argv[1:]
try:
fixture = json.loads(Path(fixture_path).read_text())
event, order = fixture["event"], fixture["order"]
payment = event["data"]["object"]
if event["type"] != "payment_intent.succeeded" or payment["status"] != "succeeded":
raise SystemExit("payment_not_succeeded")
if payment["id"] != order["payment_intent_id"]:
raise SystemExit("payment_mismatch")
if payment["currency"] != "usd" or order["currency"] != "usd":
raise SystemExit("unsupported_currency")
amounts = [item["amount_cents"] for item in order["items"]]
cents = [*amounts, order["tax_cents"], payment["amount_received"]]
if any(type(value) is not int or value < 0 for value in cents):
raise SystemExit("invalid_amount")
subtotal = sum(amounts)
total = subtotal + order["tax_cents"]
if payment["amount_received"] != total or total == 0:
raise SystemExit("payment_mismatch")
data = {
"business_name": order["merchant"]["name"],
"business_address": order["merchant"]["address"],
"business_email": order["merchant"]["email"],
"customer_name": order["customer"]["name"],
"customer_email": order["customer"]["email"],
"receipt_number": order["receipt_number"],
"paid_on": datetime.fromtimestamp(event["created"], timezone.utc).date().isoformat(),
"payment_method": order["payment_method"],
"stamp": "Paid",
"items": [{"description": item["description"], "quantity": item["quantity"],
"amount": item["amount_cents"] / 100} for item in order["items"]],
"subtotal": subtotal / 100,
"tax_label": order["tax_label"],
"tax": order["tax_cents"] / 100,
"total": total / 100,
"note": order["note"],
"footer": order["footer"],
}
request = {"template_id": template_id, "version": int(version), "data": data,
"pdf": {"format": "A4"}, "wait": False}
Path(output_path).write_text(json.dumps(request, indent=2, allow_nan=False) + "\n")
except (KeyError, TypeError, ValueError, OSError, OverflowError):
raise SystemExit("invalid_input") from None
Create one sample PDF
Set THIRDS_WORK to the private folder that holds the two files. The first command only makes a local request file. The curl command after it starts one billed render, at one credit on success. Use the PaymentIntent ID and receipt version as the Idempotency-Key:
umask 077
python3 "$THIRDS_WORK/prepare-receipt.py" \
"$THIRDS_WORK/receipt-input.json" \
"$THIRDS_TEMPLATE_ID" "$THIRDS_TEMPLATE_VERSION" \
"$THIRDS_WORK/request.json"
curl --fail-with-body --silent --show-error https://thirds.ai/v1/pdf \
-H "Authorization: Bearer $THIRDS_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: receipt-pi_sample_receipt-v1' \
--data @"$THIRDS_WORK/request.json"
Save the id from the response with your order record. Read the job until status is succeeded. Replace the sample ID with yours.
curl --fail-with-body --silent --show-error \
https://thirds.ai/v1/pdf/pdf_00000000000000000000000000000000 \
-H "Authorization: Bearer $THIRDS_API_KEY"
Copy download.url from that response and add https://thirds.ai before it. The download needs no API key.
curl --fail --silent --show-error \
'https://thirds.ai/v1/downloads/YOUR_SIGNED_DOWNLOAD_TOKEN' \
--output "$THIRDS_WORK/receipt.pdf"
shasum -a 256 "$THIRDS_WORK/receipt.pdf"
The hash must match artifact.sha256 in the job, and the file size must match artifact.byte_size.
Open receipt.pdf and check the customer, paid date, line amounts, and USD 120.00 total. The sample tax amount exists only to exercise the layout. Your billing system supplies the actual tax and receipt rules.
Use one durable operation record per PaymentIntent and receipt version. Store the event IDs it handles there too. Repeated delivery must reuse the same request and key. After the API's 24-hour retry window, use your record to find the existing job. Don't create a second PDF just because a poll stops. A refund needs its own document flow, because an old paid receipt doesn't show the new balance.
Handle errors
| Code or state | Next step |
|---|---|
payment_not_succeeded | Stop. Do not label an unpaid order Paid. |
payment_mismatch | Check the PaymentIntent ID and order total in your own records. |
unsupported_currency | Use a reviewed adapter and matching template for that currency. |
invalid_amount or invalid_input | Correct the input shape before any render. |
template_data_invalid | Check the saved receipt schema and field limits. |
idempotency_conflict | Keep the original request with its original key. |
Start with the Payment receipt template, then review one sample PDF. The API workflow guide covers durable render state, and pricing shows the credit cost. Customer delivery stays in your own system.



