Customer matching
How a receipt finds its owner. The goal: every receipt arrives in the right customer's Paperless inbox without the customer having to do anything beyond a one-time scan.
The resolution chain
When Paperless ingests an order, it tries to attach it to a known customer using the following chain. The first successful step wins; lower-priority steps are not tried.
- Barcode token — strongest. The order carries an explicit Paperless customer UUID written at checkout.
- POS-customer mapping — fast. Paperless has previously linked your POS's customer ID to a Paperless customer.
- Email / phone fallback — best-effort. Paperless looks up the POS customer's contact details and tries to match them in its own customer DB.
- Unmatched — receipt is stored with no customer. Surfaces if the customer later claims it.
1. Barcode token
The Paperless mobile app shows a barcode containing the customer's UUID, prefixed with
PL-. At the till, the cashier scans the barcode; your POS captures the
string and writes it onto the order.
Where to put it
Pattern A — in the order's custom attributes:
"custom_attributes": {
"paperless_customer_id": {
"value": "PL-3f0e2c1a-7d33-4f0a-9b9b-ac3b5b1c2f88"
}
}
Pattern B — in the receipt's customer
block (drop the PL- prefix):
"customer": {
"paperless_customer_id": "3f0e2c1a-7d33-4f0a-9b9b-ac3b5b1c2f88"
}
The barcode payload is PL-<uuid> on purpose, so terminal-side
scanners can recognize it as a Paperless barcode. Paperless strips the prefix on
ingest. In Pattern A keep the prefix as-is; in Pattern B you may keep or strip it —
Paperless accepts both.
What if the custom attribute is not supported?
Some POS platforms do not have a generic custom-attribute facility. You have two alternatives:
- Use any other order-level free-text field (note field, reference number, external reference) — coordinate the field name with your Paperless solutions engineer at onboarding.
- Skip the barcode entirely and rely on email/phone fallback (see below).
2. POS-customer mapping Pattern A only
After the first email/phone match, Paperless persists a mapping from your
customer_id to its Paperless customer. The next time it sees an order from
the same POS customer, the lookup is a single DB read — no contact data needed.
// Order from your API:
{
"id": "ord_LATER",
"customer_id": "cust_acme_007", // ← Paperless looks this up
...
}
For the mapping to exist, Paperless must have successfully matched a previous order either by barcode (which seeds the mapping) or by email/phone (step 3).
3. Email / phone fallback
When the order has no barcode and no existing mapping, Paperless calls your customer endpoint:
GET https://your-pos.example.com/v1/customers/cust_acme_007 Authorization: Bearer at_live_abc123...
{
"customer_id": "cust_acme_007",
"email": "alex@example.com",
"phone": "+14165551234",
"given_name": "Alex",
"family_name": "Doe"
}
| Field | Type | Required | Notes |
|---|---|---|---|
customer_id | string | Required | Must equal the path param. |
email | string | Optional | Lowercase, trimmed. |
phone | E.164 | Optional | Include the leading + and country code. |
given_name, family_name | string | Optional | Display only. |
Paperless tries to match email first, then phone. On match,
Paperless saves a PosCustomerMapping(provider, external_customer_id → paperless_customer_id)
so future orders from the same POS customer skip this endpoint entirely.
In Pattern B
You include the contact data directly on the receipt body — there is no callback:
"customer": {
"paperless_customer_id": null,
"email": "alex@example.com",
"phone": "+14165551234"
}
4. Unmatched receipts
If none of the steps above produce a match, the receipt is stored with a null customer reference. It is not lost:
- If the customer later installs the Paperless app and verifies an email or phone that matches the unmatched receipt, the receipt is retroactively attached.
- The customer can also claim a receipt manually using a 6-character claim code printed on the paper receipt (if your POS prints one) — outside the scope of this integration.
End-to-end decision diagram
┌────────────────────────────┐
│ Order arrives at Paperless │
└────────────┬───────────────┘
│
▼
┌─────────────────────────────────┐
│ Does it carry paperless_customer_id? │
└────────────┬────────────────────┘
yes │ │ no
▼ ▼
┌─────────────────┐ ┌─────────────────────────────────┐
│ Look up by UUID │ │ Does (provider, ext_cust_id) │
│ → match (stop) │ │ exist in PosCustomerMapping? │
└─────────────────┘ └────────────┬────────────────────┘
yes │ │ no
▼ ▼
┌──────────────────┐ ┌──────────────────────────────┐
│ Use mapped UUID │ │ Call /customers/{ext} or use │
│ → match (stop) │ │ contact block (Pattern B). │
└──────────────────┘ │ Match email, then phone. │
└────────────┬─────────────────┘
yes │ │ no
▼ ▼
┌──────────────────┐ ┌────────────────────┐
│ Save mapping, │ │ Store unmatched. │
│ → match (stop) │ │ Surface on claim. │
└──────────────────┘ └────────────────────┘
Minimum your POS must support
To make this work, you must support at least one of:
- An order-level free-text field that can carry
paperless_customer_id(preferred — best match rate). - A
GET /v1/customers/{id}endpoint returning email / phone (enables fallback matching for repeat customers). - (Pattern B only) Inline contact details in the pushed receipt body.
Supporting both the barcode field and the customer endpoint is ideal: every new customer matches immediately via barcode, and every repeat customer matches in one DB hit via the mapping cache.