ExpenseReceipt
Expense receipt upload and OCR processing
Poll the OCR result for an uploaded receipt
GET /api/v1/expenses/ocr/{receipt_ref}
Requires authentication (Bearer token) and the X-Organization-Id header.
Receipt-OCR poll (Epic #604). The client polls the opaque receipt_ref
returned by POST /api/v1/expenses/upload-receipt until the capture is
terminal. Scope expenses:read / permission expenses.view. Org-scoped:
captures never cross organizations; an unknown or cross-org receipt_ref
returns 404. Extracted fields keys equal StoreExpenseRequest exactly;
amounts are decimal strings, dates Y-m-d, currency ISO-4217. Provider is
self-hosted (EU residency); orphan captures are purged after the
retention window (default 30 days).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
receipt_ref |
path | string | yes | Opaque UUID returned by the upload endpoint. |
organization_id |
query | string | no |
Responses
| Status | Description |
|---|---|
| 202 | OCR still processing — keep polling. |
| 200 | Terminal capture state (done, needs_review or failed). |
| 404 | Unknown receipt_ref or it belongs to another organization. |
Example request
curl -X GET "https://app.encryptinvoice.com/api/v1/expenses/ocr/{receipt_ref}" \
-H "Authorization: Bearer {{access_token}}" \
-H "X-Organization-Id: {{organization_id}}" \
-H "Accept: application/json"
Upload a receipt for asynchronous OCR extraction
POST /api/v1/expenses/upload-receipt
Requires authentication (Bearer token) and the X-Organization-Id header.
Receipt-OCR upload (Epic #604, task #607). Stores the receipt on the
organization's S3 disk under expenses/receipts/{uuid}.ext, creates a
staging capture and enqueues an async OCR job on the ocr queue. Returns
202 with an opaque receipt_ref the client then polls via
GET /api/v1/expenses/ocr/{receipt_ref}.
Scope expenses:write / permission expenses.create. Org-scoped — captures never cross organizations.
Idempotency: supply an Idempotency-Key header. A repeated key (same org)
returns the same capture with no new storage write and no new job.
Optional expense_id enables post-create mode: OCR enriches that existing
expense in place instead of creating a capture-driven expense. The
expense_id must belong to the caller's organization.
The extracted fields keys equal StoreExpenseRequest exactly; amounts are
decimal strings, dates Y-m-d, currency ISO-4217. Provider is self-hosted
(PaddleOCR-VL + local LLM, EU residency, no external AI calls). Orphan
captures (never promoted to an expense) are purged after the retention
window (default 30 days). No receipt bytes or extracted PII are logged.
Note: an unsupported media type is rejected as 422 (via the mimes
validation rule), not 415. An over-size file is 413 when it exceeds the
organization storage byte-quota, otherwise 422 (the file max:10240 rule).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key |
header | string | no | Optional client-supplied key. Same key (same org) returns the same receipt_ref with no duplicate capture or OCR job. |
Responses
| Status | Description |
|---|---|
| 202 | Receipt stored; OCR enqueued. Poll receipt_ref for the result. |
| 503 | Storage or OCR provider unavailable — the client should queue and retry. |
| 413 | Receipt exceeds the organization storage byte-quota. |
| 422 |
Example request
curl -X POST "https://app.encryptinvoice.com/api/v1/expenses/upload-receipt" \
-H "Authorization: Bearer {{access_token}}" \
-H "X-Organization-Id: {{organization_id}}" \
-H "Accept: application/json"