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.

  1. Barcode token — strongest. The order carries an explicit Paperless customer UUID written at checkout.
  2. POS-customer mapping — fast. Paperless has previously linked your POS's customer ID to a Paperless customer.
  3. Email / phone fallback — best-effort. Paperless looks up the POS customer's contact details and tries to match them in its own customer DB.
  4. 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"
}
Why the prefix?

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:

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"
}
FieldTypeRequiredNotes
customer_idstringRequiredMust equal the path param.
emailstringOptionalLowercase, trimmed.
phoneE.164OptionalInclude the leading + and country code.
given_name, family_namestringOptionalDisplay 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:

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:

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.