Pattern A — Full integration

The bidirectional model. You host OAuth, locations, orders, and (optionally) customers. Paperless connects each merchant via OAuth, receives signed webhooks, and pulls the full order on demand.

At a glance
  1. (Prerequisite) You mint an OAuth client_id + client_secret for the Paperless app and hand them to us. Without this step, no merchant can connect.
  2. Merchant clicks "Connect" in the Paperless admin → OAuth handshake.
  3. Paperless calls GET /locations to sync the merchant's stores.
  4. For every completed sale, you POST a signed event to /webhooks/{provider}.
  5. Paperless calls GET /orders/{id} with the access token to fetch the full order.
  6. Paperless resolves the customer, writes a receipt, and pushes it to the customer's app.
  7. Paperless refreshes the merchant's access token before it expires.

0. Credentials handoff Prerequisite

Before any of the steps below can happen, you must register the Paperless application on your platform and hand us the resulting OAuth credentials. Until we have them, our authorize URL is invalid, the Paperless "Connect" button fails immediately, and no merchant can install the integration.

0.1 What you mint

For each environment we run (at minimum: sandbox and production), you create one OAuth client representing the Paperless application and produce three values:

ValueTypeNotes
client_idstringIssued by you. Public-ish (it ends up in the merchant's browser address bar during authorize). Stable across rotations.
client_secretstringIssued by you. Treat like a database root password. Rotatable.
Redirect URI allowlistURLYou whitelist our callback URL. We will give you the exact value per environment (e.g. https://api.sandbox.paperless.example/integrations/{provider}/oauth/callback). Exact match including trailing slashes.

Two pairs is the minimum (sandbox + prod). Three is better — add a local-dev pair so our engineers can run end-to-end tests against your sandbox without burning the shared sandbox credentials.

0.2 How to hand them to us

Pick the most secure channel your security team will approve. In rough order of preference:

  1. Partner developer portal. You host a portal (like developer.squareup.com); we log in with an account you provision, create an app, and read the credentials ourselves. Best long-term: self-service, audit log lives on your side, rotation is one click. Required if you plan to support more partners than just Paperless.
  2. Shared password-manager vault. 1Password / Bitwarden / Keeper shared vault entry, scoped to named individuals on both sides. The most common channel for early partnerships.
  3. One-time-view secret link. Onetimesecret.com or your internal equivalent. Link expires on first read. Good for the secret half; the client_id can be sent over normal email.
  4. PGP-encrypted email to a specific named Paperless engineer whose public key you've verified.
Channels to avoid

Plain email, Slack DM, Jira tickets, Zoom chat, SMS, and "read it over the phone." Every one of these has a real-world breach story attached. If your security team is comfortable with one of them anyway, document why in the contract.

0.3 What Paperless does with the credentials

  1. They are stored in our hosting platform's managed secret store — encrypted at rest, access-controlled, and scoped to the services that need them — within 1 business day of receipt.
  2. They are injected into our backend containers as environment variables at container start (e.g. {PROVIDER}_CLIENT_ID, {PROVIDER}_CLIENT_SECRET). They are never baked into container images.
  3. The application reads them at startup. They are never written to logs, error pages, container images, or git.
  4. Access is limited to named Paperless maintainers, with two-factor authentication enforced on the platform account.
  5. We do not currently provide per-secret access logs. If your security policy requires them, raise it at onboarding and we will scope it before go-live.

0.4 Information you need from Paperless before issuing credentials

To register the Paperless app on your platform you will need from us:

Your Paperless solutions engineer will provide all of these in a single document at kickoff.

0.5 Rotation

0.6 Onboarding checklist

Print this and walk through it with your Paperless solutions engineer at kickoff:

Hard dependency

Step 1 (OAuth handshake) is impossible to start without step 0 complete. If a merchant clicks "Connect" before Paperless has your client_id in its config, they see an error and the integration looks broken to them — not to you. Schedule the credentials handoff as the very first deliverable, before any engineering work.

1. OAuth handshake

Standard OAuth 2.0 authorization-code flow. You are the authorization server; Paperless is the client application. The client_id and client_secret below are the ones you minted and handed to us in step 0.

1.1 Authorization request

Paperless redirects the merchant's browser to:

GET https://your-pos.example.com/oauth/authorize
  ?client_id=<client_id you issued to Paperless>
  &response_type=code
  &redirect_uri=https://api.paperless.example/integrations/{provider}/oauth/callback
  &scope=ORDERS_READ%20PAYMENTS_READ%20MERCHANT_PROFILE_READ%20ITEMS_READ
  &state=<csrf_token>
Who issues what

In OAuth, the authorization server (you) issues the credentials and the client application (Paperless) consumes them — same pattern as Square's sq0idb-… / sq0idp-… application IDs. Paperless does not mint its own client ID; it stores the one you gave us in our secrets manager and reads it from {PROVIDER}_CLIENT_ID at runtime.

Your authorization page asks the merchant to log in (if needed) and consent to the requested scopes. On consent, redirect back to redirect_uri with code and state:

302 Found
Location: https://api.paperless.example/integrations/{provider}/oauth/callback
  ?code=AUTH_CODE_HERE
  &state=<csrf_token_echoed_back>
Required scopes

At minimum, your scope vocabulary should let Paperless read orders, payments, the merchant profile, and the item catalog. The exact scope names are yours to define (you publish them; Paperless adopts them). Map them to the four capabilities above and document them with your solutions engineer.

1.2 Token exchange

Paperless exchanges the code for tokens by POSTing to your token endpoint:

POST https://your-pos.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE_HERE
&client_id=<client_id>
&client_secret=<client_secret>
&redirect_uri=https://api.paperless.example/integrations/{provider}/oauth/callback

You respond with:

{
  "access_token":  "at_live_abc123...",
  "refresh_token": "rt_live_xyz789...",
  "token_type":    "Bearer",
  "expires_in":    2592000,
  "expires_at":    "2026-06-14T17:21:19Z",
  "merchant_id":   "MERCHANT_12345",
  "scope":         "ORDERS_READ PAYMENTS_READ MERCHANT_PROFILE_READ ITEMS_READ"
}
FieldTypeRequiredNotes
access_tokenstringRequiredBearer token. Paperless will use it in the Authorization header.
refresh_tokenstringRequiredLong-lived. Used to mint new access tokens without re-prompting the merchant.
token_typestringRequiredMust be Bearer.
expires_in OR expires_atint / ISO 8601RequiredProvide at least one. expires_at is preferred (absolute UTC).
merchant_idstringRequiredYour stable merchant identifier. Paperless persists it.
scopestringOptionalSpace-separated. Defaults to the requested scope.

1.3 Merchant profile

After token exchange, Paperless calls a profile endpoint to populate display data:

GET https://your-pos.example.com/v1/merchants/me
Authorization: Bearer at_live_abc123...
{
  "merchant_id":  "MERCHANT_12345",
  "business_name": "Acme Coffee",
  "country":       "CA",
  "tax_number":    "123456789RT0001",
  "address":       "123 King St W",
  "city":          "Toronto",
  "currency":      "CAD"
}

1.4 Reference: Python

import requests

def exchange_code(code: str) -> dict:
    response = requests.post(
        "https://your-pos.example.com/oauth/token",
        data={
            "grant_type":    "authorization_code",
            "code":          code,
            "client_id":     CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "redirect_uri":  REDIRECT_URI,
        },
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

2. Location sync

Immediately after a successful connect, Paperless calls your locations endpoint to enumerate the merchant's stores.

GET https://your-pos.example.com/v1/locations
Authorization: Bearer at_live_abc123...
{
  "locations": [
    {
      "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",
      "active":      true
    }
  ]
}
FieldTypeRequired
location_idstringRequired
namestringRequired
address, city, region, country, postal_codestringOptional
timezoneIANA tzRequired
activebooleanOptional (defaults to true)
Tip

If you add or remove a location, also emit a webhook event of type location.updated so Paperless can re-sync without waiting for the next manual reconnect.

3. Order webhook

The trigger that drives the entire receipt pipeline. For every completed sale, you POST a small signed event to Paperless. See the dedicated webhook contract page for the full spec (HMAC scheme, headers, retries). The minimum viable payload:

{
  "external_event_id": "evt_acme_8f3e1c9a-2c11-4c1b-9c3c-c5e1c0f1abc1",
  "event_type":        "order.completed",
  "created_at":        "2026-05-15T17:21:19Z",
  "merchant_id":       "MERCHANT_12345",
  "location_id":       "LOC_001",
  "data": {
    "order_id": "ord_ABC123XYZ"
  }
}

Paperless responds 200 {"ok": true} on success. The order details are fetched in step 4.

4. Order fetch

After accepting the webhook, the Paperless worker fetches the full order from your API:

GET https://your-pos.example.com/v1/orders/ord_ABC123XYZ
Authorization: Bearer at_live_abc123...
Accept: application/json

The response shape is documented in full on the payload schema page. Skeleton:

{
  "id":          "ord_ABC123XYZ",
  "merchant_id": "MERCHANT_12345",
  "location_id": "LOC_001",
  "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":      [ /* ... */ ],
  "taxes":           [ /* ... */ ],
  "tenders":         [ /* ... */ ],
  "custom_attributes": {
    "paperless_customer_id": { "value": "PL-3f0e2c1a-..." }
  }
}

4.1 cURL

curl -sS \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json" \
  "https://your-pos.example.com/v1/orders/ord_ABC123XYZ"

4.2 Failure behavior Paperless expects

ResponsePaperless reaction
200 + bodyProcess the order.
401 / 403Treat token as invalid. Trigger refresh. Retry once.
404Drop event silently — order does not exist on your side (probably a test).
429Honor Retry-After if present; otherwise exponential backoff.
5xxRetry up to 3× with exponential backoff (30 s → 60 s → 120 s). Then DLQ.

5. Customer identifier on orders

For the receipt to land in a customer's app, the order must carry a Paperless customer identifier. The simplest way: support an order-level custom attribute named paperless_customer_id whose value is PL-<uuid>. Your terminal / cashier flow writes this in when the customer scans the Paperless barcode at checkout.

"custom_attributes": {
  "paperless_customer_id": {
    "value": "PL-3f0e2c1a-7d33-4f0a-9b9b-ac3b5b1c2f88"
  }
}

If your platform does not support order-level custom attributes, the alternative is to expose a customer-lookup endpoint and rely on email/phone fallback matching. See customer matching for the full chain.

6. Token refresh

A daily scheduled task on the Paperless side refreshes tokens that are within 7 days of expiry. Your token endpoint must accept the standard refresh grant:

POST https://your-pos.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=rt_live_xyz789...
&client_id=<client_id>
&client_secret=<client_secret>
{
  "access_token":  "at_live_NEW...",
  "refresh_token": "rt_live_NEW...",
  "token_type":    "Bearer",
  "expires_at":    "2026-07-14T17:21:19Z"
}
Refresh-token rotation

If you rotate refresh tokens on every refresh, return the new value in the response. Paperless will persist it immediately. If you do not rotate, omit refresh_token from the refresh response.

6.1 Refresh failure

If you return 400 invalid_grant on refresh, Paperless marks the integration as disconnected, stops calling your APIs, and emails the merchant to reconnect.

7. Disconnect / revoke

When a merchant disconnects via the Paperless UI, Paperless calls:

POST https://your-pos.example.com/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=at_live_abc123...
&token_type_hint=access_token
&client_id=<client_id>
&client_secret=<client_secret>

You return 200. Both the access and refresh tokens for that merchant should be invalidated server-side. Paperless also stops sending order fetches for that merchant.

Implementation checklist