Testing checklist

A staged path from "first signed request reaches Paperless" to "merchant goes live." Do not skip stages — every stage exists because a partner has historically tripped on something at that point.

Stage 0 — Environments and credentials

EnvBase URLWhat it does
Sandboxhttps://api.sandbox.paperless.exampleFunctional testing. No real merchants. Receipts visible only to test customer accounts you provision.
Staginghttps://api.staging.paperless.examplePre-prod parity. Real merchant + real customer end-to-end. Required before go-live.
Productionhttps://api.paperless.exampleLive.

Confirm before you write any code:

Stage 1 — Signature validator

Before sending real receipts, validate your HMAC implementation against the Paperless debug endpoint. It accepts any body and tells you whether the signature is correct.

POST https://api.sandbox.paperless.example/debug/verify-signature
Content-Type: application/json
X-Paperless-Signature: <your hex signature>
X-Paperless-Timestamp: <unix seconds>
X-Paperless-Provider:  acme

{ "any": "payload" }
ResponseMeaning
200 {"valid": true}You're good.
200 {"valid": false, "reason": "bad_signature"}HMAC mismatch.
200 {"valid": false, "reason": "skew"}Timestamp drift > 5 min.
404Unknown provider slug.

Common failure modes

Stage 2 — Single test webhook (Pattern A) / single test push (Pattern B)

Pattern A

BODY='{"external_event_id":"evt_smoke_001","event_type":"order.completed","created_at":"2026-05-15T17:21:19Z","merchant_id":"MERCHANT_SANDBOX_1","location_id":"LOC_001","data":{"order_id":"ord_smoke_001"}}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SIGNING_SECRET" | awk '{print $2}')

curl -sS -X POST \
  -H "Content-Type: application/json" \
  -H "X-Paperless-Signature: $SIG" \
  -H "X-Paperless-Timestamp: $TS" \
  --data "$BODY" \
  "https://api.sandbox.paperless.example/webhooks/acme"

Expected: 200 {"ok": true}. Then within ~30 s, Paperless calls your sandbox GET /v1/orders/ord_smoke_001. Have a hand-rolled fixture ready that returns the order in the schema from payload schema.

Pattern B

POST a full receipt to /ingest/acme/receipts per Pattern B. Expected: 201 {"receipt_id": "...", "matched_customer": true} if you included the test customer's paperless_customer_id.

Stage 3 — End-to-end with a real test customer

  1. Sign into the sandbox Paperless mobile app with your test customer account.
  2. Display the barcode. Note the PL-<uuid> payload.
  3. Ring a transaction in your sandbox POS, scan the barcode, complete the sale.
  4. Wait ≤ 5 s, open the mobile app's receipt inbox. The receipt should appear.
  5. Verify on the receipt: line items, taxes, tenders, merchant, location all correct.

Stage 4 — Edge cases

Cover each of these against sandbox before staging:

ScenarioExpected behavior
Duplicate external_event_id200 {"ok": true, "duplicate": true}. No second receipt.
Bad signature401. No event stored.
Skewed timestamp401.
Unknown provider404.
Order with no barcode, unknown customerReceipt stored unmatched. Customer must claim.
Order with no barcode, known customer (email match)Receipt matched on email. Mapping saved (Pattern A).
Refund event for unknown order_id400 in Pattern A (event accepted but refund dropped). In B you must wait for receipt to exist first.
Multiple tenders (split tender)Receipt shows both. Sum equals total.
Cash-only sale (no last4)Receipt shows tender method = CASH.
Refund-then-resend (idempotency)Only one refund applied.
Re-deliver same event 5 minutes laterDeduped. No-op.
Token expires mid-flight (Pattern A)Order fetch returns 401, Paperless refreshes, retries, succeeds.

Stage 5 — Load test

Stage 6 — Staging dry run

Staging mirrors production. Repeat stages 3 and 4 against one real pilot merchant. Do not connect any live merchants until staging is green.

Stage 7 — Go-live

Sample signed-webhook fixture

Copy this exact fixture to confirm your verifier matches Paperless's. Secret: test_secret_do_not_use_in_prod.

Body:
{"external_event_id":"evt_fixture_1","event_type":"order.completed","created_at":"2026-05-15T17:21:19Z","merchant_id":"MERCHANT_FIXTURE","location_id":"LOC_FIXTURE","data":{"order_id":"ord_fixture_1"}}

Timestamp:
1747329679

Canonical string:
1747329679.{...the body above, byte-for-byte...}

Expected signature (hex, lowercase):
<run the canonical string through HMAC-SHA256 with the secret; your value should match what
 Paperless returns on the /debug/verify-signature endpoint.>

Troubleshooting

SymptomProbable cause
401 on every requestPretty-printed JSON, wrong secret, clock skew, or hex case.
200 on webhook but no receipt in appPattern A order fetch is failing — check Paperless logs (ask ops) and your /v1/orders/{id} endpoint.
Receipt appears with wrong customerEmail/phone fallback matched a different customer with the same contact. Use the barcode instead.
Receipt appears unmatched repeatedlyCustom attribute field name doesn't match. Confirm with ops; the agreed-upon field name must be paperless_customer_id in Pattern A.
Receipt has wrong tax breakdownTax name may not match GST / HST recognition list; relevant only for CA reporting.
Receipt totals are off by 1 centsubtotal + tax ≠ total. Paperless requires strict equality.