Pattern B — Push mode

The one-way model. You POST a complete, signed receipt to Paperless after every completed sale. No OAuth, no order fetch, no token refresh.

At a glance
  1. Paperless ops provisions your merchant out of band and shares a signing key + API key.
  2. For every completed sale, you POST a full Receipt object to /ingest/{provider}/receipts.
  3. You sign the request with HMAC-SHA256 and include the static API key in Authorization.
  4. Paperless dedupes by external_id, resolves the customer, and stores the receipt.

1. Onboarding

Without OAuth, you onboard a new merchant by working with Paperless ops directly:

  1. You provide: your merchant ID, business name, country, locations, tax number.
  2. Paperless creates the merchant record and returns: API_KEY, SIGNING_SECRET, provider slug, base URL.
  3. You store the secrets in a vault on your side. Treat them like production database credentials.
Key rotation

Paperless will rotate the signing secret every 12 months by default. You can request rotation at any time. A dual-key window of 7 days lets you flip cleanly without dropping webhooks. See Security for the procedure.

2. The push endpoint

POST https://api.paperless.example/ingest/{provider}/receipts
Content-Type: application/json
Authorization: Bearer <API_KEY>
X-Paperless-Signature: <hex-encoded HMAC-SHA256 of the raw body>
X-Paperless-Timestamp: 1747329679
Idempotency-Key: ord_ABC123XYZ
HeaderRequiredNotes
AuthorizationRequiredBearer with the static API key.
X-Paperless-SignatureRequiredHMAC-SHA256 of the raw body, hex-encoded.
X-Paperless-TimestampRequiredUnix seconds. Rejected if skew > 5 minutes.
Idempotency-KeyRequiredShould equal the receipt's external_id.
Content-TypeRequiredMust be application/json.

2.1 Request body

The body is a complete Receipt object. See payload schema for every field. 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
  }
}

2.2 Response

StatusBodyMeaning
201 Created{"receipt_id": "...", "matched_customer": true}New receipt stored.
200 OK{"receipt_id": "...", "matched_customer": true, "duplicate": true}Duplicate external_id — already stored. Safe to ignore.
400 Bad RequestValidation errorSchema problem. Do not retry; fix and re-send.
401 UnauthorizedBad API key or bad signature.
409 ConflictValidation errorSame external_id with conflicting data. Do not retry.
429 Too Many RequestsHonor Retry-After.
5xxTransient. Retry per the schedule below.

3. Signing the request

Paperless verifies signatures using the same scheme described on the webhook contract page, but in the opposite direction — you sign and Paperless verifies.

canonical_string = X-Paperless-Timestamp + "." + raw_request_body
signature        = HMAC-SHA256(SIGNING_SECRET, canonical_string)
X-Paperless-Signature = hex(signature)

3.1 Python reference

import hashlib
import hmac
import json
import time
import requests

def push_receipt(receipt: dict) -> dict:
    body = json.dumps(receipt, separators=(",", ":"), sort_keys=True).encode("utf-8")
    timestamp = str(int(time.time()))
    canonical = timestamp.encode("utf-8") + b"." + body
    signature = hmac.new(
        SIGNING_SECRET.encode("utf-8"),
        canonical,
        hashlib.sha256,
    ).hexdigest()

    response = requests.post(
        f"https://api.paperless.example/ingest/{PROVIDER}/receipts",
        data=body,
        headers={
            "Authorization":         f"Bearer {API_KEY}",
            "Content-Type":          "application/json",
            "X-Paperless-Signature": signature,
            "X-Paperless-Timestamp": timestamp,
            "Idempotency-Key":       receipt["external_id"],
        },
        timeout=5,
    )
    response.raise_for_status()
    return response.json()

3.2 cURL

BODY=$(cat receipt.json)
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')

curl -sS -X POST \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Paperless-Signature: $SIG" \
  -H "X-Paperless-Timestamp: $TS" \
  -H "Idempotency-Key: ord_ABC123XYZ" \
  --data-binary "$BODY" \
  "https://api.paperless.example/ingest/$PROVIDER/receipts"

4. Retry strategy

You own retries in push mode (there is no webhook layer between you and the receipt store). On a non-2xx response that is retry-eligible (5xx or 429), retry up to 3 times with exponential backoff:

AttemptWait before next try
1 (initial)30 s
260 s
3 (final)DLQ on your side; alert ops.

Each retry must use the same external_id and the same body. Re-sign with a fresh X-Paperless-Timestamp on every attempt.

5. Customer identification

In push mode, the customer is identified by one of three things on the receipt body:

  1. customer.paperless_customer_id — the barcode value the cashier captured at checkout. Strongest signal.
  2. customer.email — must match exactly an email Paperless has on file.
  3. customer.phone — E.164 format. Must match exactly.

Send whichever you have. Paperless tries them in order. If none match, the receipt is stored unmatched and will surface to the customer if they later attach the merchant.

See customer matching for the full resolution chain.

6. Limitations vs Pattern A

Implementation checklist