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.
- (Prerequisite) You mint an OAuth
client_id+client_secretfor the Paperless app and hand them to us. Without this step, no merchant can connect. - Merchant clicks "Connect" in the Paperless admin → OAuth handshake.
- Paperless calls
GET /locationsto sync the merchant's stores. - For every completed sale, you POST a signed event to
/webhooks/{provider}. - Paperless calls
GET /orders/{id}with the access token to fetch the full order. - Paperless resolves the customer, writes a receipt, and pushes it to the customer's app.
- 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:
| Value | Type | Notes |
|---|---|---|
client_id | string | Issued by you. Public-ish (it ends up in the merchant's browser address bar during authorize). Stable across rotations. |
client_secret | string | Issued by you. Treat like a database root password. Rotatable. |
| Redirect URI allowlist | URL | You 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:
- 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.
- Shared password-manager vault. 1Password / Bitwarden / Keeper shared vault entry, scoped to named individuals on both sides. The most common channel for early partnerships.
- One-time-view secret link. Onetimesecret.com or your internal
equivalent. Link expires on first read. Good for the secret half; the
client_idcan be sent over normal email. - PGP-encrypted email to a specific named Paperless engineer whose public key you've verified.
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
- 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.
- 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. - The application reads them at startup. They are never written to logs, error pages, container images, or git.
- Access is limited to named Paperless maintainers, with two-factor authentication enforced on the platform account.
- 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:
- App display name and logo (shown on your consent screen).
- The exact redirect URI(s) per environment.
- Webhook callback URL per environment (where you POST signed events).
- The set of scopes we are requesting (see section 1).
- A privacy policy URL and a support contact email.
Your Paperless solutions engineer will provide all of these in a single document at kickoff.
0.5 Rotation
- Default cadence: rotate
client_secretevery 12 months. - On suspected compromise: rotate within 1 business day of either side flagging it.
- Dual-secret window: for a 7-day period after rotation, your token endpoint should accept either the old or the new
client_secret. This lets Paperless cut over without dropping in-flight merchant connects. After 7 days the old secret is retired. - Notification: give us at least 30 days notice for planned rotations. Emergency rotations can be done same-day on a phone bridge.
0.6 Onboarding checklist
Print this and walk through it with your Paperless solutions engineer at kickoff:
- [ ] Channel agreed for credential handoff (and named contacts on both sides).
- [ ] Sandbox
client_id+client_secretissued and delivered. - [ ] Production
client_id+client_secretissued and delivered. - [ ] Redirect URIs whitelisted in your portal:
https://api.sandbox.paperless.example/integrations/{provider}/oauth/callbackand the prod equivalent. - [ ] Scopes whitelisted on our app:
ORDERS_READ,PAYMENTS_READ,MERCHANT_PROFILE_READ,ITEMS_READ(or your equivalents). - [ ] Rotation cadence and emergency contact agreed in writing.
- [ ] First sandbox merchant connects end-to-end successfully (validates the credentials are wired correctly).
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>
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>
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"
}
| Field | Type | Required | Notes |
|---|---|---|---|
access_token | string | Required | Bearer token. Paperless will use it in the Authorization header. |
refresh_token | string | Required | Long-lived. Used to mint new access tokens without re-prompting the merchant. |
token_type | string | Required | Must be Bearer. |
expires_in OR expires_at | int / ISO 8601 | Required | Provide at least one. expires_at is preferred (absolute UTC). |
merchant_id | string | Required | Your stable merchant identifier. Paperless persists it. |
scope | string | Optional | Space-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
}
]
}
| Field | Type | Required |
|---|---|---|
location_id | string | Required |
name | string | Required |
address, city, region, country, postal_code | string | Optional |
timezone | IANA tz | Required |
active | boolean | Optional (defaults to true) |
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
| Response | Paperless reaction |
|---|---|
200 + body | Process the order. |
401 / 403 | Treat token as invalid. Trigger refresh. Retry once. |
404 | Drop event silently — order does not exist on your side (probably a test). |
429 | Honor Retry-After if present; otherwise exponential backoff. |
5xx | Retry 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"
}
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
- [ ] Credentials minted and handed to Paperless for sandbox and production (see step 0).
- [ ] Redirect URIs and scopes whitelisted for the Paperless app on your portal.
- [ ] OAuth authorize endpoint with consent screen.
- [ ] OAuth token endpoint supporting both
authorization_codeandrefresh_tokengrants. - [ ] OAuth revoke endpoint.
- [ ] Merchant profile endpoint (
/v1/merchants/me). - [ ] Locations endpoint (
/v1/locations). - [ ] Order fetch endpoint (
/v1/orders/{id}) returning the schema on payload schema. - [ ] Webhook emitter signing every event with HMAC-SHA256.
- [ ] Webhook retry loop (3 attempts, exp. backoff 30/60/120 s) on non-2xx.
- [ ] Order-level
paperless_customer_idcustom attribute support. - [ ] (Optional) Customer lookup endpoint for email/phone fallback.
- [ ] Sandbox environment Paperless ops can hit before production.
- [ ] Signing-key rotation procedure documented internally.