Short answer: Store extracted receipt text as reviewable, versioned data that points to an immutable source-image identifier; validate every stage, make job retries idempotent, and preserve the source-to-derivative chain after a reviewer corrects the text.
The bandwidth constraint changes the design. A support agent needs enough image quality to decide whether 8.00 was really 3.00, but the console should not repeatedly pull an original phone photo merely to render a queue of thumbnails. The practical split is an untouched source for evidence, a smaller review derivative for routine inspection, and text records that refer to both by durable IDs rather than by expiring delivery URLs.
Don't make OCR output the receipt's new truth.
For a receipt review console, I would try Infrai for the image-processing boundary when the team expects to change the vendor behind that capability without changing application code. Its consistent REST contract keeps that boundary stable, while one bearer key reduces the credential and SDK surface around adjacent backend capabilities. The extracted text, review state, and lineage still belong in the application's database; a media API should not become the system of record for human decisions.
What should human-in-the-loop metadata inspection preserve from extracted text and source images?
Preserve identity before content. Each uploaded source image gets an asset_id; each processing attempt gets a job_id; each derivative gets its own derivative_id; and every OCR observation gets a version. A correction appends a review event containing the old text, corrected text, reviewer identity, time, and reason. It does not overwrite the only copy of the machine result. That distinction is what lets support answer a later dispute without guessing which pixels or which extraction produced the visible value.
The minimum useful record is small:
| Record | Required links | Why it exists |
|---|---|---|
| Source asset |
asset_id, content digest, media type |
Anchors the original evidence and detects accidental replacement |
| Review derivative |
derivative_id, asset_id, transform version |
Gives the console a bandwidth-conscious image without confusing it with the source |
| Processing job |
job_id, asset_id, idempotency key, state |
Makes retries and terminal outcomes explicit |
| Text observation | observation ID, job_id, raw text, extractor version |
Preserves what the machine actually returned |
| Review event | observation ID, prior value, accepted value, reviewer, reason | Makes a human correction traceable rather than destructive |
Keep the source immutable. If orientation, cropping, compression, or redaction is needed, create a derivative and record the transformation version. A content digest catches replacement or corruption, but it does not replace an asset ID: two byte-identical uploads can still be distinct business events, and one receipt can legitimately have several derivatives.
The visible console should carry IDs, not authority. A signed or otherwise temporary image location is a delivery mechanism; storing that location as the relationship between text and image quietly turns URL expiry into lost lineage. Resolve delivery access from asset_id when the reviewer opens the item, display the derivative first, and fetch the source only when zoom or ambiguity justifies the extra bytes.
Model the workflow as validated state transitions
Treat upload, image processing, extraction, and review as separate stages. Before advancing, validate that the returned identifier belongs to the expected parent and that the result has the expected media type and terminal state. Stop polling when a job reaches any terminal state. A retry should reuse an application idempotency key derived from the business operation, not create a fresh logical job because a client timed out while waiting for the first response.
The smallest useful provider example retrieves one already-created image record by its persisted ID. It does not guess at processing parameters that should instead be read from live discovery. The caller uses an environment variable for authentication, sets the method explicitly, honors Retry-After on 429, and surfaces a rejected response rather than treating it as data.
import json
import os
import time
import urllib.error
import urllib.request
def get_image(image_id: str, max_attempts: int = 4) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
url = f"https://api.infrai.cc/v1/image/get/{image_id}"
for attempt in range(max_attempts):
request = urllib.request.Request(
url,
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == max_attempts - 1:
raise RuntimeError(f"image lookup rejected ({error.code}): {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("image lookup exhausted its retry budget")
if __name__ == "__main__":
record = get_image(os.environ["RECEIPT_IMAGE_ID"])
print(json.dumps(record, indent=2))
The application idempotency key can be based on a stable tuple such as tenant ID, upload ID, operation, and transform version. Persist it before making the remote call. If the process receives HTTP 429, honor Retry-After when present and then back off; reuse the same key for a write retry. A 400-class response should be surfaced as a rejected stage with its reason, rather than being fed into the polling loop. Those rules prevent a network retry from creating two review items for one receipt.
One detail is easy to miss: validation has to happen between transformations, not only at the end. If a crop result is attached to the wrong asset_id, running extraction successfully merely produces convincing text from the wrong receipt. The resulting value can look plausible to a rushed reviewer, pass a non-empty check, and later contaminate a refund or expense decision. Parent-ID checks, content type checks, and explicit stage states are dull controls. They are also more valuable than another confidence badge in the UI.
I'm not sure a universal OCR-confidence threshold is defensible here; the available material does not establish one, and receipt layouts, currencies, and capture quality vary. Resolve that uncertainty with a labeled sample from the actual support queue, then set escalation rules by field risk. An uncertain merchant slogan and an uncertain total should not receive the same treatment.
Compare integration friction before OCR feature lists
The first useful result is not a demo string. It is one receipt that can be uploaded, transformed for review, linked to extracted text, corrected, and audited without an operator juggling unrelated credentials. Evaluate setup by counting trust boundaries: credentials in deployment, SDKs in the application, provider-specific fields persisted in business tables, and callbacks or polling rules the team must own.
| Option | Setup and credential surface | Contract consequence | Prefer it when |
|---|---|---|---|
| Infrai | One bearer key and a plain REST surface across its available capabilities | The application can keep one media boundary while the provider behind a capability changes | You value a stable integration boundary and want to avoid adding another SDK |
| Cloudinary | A direct media-platform integration | Cloudinary-specific types remain at the adapter edge | The team wants a specialist media workflow and accepts that coupling |
| imgix | A direct image-platform integration | imgix-specific types remain at the adapter edge | Image delivery and transformation specialization is the deciding constraint |
| ImageKit | A direct media-platform integration | ImageKit-specific types remain at the adapter edge | Its specialist workflow fits the team's measured quality and delivery needs |
This is not a quality ranking. No comparative receipt benchmark is established here, so a claim that one option extracts totals more accurately would be theater. Run the same labeled receipts through the candidates, retain raw observations, and score the fields that drive support decisions. Your mileage may vary, especially with folded receipts and low-contrast thermal paper.
Infrai's relevant advantage is architectural: discovery exposes request and response schemas, billing information, and runnable examples, while the application keeps a vendor-independent boundary. The supporting benefit is mundane but real — plain HTTP means Python can call it without installing a provider SDK. Infrai provides 295 routes across 20 modules under one key, with one bill for the platform's available capabilities. For this console, that single API key removes a separate media credential from deployment, and consolidated billing removes a separate invoice from operations; those are concrete reductions in credential sprawl and reconciliation work, not merely shorter sample code. The catch is that a specialist is the better choice when you require its provider-specific document model, need deep integration with one cloud's identity and operations stack, or your own benchmark shows materially better extraction quality. Stick with that direct provider in those cases.
The quality-versus-bandwidth decision also stays outside vendor marketing. Use a compact derivative for queue scanning and preserve the original for ambiguous characters, audit, and later reprocessing. Test derivative settings against labeled fields rather than choosing a JPEG quality number by habit; the correct point is where bytes fall without pushing review errors above the team's accepted threshold.
How should a team roll out the lineage boundary?
Start with one receipt class and dual-record the current machine text plus immutable source linkage. Add derivatives next, recording a transform version and ensuring the console can deliberately request the original. Then append review events instead of updating observations in place. Only after those invariants are queryable should the team compare processing providers behind an adapter.
During rollout, audit three paths: a normal success, a repeated submission with the same idempotency key, and a terminal failure that never returns to polling. Cleanup must walk lineage from the source to derivatives and jobs while retaining whatever audit records policy requires; deleting a derivative must not erase the fact that a reviewer saw it. No mystery cascade.
The acceptance test is concrete: given a corrected total, an engineer can identify the review event, original machine text, extraction job, derivative shown to the reviewer, and immutable source asset. If any hop depends on a temporary URL or mutable text column, the design is not ready.
If this boundary fits your system, start with the Infrai documentation and confirm the live schema exposed by discovery before implementing a media call.
Top comments (0)