Payload schema
The exact JSON shapes Paperless expects for orders (Pattern A) and receipts (Pattern B). Every field with type, required/optional status, example, and mapping to the Paperless data model.
Conventions
- Money is always an integer in the currency's smallest unit (cents for USD/CAD, pence for GBP).
- Currency is an ISO 4217 alpha-3 code (
USD,CAD,GBP). - Timestamps are ISO 8601 with timezone. UTC strongly preferred (suffix
Z). - Booleans are JSON
true/false, not strings. - Enum strings are uppercase (
CARD,ADDITIVE). - Null vs missing: for optional fields you may omit the key or set it to
null. Either is accepted.
Pattern A: Order object
This is the body Paperless expects from GET /v1/orders/{id} on your side.
Full example
{
"id": "ord_ABC123XYZ",
"merchant_id": "MERCHANT_12345",
"location_id": "LOC_001",
"state": "COMPLETED",
"created_at": "2026-05-15T17:21:14Z",
"closed_at": "2026-05-15T17:21:19Z",
"currency": "CAD",
"total_money": { "amount": 1357, "currency": "CAD" },
"total_tax_money": { "amount": 157, "currency": "CAD" },
"line_items": [
{
"uid": "li_1",
"name": "Latte",
"quantity": "1",
"base_price_money": { "amount": 600, "currency": "CAD" },
"total_money": { "amount": 600, "currency": "CAD" },
"applied_taxes": [
{ "tax_uid": "tax_1", "applied_money": { "amount": 78, "currency": "CAD" } }
]
},
{
"uid": "li_2",
"name": "Croissant",
"quantity": "1",
"base_price_money": { "amount": 600, "currency": "CAD" },
"total_money": { "amount": 600, "currency": "CAD" },
"applied_taxes": [
{ "tax_uid": "tax_1", "applied_money": { "amount": 79, "currency": "CAD" } }
]
}
],
"taxes": [
{
"uid": "tax_1",
"name": "HST",
"percentage": "13.0",
"type": "ADDITIVE",
"applied_money": { "amount": 157, "currency": "CAD" }
}
],
"tenders": [
{
"id": "tnd_1",
"type": "CARD",
"amount_money": { "amount": 1357, "currency": "CAD" },
"card_details": {
"card": {
"brand": "VISA",
"last_4": "4242"
}
}
}
],
"custom_attributes": {
"paperless_customer_id": {
"value": "PL-3f0e2c1a-7d33-4f0a-9b9b-ac3b5b1c2f88"
}
}
}
Top-level fields
| Field | Type | Required | Maps to | Notes |
|---|---|---|---|---|
id | string | Required | Receipt.external_id | Globally unique. Idempotency key. |
merchant_id | string | Required | Receipt.merchant_id | Your merchant ID. |
location_id | string | Required | — | Must match a location from /locations. |
state | string | Optional | — | Informational. Paperless only ingests COMPLETED orders regardless. |
created_at | ISO 8601 UTC | Required | — | When the order opened. |
closed_at | ISO 8601 UTC | Required | Receipt.purchased_at | When payment completed. |
currency | string (ISO 4217) | Required | Receipt.currency | |
total_money | Money | Required | Receipt.total_cents | Grand total. |
total_tax_money | Money | Required | Receipt.tax_cents | Total tax. Subtotal = total - tax. |
line_items | array | Required | ReceiptItem[] | Empty array is invalid. Must have ≥ 1. |
taxes | array | Optional | ReceiptTax[] | Empty when there is no tax. |
tenders | array | Required | ReceiptPayment[] | ≥ 1 entry. |
custom_attributes | object | Optional | — | Holds paperless_customer_id when present. |
LineItem
| Field | Type | Required | Maps to | Notes |
|---|---|---|---|---|
uid | string | Optional | — | Reference for applied_taxes.tax_uid. |
name | string | Required | ReceiptItem.name | |
quantity | string (numeric) | Required | ReceiptItem.quantity | Sent as a string for decimal support (e.g. "1.5"); rounded to integer by Paperless. |
base_price_money | Money | Required | ReceiptItem.unit_price | Pre-tax unit price. |
total_money | Money | Required | ReceiptItem.total_price | Pre-tax line total. |
applied_taxes | array | Optional | ReceiptItem.tax_cents | Sum of applied_money across entries. |
Tax
| Field | Type | Required | Maps to | Notes |
|---|---|---|---|---|
uid | string | Optional | — | Referenced by line_items.applied_taxes.tax_uid. |
name | string | Required | ReceiptTax.name | E.g. HST, GST, VAT. |
percentage | string | Optional | ReceiptTax.rate | Decimal string, e.g. "13.0". |
type | enum | Required | ReceiptTax.type | ADDITIVE or INCLUSIVE. |
applied_money | Money | Required | ReceiptTax.amount_cents | Total tax collected under this rule. |
Paperless rolls up taxes whose name matches GST, HST,
Goods and Services Tax, or Harmonized Sales Tax (case-insensitive)
into a separate gst_hst_cents field used for Canadian tax reporting. Naming
consistency matters.
Tender
| Field | Type | Required | Maps to | Notes |
|---|---|---|---|---|
id | string | Optional | — | Your tender ID. |
type | enum | Required | ReceiptPayment.method | CARD, CASH, GIFT_CARD, WALLET, OTHER. |
amount_money | Money | Required | ReceiptPayment.amount_cents | |
card_details.card.last_4 | string | Optional | ReceiptPayment.last4 | Card tenders only. Never send full PAN. |
card_details.card.brand | string | Optional | — | Informational. |
Money
{
"amount": 1357, // integer minor units
"currency": "CAD" // ISO 4217
}
Pattern B: Receipt object
This is the body Paperless expects from POST /ingest/{provider}/receipts.
It is a flattened, fully self-contained shape — no callbacks required.
Full example
{
"external_id": "ord_ABC123XYZ",
"external_event_id": "evt_acme_8f3e1c9a",
"merchant_id": "MERCHANT_12345",
"location": {
"location_id": "LOC_001",
"name": "Acme Coffee — King St",
"address": "123 King St W",
"city": "Toronto",
"region": "ON",
"country": "CA",
"postal_code": "M5H 1A1",
"timezone": "America/Toronto"
},
"merchant": {
"business_name": "Acme Coffee",
"tax_number": "123456789RT0001"
},
"purchased_at": "2026-05-15T17:21:19Z",
"currency": "CAD",
"subtotal_cents": 1200,
"tax_cents": 157,
"total_cents": 1357,
"line_items": [
{ "name": "Latte", "quantity": 1, "unit_price": 600, "total_price": 600, "tax_cents": 78 },
{ "name": "Croissant", "quantity": 1, "unit_price": 600, "total_price": 600, "tax_cents": 79 }
],
"taxes": [
{ "name": "HST", "rate": "13.0", "type": "ADDITIVE", "amount_cents": 157 }
],
"payments": [
{ "method": "CARD", "last4": "4242", "amount_cents": 1357 }
],
"customer": {
"paperless_customer_id": "3f0e2c1a-7d33-4f0a-9b9b-ac3b5b1c2f88",
"email": null,
"phone": null
}
}
Required vs optional
| Field | Type | Required | Notes |
|---|---|---|---|
external_id | string | Required | Idempotency key. Globally unique per merchant. |
external_event_id | string | Required | Unique per push. Differs from external_id if you re-push the same receipt. |
merchant_id | string | Required | Your merchant ID. |
location | object | Required | Inlined location snapshot. |
merchant.business_name | string | Required | Display name on the receipt. |
merchant.tax_number | string | Optional | Required in jurisdictions that need it on receipts (CA, EU, etc.). |
purchased_at | ISO 8601 UTC | Required | |
currency | ISO 4217 | Required | |
subtotal_cents, tax_cents, total_cents | int | Required | subtotal + tax = total must hold. |
line_items | array | Required | ≥ 1. |
taxes | array | Optional | Empty when there is no tax. |
payments | array | Required | ≥ 1. Sum of amount_cents must equal total_cents. |
customer.paperless_customer_id | UUID | Optional | Strongest match signal. Bare UUID, no PL- prefix. |
customer.email | string | Optional | Used for fallback match. |
customer.phone | E.164 string | Optional | Used for fallback match. |
Refund / void payloads (both patterns)
To record a refund or void after the original receipt has been written, post a webhook event with one of:
{
"external_event_id": "evt_acme_refund_001",
"event_type": "order.refunded",
"created_at": "2026-05-16T11:02:00Z",
"merchant_id": "MERCHANT_12345",
"location_id": "LOC_001",
"data": {
"order_id": "ord_ABC123XYZ",
"refund_id": "rf_acme_001",
"amount_cents": 600,
"reason": "customer_request"
}
}
{
"external_event_id": "evt_acme_void_001",
"event_type": "order.voided",
"created_at": "2026-05-15T17:22:00Z",
"merchant_id": "MERCHANT_12345",
"location_id": "LOC_001",
"data": {
"order_id": "ord_ABC123XYZ",
"reason": "cashier_correction"
}
}
Validation rules Paperless enforces
subtotal_cents + tax_cents == total_cents— strictly. Off-by-one rejects the receipt with 400.sum(payments[*].amount_cents) == total_cents— strictly.- Every
applied_taxes.tax_uidmust reference ataxes[*].uidin the same order. - All money amounts must use the same
currencyas the order/receipt root. purchased_atmay not be more than 60 days in the past or more than 1 hour in the future.last4must match^[0-9]{4}$. Anything else is rejected as a possible PAN leak.