Send a document.
Read back what was on it.
The API takes the same documents as the app and returns the same fields. It is included on the Business plan, up to 2,500 documents a month.
Every route, field and error is in the API reference. The OpenAPI document generates a client in most languages.
Beyond receipts, the API reads passports, contracts, tax forms and more: see every document it reads, by industry, type, region and use case.
Send a document
Upload one file in a multipart field named file, up to 25 MB, or 50 MB for a PDF sent to be split into a set. PaperTotal goes by the file’s bytes, not its name: JPEG, PNG, WebP, HEIC, TIFF, PDF, DOCX, XLSX, CSV, TXT, HTML, EML or ZIP.
Send a clientId of your own so that a retry returns the first document rather than a new one.
curl https://api.papertotal.com/v1/documents \
-H "Authorization: Bearer ptk_..." \
-F "file=@receipt.jpg" \
-F "type=RECEIPT" \
-F "clientId=expense-import-000184"Read it back
Reading takes a moment, so a new document starts in processing. Fetch it by id until status is PROCESSED or FAILED. Money is in minor units, so 6226 is $62.26, and dates are YYYY-MM-DD.
curl https://api.papertotal.com/v1/documents/DOCUMENT_ID \
-H "Authorization: Bearer ptk_..."{
"id": "0199f1c2-7a3e-7c41-9b1e-5d2f0a8e4c17",
"type": "RECEIPT",
"source": "API",
"status": "PROCESSED",
"extraction": {
"fields": {
"merchantName": "Pearl Street Lunch Room",
"date": "2026-10-21",
"currency": "USD",
"subtotalMinor": 5200,
"discountMinor": 400,
"taxMinor": 426,
"tipMinor": 1000,
"totalMinor": 6226
},
"confidence": 0.82,
"flags": []
}
}Keys and errors
Every request carries Authorization: Bearer ptk_.... A key is shown once and stored as a hash, so keep it somewhere safe. Keys are scoped: creating documents takes documents:write, and listing or fetching them takes documents:read.
Every error has the same shape, and message is written to be shown to a person.
| Code | Meaning | Status |
|---|---|---|
| UNAUTHENTICATED | The key is missing, revoked or wrong. | 401 |
| PLAN_LIMIT_REACHED | This month’s documents are used up. | 402 |
| SUBSCRIPTION_REQUIRED | The workspace has no active plan. Reads still work. | 402 |
| IDEMPOTENCY_IN_PROGRESS | The first request with this key is still running. Wait for Retry-After. Nothing was charged. | 409 |
| IDEMPOTENCY_CONFLICT | That key was used for a different request. Use a new key. | 409 |
| PAYLOAD_TOO_LARGE | The file is over 25 MB, or over 50 MB for a PDF sent to be split into a set. | 413 |
| UNSUPPORTED_FILE | Send a JPEG, PNG, WebP, HEIC, TIFF, PDF, DOCX, XLSX, CSV, TXT, HTML, EML or ZIP. | 415 |
| FILE_TOO_SMALL | The file is empty, or too small to be a document. | 422 |
| IMAGE_TOO_SMALL | The image has too few pixels to read. | 422 |
| FILE_CORRUPTED | The file is damaged. Export or scan it again. | 422 |
| FILE_ENCRYPTED | The file has a password on it. Remove the password. | 422 |
| PLAN_FEATURE_REQUIRED | This workspace’s plan does not include the API. Reads still work. | 402 |
| VALIDATION_FAILED | The details list each field that needs attention. | 422 |
| RATE_LIMITED | Too many requests. Try again shortly. | 429 |
{
"error": {
"code": "PLAN_LIMIT_REACHED",
"message": "This month's limit is used up. Upgrade to keep going.",
"details": { "metric": "scans", "limit": 2500, "period": "2026-10" },
"requestId": "..."
}
}Rotate a key without stopping anything
- Create the new key. Under Settings, Developers, choose Manage and then Rotate this key, or call
POST /v1/api-keys/:id/rotate. It has the same access, and the old key keeps working. - Roll it out everywhere the old one is used. Ask
GET /v1/api-keys/verifyfrom each place to see what the key grants and when it expires. The old key's last-used time tells you when nothing is left on it. - Revoke the old key. It stops on its next request, and the new one carries on. Each key remembers the one it replaced.
# 1. Make the new key. The old one keeps working.
curl -X POST https://api.papertotal.com/v1/api-keys/KEY_ID/rotate \
-H "Authorization: Bearer YOUR_SIGN_IN_TOKEN"
# 2. Roll it out, then check what it grants from where it runs.
curl https://api.papertotal.com/v1/api-keys/verify \
-H "Authorization: Bearer ptk_live_NEW_KEY"
# 3. Revoke the old one. It stops on its next request.
curl -X DELETE https://api.papertotal.com/v1/api-keys/KEY_ID \
-H "Authorization: Bearer YOUR_SIGN_IN_TOKEN"Sign your requests
A key can also have a client secret, shown once. Sign the timestamp, method, path with its query string, and the exact bytes of the body with HMAC-SHA256, joined by dots, and send the hex digest in X-PaperTotal-Request-Signature with the Unix time in X-PaperTotal-Request-Timestamp. A signature is good for 30 minutes either way.
A signature that is sent is always checked. A key set to require one refuses anything unsigned, with a 401. A signature covers a JSON body or none, so send a file as file_data rather than multipart.
const timestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify(payload);
const signature = createHmac('sha256', clientSecret)
.update(`${timestamp}.POST./v1/documents/url.${body}`)
.digest('hex');
await fetch(`${apiUrl}/v1/documents/url`, {
method: 'POST',
body,
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
'X-PaperTotal-Request-Timestamp': String(timestamp),
'X-PaperTotal-Request-Signature': signature,
},
});Copy receipts to your customers
A merchant needs no API for this. When you email a customer their receipt, copy receipts@papertotal.com as well, from an address we have added to our list. Write to support@papertotal.com to be added.
We file it only when every line opposite holds. A customer has to have agreed to merchant copies, and anything else is left unfiled.
- From is one address, exactly as it is on our list.
- To is one address, the customer's, which they have verified with PaperTotal.
- Cc holds
receipts@papertotal.com. - The receipt is the only attachment: a PDF or an image. Write amounts with ISO 4217 currency codes.
- A DKIM signature from your own domain passes and covers the To header. SPF alone is not enough, because it says nothing about who the message is for.
Build an app people connect
A key is for your own workspace. For an app other people connect to theirs, register it under Settings, Developers and use the authorization code flow with PKCE. Each person sees what it asks for, chooses a workspace and can take any of it away.
A token acts as the person who granted it, with no more than they can do, in that one workspace. An access token lasts 30 minutes. Refreshing it replaces the refresh token too, and a spent one presented again ends the connection. Redirect URLs match exactly.
scope=all asks for everything your app may ask for. The rest of the details are at the server’s metadata.
# Send the person here, with a PKCE challenge
GET https://api.papertotal.com/oauth/authorize
?response_type=code
&client_id=ptc_...
&redirect_uri=https://your-app.example.com/callback
&scope=expenses:read reports:read
&state=...
&code_challenge=...
&code_challenge_method=S256
# Swap the code they come back with for tokens
curl https://api.papertotal.com/oauth/token \
-d grant_type=authorization_code \
-d code=... \
-d code_verifier=... \
-d redirect_uri=https://your-app.example.com/callback \
-d client_id=ptc_... \
-d client_secret=pts_...Connect an AI assistant
PaperTotal runs a Model Context Protocol server at https://api.papertotal.com/mcp. An assistant connects through OAuth: you sign in, choose a workspace and say what it may do.
It sees what you can see in that workspace and nothing else. It can search and read your expenses, reports, receipts, reimbursements and spending. It cannot change anything, unless you tick the one permission that lets it file a document you hand it. Every call is in your audit log.
To cut one off, disconnect it in the assistant, or under Settings, Profile in PaperTotal.
Claude
Add a custom connector, give it https://api.papertotal.com/mcp as the server URL, then choose Connect and sign in. On a Team or Enterprise plan, an owner adds it under Organization settings, Connectors.
ChatGPT
Turn on developer mode in ChatGPT’s connector settings, create a connector with that address, and choose OAuth. Then open the + menu in a chat and select it.
Cursor
Add it to Cursor in one click, or add this to ~/.cursor/mcp.json.
{
"mcpServers": {
"papertotal": { "url": "https://api.papertotal.com/mcp" }
}
}Use the command line
papertotal, also installed as pt, lists, totals and exports your receipts, reads any document, and serves the process_document tool to an assistant on your computer. Every command takes --json, and an HTTP error exits with its status less 300, so a 404 exits 104.
It is not on npm yet. Install it from this site with Node 22 or later, or download papertotal-cli-0.1.0.tgz.
Set PAPERTOTAL_API_KEY, or sign in with your own app: papertotal login --client-id and its client id, with the redirect URL http://localhost:4517/callback. The sign-in is kept in your macOS keychain or Linux keyring.
npm install -g https://papertotal.com/downloads/papertotal-cli-0.1.0.tgz
export PAPERTOTAL_API_KEY=ptk_live_...
papertotal receipts --category Meals --year 2026
papertotal categories
papertotal export --out ./receipts-2026 --year 2026
cat receipt.pdf | papertotal process - --name receipt.pdf --jsonLet a coding assistant run it
Claude Code, Codex, Cursor and other coding assistants can install the CLI and run it for you. You do the account setup, so your key never goes through the chat:
- In PaperTotal, under Settings, Developers, API keys, create a key with documents:read, documents:write, expenses:read and cards:read.
- Set it as PAPERTOTAL_API_KEY in the terminal you start the assistant from.
- Give the assistant this prompt.
The skill it reads lists every command, flag and exit code, and tells it never to ask for your key.
Install the PaperTotal CLI:
npm install -g https://papertotal.com/downloads/papertotal-cli-0.1.0.tgz
Then read the skill that comes with it:
$(npm root -g)/papertotal/skill/SKILL.md
My API key is already in PAPERTOTAL_API_KEY. Do not ask me for it.
Use --json on every command.Claude Desktop, on your computer
papertotal mcp serves the process_document tool over stdio. Add it to claude_desktop_config.json with the path that which papertotal prints, then restart Claude Desktop.
{
"mcpServers": {
"papertotal": {
"command": "/usr/local/bin/papertotal",
"args": ["mcp"],
"env": { "PAPERTOTAL_API_KEY": "ptk_live_..." }
}
}
}Client libraries
Generated from the OpenAPI document and tested against the API: a typed method for every call, paging, retries that wait as long as Retry-After says, typed errors with the code and request id, and uploads straight to storage. Money stays in minor units.
They are not in package registries yet. Download one, and its README says how to add it to a project.
Get help with the API
Write to support@papertotal.com, and a person answers by email. It is the one place for bugs and questions about the API, the libraries and the CLI: there is no public issue tracker yet.
Put the request id in your message, so we can find the exact call. Every response carries it in the X-Request-Id header, and every error body has it as requestId.
Try the product first.
The API is part of the Business plan. The trial runs on Pro.