SmartReceiptPublic API
https://smartreceipt-api.zajelsoft.com

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.

Current scope: bank & money-transfer receipts. Purchase invoices (vendor bills, itemised store receipts) work in the web app but have no API endpoint yet — see Not built yet.

Authentication

  1. 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. 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. 3

    Send it as a bearer token on every request:

Authorization: Bearer rk_live_••••••••••••••••••••••••••••••
ScopeGrants
receipts:readList and fetch receipts
receipts:writeUpload receipts, submit corrections
exports:readFetch export status and download links
exports:writeRequest 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.

POST/api/v1/receiptsscope receipts:write

Upload one receipt as multipart/form-data. Extraction happens asynchronously — this returns as soon as the file is stored.

Request
curl -X POST \
  https://smartreceipt-api.zajelsoft.com/api/v1/receipts \
  -H "Authorization: Bearer $KEY" \
  -F "file=@transfer_slip.jpg"
Response — 202 Accepted
{
  "receipt_id": "9c2b1e4a-...",
  "duplicate": false
}
  • Accepted types: JPEG, PNG, WEBP, HEIC/HEIF, TIFF, PDF. Max 20 MB.
  • Poll GET /api/v1/receipts/{id} until status leaves queued / 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.
GET/api/v1/receiptsscope receipts:read

List 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).

Response — 200 OK
{
  "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.

GET/api/v1/receipts/{id}scope receipts:read

Full 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.

PATCH/api/v1/receipts/{id}scope receipts:write

Correct 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.

POST/api/v1/exportsscope exports:write

Build a CSV or JSON export — the same engine behind the web app's Export screen. Only approved receipts are ever included.

Request
{
  "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.
  • columns is optional — omit for the default set. Selecting amount requires also selecting currency: a bare amount column is a list of numbers in mixed currencies that adds up to nothing real.
  • Returns immediately with status: "queued".
GET/api/v1/exports/{id}scope exports:read
Response — 200 OK
{
  "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": {} } }
CodeStatusMeaning
validation_error422Malformed or invalid request body / params
not_found404Doesn't exist, or belongs to another tenant
unsupported_type415File's content-type isn't in the accepted list
file_too_large413Over the 20 MB per-file limit
quota_exceeded402Would exceed your plan's monthly quota
insufficient_scope403Your key doesn't have the scope this route needs
ip_not_allowed403Valid 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/invoices equivalent 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.