Read a receipt from your own code.
A bearer-key REST API for uploading receipts, reading extraction results, and building exports — the same engine the web app runs on, without a browser session in the loop.
Every request authenticates with a key in the Authorization header. No cookies, no CORS, no session state.
Authentication
- 1
Create a key. Sign in to the web app, open Settings → API Keys, and choose which scopes it needs. The full key is shown once, at creation — copy it immediately. Only owners and admins can create or revoke keys.
- 2
Allowlist your server's IP. Also under Settings — required, deny-by-default. A valid key from an unlisted address is rejected outright. Calling from a rotating IP range? List the range, not one address.
- 3
Send it as a bearer token on every request:
Authorization: Bearer rk_live_••••••••••••••••••••••••••••••| Scope | Grants |
|---|---|
receipts:read | List and fetch receipts |
receipts:write | Upload receipts, submit corrections |
exports:read | Fetch export status and download links |
exports:write | Request a new export |
A key only grants what its scopes list. Missing one gets a distinct 403 insufficient_scope — never a generic auth failure — so the two are easy to tell apart in your logs.
receipts:writeUpload one receipt as multipart/form-data. Extraction happens asynchronously — this returns as soon as the file is stored.
curl -X POST \
https://smartreceipt-api.zajelsoft.com/api/v1/receipts \
-H "Authorization: Bearer $KEY" \
-F "file=@transfer_slip.jpg"{
"receipt_id": "9c2b1e4a-...",
"duplicate": false
}- Accepted types: JPEG, PNG, WEBP, HEIC/HEIF, TIFF, PDF. Max 20 MB.
- Poll
GET /api/v1/receipts/{id}untilstatusleavesqueued/processing. - Safe to retry — uploading identical bytes again for the same tenant returns the original receipt with
duplicate: true, never a second row. Retry blindly on a timeout.
receipts:readList receipts, newest first.
Query params (all optional): batch_id, status, bank, currency, date_from, date_to, q (searches extracted text), cursor (from the previous page's next_cursor), limit (default 50, max 200).
{
"receipts": [
{
"id": "9c2b1e4a-...",
"status": "approved",
"bank_name": "Bank of Palestine",
"amount": "1250.00",
"currency": "ILS",
"reference_number": "TX-88213940",
"transaction_date": "2026-08-14",
"review_reasons": [],
"confidence": "0.95",
"created_at": "2026-08-14T10:02:11Z"
}
],
"next_cursor": null
}Money is always a string — "1250.00", never a JSON number. Parse it as a decimal, or you'll lose precision the same way this platform refuses to.
receipts:readFull detail — the list shape above, plus raw_text, sender_name, receiver_name, account_number, transaction_type, notes, provenance fields (provider, model, prompt_version, token counts), and original_url — a short-lived signed link to the source file, null once it's aged past your retention window.
A cross-tenant or nonexistent id returns a plain 404, never a 403 — this API never confirms a receipt exists in an account that isn't yours.
receipts:writeCorrect one or more extracted fields — only while the receipt is extracted or needs_review. Once approved, a correction is rejected: that status is a sign-off on specific values, not a draft.
Send only the fields you're changing, from: bank_name, bank_name_original, amount, currency, reference_number, transaction_date, transaction_time, sender_name, receiver_name, account_number, transaction_type, primary_language, notes. Anything else in the body is rejected by name.
Returns the updated detail object. Correcting the last thing flagging a receipt moves its status to extracted automatically.
exports:writeBuild a CSV or JSON export — the same engine behind the web app's Export screen. Only approved receipts are ever included.
{
"format": "csv",
"filters": {
"currency": "ILS",
"date_from": "2026-08-01",
"date_to": "2026-08-31"
},
"columns": [
{ "field": "transaction_date", "header": "Date" },
{ "field": "amount", "header": "Amount" },
{ "field": "currency", "header": "Currency" }
]
}format:"csv"or"json"(XLSX isn't built yet). Only formats your plan includes are accepted.columnsis optional — omit for the default set. Selectingamountrequires also selectingcurrency: a bare amount column is a list of numbers in mixed currencies that adds up to nothing real.- Returns immediately with
status: "queued".
exports:read{
"id": "b7e1...",
"format": "csv",
"status": "ready",
"row_count": 42,
"error_message": null,
"created_at": "2026-09-05T09:00:00Z",
"expires_at": "2026-09-06T09:00:00Z",
"download_url": "https://smartreceipt-files.zajelsoft.com/...&signature=..."
}download_url only appears while status == "ready" and unexpired — a fresh signed URL on every poll, always valid for whatever remains of the original 24-hour window.
Errors
Every non-2xx response takes this shape:
{ "error": { "code": "quota_exceeded", "message": "...", "details": {} } }| Code | Status | Meaning |
|---|---|---|
validation_error | 422 | Malformed or invalid request body / params |
not_found | 404 | Doesn't exist, or belongs to another tenant |
unsupported_type | 415 | File's content-type isn't in the accepted list |
file_too_large | 413 | Over the 20 MB per-file limit |
quota_exceeded | 402 | Would exceed your plan's monthly quota |
insufficient_scope | 403 | Your key doesn't have the scope this route needs |
ip_not_allowed | 403 | Valid key, but this IP isn't on your tenant's allowlist |
A 401 with no JSON body means the Authorization header itself is missing, malformed, or the key is invalid, revoked, or expired.
Worked example
Upload, poll, export — start to finish.
# 1. Upload
RECEIPT_ID=$(curl -s -X POST "$BASE/api/v1/receipts" \
-H "Authorization: Bearer $KEY" \
-F "file=@transfer_slip.jpg" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['receipt_id'])")
# 2. Poll until it's out of the queue
while true; do
STATUS=$(curl -s "$BASE/api/v1/receipts/$RECEIPT_ID" \
-H "Authorization: Bearer $KEY" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['status'])")
[ "$STATUS" != "queued" ] && [ "$STATUS" != "processing" ] && break
sleep 2
done
echo "landed on: $STATUS" # approved, needs_review, or failed
# 3. Export your approved receipts
curl -s -X POST "$BASE/api/v1/exports" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"format": "csv"}'Not built yet
- Purchase invoices. The web app's invoice flow — vendor, tax ID, line items — has no
/api/v1/invoicesequivalent yet. A real gap, not a design choice; email us if you need it. - Rate limiting. Nothing currently throttles request volume on these routes. Don't hammer the API regardless — a concrete limit may land later.
- Webhooks. Not built. Poll for status changes for now.