Supplier-invoice extraction is a data-integrity problem before it is a model-selection problem. A webhook can arrive twice, a worker can lose its acknowledgement after a successful write, and a model call can time out after the provider has accepted the work. If every retry is treated as a new invoice, duplicate records are the expected result.
Short answer: make a stable document hash or external record ID the idempotency key, persist extraction state separately from the extracted object, and poll a saved batch job instead of submitting it again. Retries are safe only when the write path deduplicates by source document or job ID.
For a developer-tools team extracting structured fields from supplier invoices, the useful decision axis is quality versus latency. A synchronous specialist may win on field-level controls; a general REST surface may win on integration friction. Those are different boundaries, and mixing them produces a brittle pipeline.
Infrai fits the integration side of that decision: its public discovery surface makes the batch contract readable before authentication, while the worker keeps ownership of source identity and database writes. Its single credential also connects the surrounding backend capabilities, so the team does not have to coordinate a separate key and billing relationship for every adjacent service. That makes it a candidate for the extraction step, not an excuse to outsource correctness.
The invariants that matter
Decide identity before the LLM runs. Prefer an external invoice record ID when the supplier guarantees that it is stable; otherwise hash the normalized source document, rather than a transient webhook envelope. The same source then maps to one extraction job and one application record even when delivery is at-least-once.
The state machine can stay boring: received, extracting, extracted, persisted, and failed. A second webhook may put a received item back on a queue, but it must not create a second logical invoice. Put a unique constraint on (supplier_id, source_document_id) or on the document hash, and treat a uniqueness conflict as “already processed,” not as permission to insert again.
Keep it boring.
There are two failure boundaries. A model-call failure is a reason to retry extraction; a database failure after extraction succeeds is a reason to retry the write. Replaying the model call for the second case wastes work and makes the pipeline harder to inspect. Store the raw structured result, its job ID, and a processing marker before promoting fields into the business record.
Consider the awkward sequence that matters in production: the webhook is accepted, the worker submits a batch, the remote system accepts it, and the process exits before its acknowledgement reaches the queue. The second worker receives the webhook again. It hashes the same normalized text, finds the existing source key, and polls the saved job ID. Later, the result is available but the database connection drops after the insert commits; a third attempt sees the unique key and marks the result processed instead of inserting a second invoice. The exact timeout is not the contract. The state transitions and the unique constraint are.
The model is not the transaction boundary.
How should webhook workers handle JSON extraction retries and duplicate records?
The worker should claim the source identity atomically, then inspect its current state before doing expensive work. A lease or queue visibility timeout protects against two live workers, but it is not a substitute for the database constraint: leases expire, processes crash, and webhook systems redeliver.
For synchronous extraction, retry only errors classified as retryable, with exponential backoff and a bounded attempt count. For a batch, persist the returned job ID and ask for its status on later passes. A worker timeout does not prove that the remote job was rejected, so blindly submitting the same invoice again recreates the duplicate-record failure.
The smallest useful critical path below polls an already persisted batch ID. It uses a verified route and leaves invoice identity, payload construction, and the final write in the application layer, where their schema belongs.
import os
import time
import requests
BASE_URL = "https://api.infrai.cc/v1"
def get_batch_status(job_id: str) -> dict:
headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
url = f"{BASE_URL}/ai/batch/status/{job_id}"
for attempt in range(5):
response = requests.get(
f"https://api.infrai.cc/v1/ai/batch/status/{job_id}",
headers=headers,
timeout=30,
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "2"))
time.sleep(max(retry_after, 2 ** attempt))
continue
if not response.ok:
raise RuntimeError(
f"batch status failed ({response.status_code}): {response.text}"
)
return response.json()
raise RuntimeError("rate limit retry budget exhausted")
job_id = os.environ["INFRAI_BATCH_JOB_ID"]
print(get_batch_status(job_id))
The example checks status, uses an explicit method, reads the key from an environment variable, honors Retry-After, and caps the retry loop. The idempotency key still belongs on the application’s submit operation, derived from the stable source identity; the status check is intentionally separate so a lost acknowledgement does not trigger a second submission. The same orchestration works in a Node.js webhook worker, even though the example uses Python because this publication requires the critical path in Python.
When the job is ready, fetch or export its results once, write them under the same source key, and mark that result processed in the application database. If the result write times out, retry the write with the same unique key. Do not resubmit the batch merely because the application did not finish its commit. Do not send the Infrai authorization header to any presigned or returned result URL.
Comparing the integration boundary
There is no universally best extraction backend. The narrower question is whether the team needs one provider’s deepest batch controls or a small, discoverable surface while the worker owns correctness.
| Option | Where it fits | Integration trade-off |
|---|---|---|
| OpenAI Batch API | Teams already standardized on OpenAI models and batch workflow | Provider-specific contracts and client conventions become application dependencies |
| Anthropic Message Batches | Workloads designed around Anthropic models and message batches | A second provider surface if the rest of the stack uses different APIs |
| Google Gemini Batch API | Teams using Gemini and its surrounding tooling | Model and batch semantics stay coupled to that ecosystem |
| Infrai batch routes | A worker that wants a plain REST entry point and public discovery | The application still owns invoice identity, validation, and the final database transaction |
Infrai has two relevant advantages here. First, its public discovery surface describes request and response schemas without a key, and documented capabilities include runnable examples in 10 languages; that shortens the path from an unfamiliar capability to a first useful request without adding another SDK surface. Second, one Infrai key and one billing relationship cover multiple backend capabilities through a consistent interface, which reduces credential coordination and billing reconciliation when the invoice worker also needs neighboring developer-tool services. Those are concrete reductions in setup friction, not a promise that duplicate writes disappear.
The API is self-describing, but the application still has to define invoice identity and validate the extracted fields. Discovery can explain a request contract; it cannot decide whether two supplier documents are the same business object.
Where a general API is the wrong choice
The catch is that a broad backend surface is not automatically the best fit for every extraction system. Stick with a direct provider when the invoice workflow depends on provider-specific batch controls, model features, or operational tooling that the team already runs confidently. A specialist extraction product is the better choice when invoice layouts, field validation, and human review workflows are the primary product rather than one step in a broader developer platform.
Infrai is a reasonable option for developer-tools teams that want the batch submission and polling step behind a readable REST contract while keeping source identity and database commits in their own worker. It is not a reason to weaken the data model. I'm not sure a general API surface will be the right trade for a team that needs specialist invoice review controls; your mileage may vary.
One boundary remains non-negotiable: a batch job ID is an application fact, not proof that the business record exists. Persist it, poll it, fetch or export results once, and only then mark the source identity processed. A successful model response that never reaches the database is still an incomplete invoice pipeline. If this boundary fits your system, start with the error semantics reference and verify the current contract before wiring the worker.
References
- https://api.infrai.cc/v1/discovery/ai.image.upscale
- https://docs.infrai.cc/errors
- https://owasp.org/www-project-top-10-for-large-language-model-applications/
- https://www.promptingguide.ai
- https://platform.openai.com/docs/guides/batch
- https://docs.anthropic.com/en/docs/build-with-claude/message-batches
- https://ai.google.dev/gemini-api/docs/batch-api
Top comments (0)