What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
A Node.js worker that sends supplier invoices to an LLM has two separate duplication risks. The first is the model call: a timeout, a 429, or a dropped connection can send the same document again. The second is the business write: a retried or redelivered job can create a second payable. The OpenAI SDK’s retries and its idempotencyKey request option reduce the first risk for one logical call. Schema-constrained output makes the response shape predictable, but it does not make the invoice values correct. The second risk is controlled only by your own database, keyed to the invoice’s business identity, with every attempt recorded so that a replay can be recognised rather than guessed at.
Two retry layers, and where the SDK stops
The OpenAI JavaScript/TypeScript SDK is intended for server-side JavaScript environments, including Node.js, and the OpenAI developer quickstart begins with a Responses API call. The client also retries some failures without your code asking it to. OpenAI’s Node.js SDK configuration documentation states: “The client retries temporary connection errors and HTTP 408, 409, 429, and 500-or-higher responses twice by default.” (Client Configuration, openai-node)
As an Amazon Associate I earn from qualifying purchases.
That sentence describes one layer. Your extraction system has at least three, and each one needs a stated owner:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →| Layer | What it retries | Default or starting behaviour | Who controls it |
|---|---|---|---|
| OpenAI Node SDK, inside one call | Temporary connection errors; HTTP 408, 409, 429, and 500 or higher | Two retries by default; request timeout of ten minutes by default | maxRetries and timeout, set on the client or per request |
| Your worker, across attempts | Whole extraction attempts after a failure your code classifies as retryable | Not set by the SDK; you choose the attempt budget and backoff | Your application code |
| Queue or orchestrator | Redelivery of a message that was not acknowledged | Depends on the broker or orchestrator; not covered by the SDK | Broker or orchestrator configuration |
The SDK cannot tell whether a timed-out request already wrote a row, whether the queue will redeliver the message, or whether an accounting system already holds the invoice. Those questions belong to the worker.
#1 Best Overall
- PORTABLE SCANNER FOR USE ON-THE-GO — The fastest and lightest mobile single-sheet-fed compact document scanner in its class¹
- QUICK DOCUMENT SCANNING ― This Epson ultra-fast scanner scans a single page as quickly as 5.5 seconds²; Windows and Mac compatible
- VERSATILE PAPER HANDLING ― Portable scanner scans documents up to 8.5 x 72 in; Also easily digitizes receipts and ID cards to make accounting, bookkeeping, and organizing simpler
- INTUITIVE, HIGH-SPEED SOFTWARE — Epson ScanSmart Software³ is a smart tool allowing you to easily scan, review, and save; Stay organized easily with the help of this Epson scanner
- EASY SETUP — USB-powered connect to your computer for quick and simple scanning; No batteries or external power supply required to operate portable document scanner; Standard Connectivity: USB 2.0
Retry settings also multiply. With SDK defaults and a worker allowing four total attempts, one document that keeps receiving 429 responses can produce up to twelve model requests: four worker attempts, each with the original request and two SDK retries. That is an upper bound, not an expected cost, but it is the figure to budget against. Choose one layer to own most retries. The starting configuration later in this guide keeps the SDK at one retry per call and places job-level backoff in the worker.
Schema-constrained output proves shape, not truth
The SDK’s structured-output path uses responses.parse() with a schema helper, and the parsed result is exposed as output_parsed. The openai-node Structured Outputs documentation sets out the schema rules. The example below uses Zod. Install openai and zod, and check the helper’s Zod requirement in that document for the version you install.
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
export const SupplierInvoice = z.object({
supplier_name: z.string(),
supplier_tax_id: z.string().nullable(),
invoice_number: z.string(),
issue_date: z.string(),
due_date: z.string().nullable(),
currency: z.string(),
subtotal_minor: z.number().int(),
tax_minor: z.number().int(),
total_minor: z.number().int(),
line_items: z.array(
z.object({
description: z.string(),
amount_minor: z.number().int(),
})
),
});
const client = new OpenAI({
maxRetries: 1, // SDK-level retries for this call only
timeout: 120_000, // milliseconds; confirm whether this applies per HTTP attempt
});
export class ExtractionError extends Error {
constructor(code, status) {
super(code);
this.code = code;
this.status = status;
}
}
export async function extractInvoice({ documentText, jobId, extractorVersion, attemptId }) {
const response = await client.responses.parse(
{
model: process.env.EXTRACTION_MODEL,
input: [
{ role: "system", content: "Extract supplier invoice fields as printed. Use null only where the field is absent." },
{ role: "user", content: documentText },
],
text: { format: zodTextFormat(SupplierInvoice, "supplier_invoice") },
},
{
idempotencyKey: `extract:${jobId}:${extractorVersion}`,
headers: { "X-Client-Request-Id": attemptId },
}
);
if (response.status !== "completed" || response.output_parsed == null) {
throw new ExtractionError("no_parsed_output", response.status);
}
return response.output_parsed;
}
The schema establishes a narrower set of facts than it often appears to:
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Rank #2
- FAST SPEEDS - Scans color and black and white documents a blazing speed up to 16ppm (1). Color scanning won’t slow you down as the color scan speed is the same as the black and white scan speed.
- ULTRA COMPACT – At less than 1 foot in length and only about 1. 5lbs in weight you can fit this device virtually anywhere (a bag, a purse, even a pocket).
- READY WHENEVER YOU ARE – The DS-640 mobile scanner is powered via an included micro USB 3. 0 cable allowing you to use it even where there is no outlet available. Plug it into you PC or laptop and you are ready to scan.
- WORKS YOUR WAY – Use the Brother free iPrint&Scan desktop app for scanning to multiple “Scan-to” destinations like PC, Network, cloud services, Email and OCR. (2) Supports Windows, Mac and Linux and TWAIN/WIA for PC/ICA for Mac/SANE drivers. (3)
- OPTIMIZE IMAGES AND TEXT – Automatic color detection/adjustment, image rotation (PC only), bleed through prevention/background removal, text enhancement, color drop to enhance scans. Software suite includes document management and OCR software. (4)
- Field names and types match the declared schema.
- Every property is required. A field that may legitimately be absent is declared as a required nullable field, so absence is an explicit
null, not a missing key. - A schema-valid response can still contain a wrong total, a misread supplier name, or a date taken from the wrong label.
- An incomplete response may leave
output_parsedempty. Checkresponse.statusand the parsed object before reading any field. A missing parse is a failure class in its own right, not an empty invoice.
There is a design choice in the schema itself. Asking the model to convert a printed amount such as “1.234,50 EUR” into integer minor units moves the conversion into the model, where a wrong conversion still passes the schema. A common alternative is to extract the printed amount as text and normalise it in code with a locale-aware parser, so the arithmetic checks run on numbers your code produced. Either design still depends on the validation rules in a later section.
Request keys are not business keys
The SDK’s per-request options include idempotencyKey. The openai-node request options source describes it as a unique key for the request. Set it on the model call so that the SDK’s own retries of one logical call carry the same key, and confirm in your installed version that the key is sent unchanged on each retry. Treat that as protection for one logical request.
It does not protect a database insert, a message published to an accounting queue, or a second job that arrives later for the same invoice from a different mailbox. Whether a provider honours a key across a longer time window is endpoint-specific; check the documentation for the endpoint you call rather than assuming it.
Rank #3
- Fast and Efficient: Scans both sides of a document at the same time, in color, at up to 45 pages per minute, with a 60 sheet automatic feeder, and one touch operation. Innovative Feeding System.
- Reliably Handles Many Different Document Types: Receipts, business cards, reports, contracts, long documents, thick or thin documents, and more. Monochrome LCD Display.
- Designed exclusively for the included Canon CaptureOnTouch software;TWAIN and ISIS drivers are not supported.
- Easy Setup: Simply connect to your computer using the supplied USB-C cable.
- Bundled Software: Includes easy-to-use Canon CaptureOnTouch scanning software.
| Question | Request idempotency key | Business uniqueness rule in your database |
|---|---|---|
| What it scopes | One logical model request as the SDK sends it | A business identity such as supplier and invoice number |
| A second job for the same invoice | Not established by the SDK documentation | Blocked by the unique constraint |
| A worker crash after the payable is written | Not applicable; the payable is outside the model request | The payable row and the job state change in one transaction |
| Who enforces it | The provider, per endpoint | Your database |
Job identity and attempt state
Assign each document one job identity before the first model call. A practical rule is a tenant identifier, a SHA-256 hash of the source file bytes, and the extractor version. The same file processed by the same pipeline maps to the same job. A new extractor version creates a deliberate second job, and the business key in the next section stops that second job from creating a second payable.
Store the job row with its current state, and store each attempt as its own row. The job row answers “what is the state now”; the attempt rows answer “what happened on each try”.
| State | Meaning | Allowed next states |
|---|---|---|
| received | Source stored and hashed | extracting |
| extracting | An attempt is in flight while the worker holds the job’s lease | extracted, failed_retryable, review_required |
| extracted | Parsed output is stored | validated, review_required |
| validated | Arithmetic and identity checks passed | committing |
| committing | The commit transaction is open | committed, review_required |
| committed | The payable exists and is linked to this job | Terminal |
| failed_retryable | The attempt failed and budget remains | extracting |
| review_required | A person must decide | Terminal until a reviewer acts |
Committing once: the write path
The following schema uses PostgreSQL syntax. The principle carries to other databases, which need an equivalent insert-if-absent statement.
Rank #4
- OUR MOST ADVANCED SCANSNAP. Large touchscreen, fast 45ppm double-sided scanning, 100-sheet document feeder, Wi-Fi and USB connectivity, automatic optimizations, and support for cloud services. Upgraded replacement for the discontinued iX1600
- CUSTOMIZABLE. SHARABLE. Select personalized profiles from the touchscreen. Send to PC, Mac, mobile devices, and clouds. QUICK MENU lets you quickly scan-drag-drop to your favorite computer apps
- STABLE WIRELESS OR USB CONNECTION. Built-in Wi-Fi 6 for the fastest and most secure scanning. Connect to smart devices or cloud services without a computer. USB-C connection also available
- PHOTO AND DOCUMENT ORGANIZATION MADE EFFORTLESS. Easily manage, edit, and use scanned data from documents, receipts, photos, and business cards. Automatically optimize, name, and sort files
- AVOIDS PAPER JAMS AND DAMAGE. Features a brake roller system to feed paper smoothly, a multi-feed sensor that detects pages stuck together, and skew detection to prevent paper damage and data loss
CREATE TABLE extraction_jobs (
job_id uuid PRIMARY KEY,
tenant_id text NOT NULL,
source_sha256 text NOT NULL,
extractor_version text NOT NULL,
state text NOT NULL,
version integer NOT NULL DEFAULT 0,
committed_payable_id uuid,
UNIQUE (tenant_id, source_sha256, extractor_version)
);
CREATE TABLE payables (
payable_id uuid PRIMARY KEY,
tenant_id text NOT NULL,
supplier_key text NOT NULL,
invoice_number text NOT NULL,
content_sha256 text NOT NULL,
committed_by_job_id uuid NOT NULL REFERENCES extraction_jobs (job_id),
UNIQUE (tenant_id, supplier_key, invoice_number)
);
CREATE TABLE extraction_attempts (
attempt_id uuid PRIMARY KEY,
job_id uuid NOT NULL REFERENCES extraction_jobs (job_id),
attempt_no integer NOT NULL,
started_at timestamptz NOT NULL,
finished_at timestamptz,
outcome text,
error_class text,
http_status integer,
api_request_id text,
UNIQUE (job_id, attempt_no)
);
The commit then runs in a single transaction:
- Move the job from
validatedtocommittingwith a conditional update:UPDATE extraction_jobs SET state = 'committing', version = version + 1 WHERE job_id = $1 AND version = $2 AND state = 'validated'. If zero rows change, another worker or a newer attempt owns the job. Stop and acknowledge the message. - Insert the payable with
INSERT ... ON CONFLICT (tenant_id, supplier_key, invoice_number) DO NOTHING RETURNING payable_id. - If a row returns, set the job to
committedand setcommitted_payable_id. Commit the transaction. - If no row returns, load the existing payable. If its
content_sha256matches and it was committed by a different job, the same invoice arrived from another source: link this job to the existing payable and mark it committed as a duplicate. If the hash differs, set the job toreview_requiredwith the reasonsupplier_invoice_conflictand commit that status change.
The unique key is a starting point, not a complete rule. Credit notes, corrected invoices, and suppliers that reuse numbers need a documented policy before this constraint is applied to production data.
The database cannot make an external accounting write atomic with the local commit. Write an outbox row in the same transaction as the commit, and let a sender deliver it to the ledger using the payable’s business key as the external reference. If a send times out, query the ledger by that reference before sending again. Not every ledger offers a lookup by external reference, so confirm that before you rely on this step.
Replay scenarios and what protects each one
| Scenario | What the worker does | What prevents a duplicate |
|---|---|---|
| The SDK retries a 429 inside one call | The same idempotencyKey is sent; no database write happens until validation passes |
One logical call; only a validated attempt can reach commit |
| The client times out, but the provider finished the request | The next worker attempt makes a new model call; its output may differ from the first | Only one attempt wins the conditional job update; the other output is discarded and logged |
| The queue redelivers after a lost acknowledgement | The worker loads the job; if it is committed, it acknowledges and exits |
The job state and the unique payable key |
| Two workers hold the same job after a lease expires | The stale worker’s conditional update matches zero rows and it stops | The version column |
| The same invoice arrives by email and through a supplier portal | A second job is created for the second source file | The payable’s unique key and the content hash comparison |
| The local commit succeeds, but the ledger response is lost | The outbox row stays pending; the sender queries the ledger by external reference before resending | The external reference and the reconciliation lookup |
Validating invoice meaning before commit
After the schema check passes, run deterministic rules in your own code. The SDK does not validate invoices, and a model’s self-reported confidence should not replace these checks; the schema above has no confidence field.
Best Value
- FAST DOCUMENT SCANNING — Document scanner with feeder allows you to speed through stacks with a 50-sheet Auto Document Feeder (ADF); Efficient office scanner to help you scan more productively
- INTUITIVE, HIGH-SPEED SOFTWARE — Quickly scan with this desktop document scanner; Epson ScanSmart Software lets you easily preview scans, email files, upload to the cloud, and more; Plus, automatic file naming saves even more time
- SEAMLESS INTEGRATION — Easily incorporate your data into most document management software with the included TWAIN driver; Office document scanner integrates seamlessly with business workflows
- EASY SHARING — Duplex scanner allows you to scan straight to email or popular cloud storage2 services like Dropbox, Evernote, Google Drive, and OneDrive for simple storage and sharing
- SIMPLE FILE MANAGEMENT — Scanner allows the creation of searchable PDFs with Optical Character Recognition (OCR) and convert scans to editable Word or Excel files effortlessly; Designed for home and office document scanning
- Supplier identity: a tax identifier or a supplier key resolved against your vendor master. A supplier name alone is not an identity.
- Invoice number: present after trimming whitespace. Store the printed form and a normalised form, and use the normalised form in the unique key.
- Dates:
issue_dateparses as a calendar date, anddue_date, when present, is on or afterissue_date. Dates outside the window your ingestion rules allow go to review. - Currency: an ISO 4217 code that your ledger accepts for that supplier.
- Amounts: integers in minor units. Do not store or compare amounts as floating-point numbers.
- Arithmetic: the sum of line amounts equals the subtotal, and subtotal plus tax equals the total. Use a zero tolerance for integer minor units unless the document shows per-line rounding. In that case, apply a documented per-line tolerance rather than a global one.
- Grounding: the total and the invoice number appear in the source text after normalising the printed format. A value that cannot be found in the document is a review item.
Failure classes and routing
| Failure class | Examples | Retry | Route |
|---|---|---|---|
| Transient transport or provider | Connection error; HTTP 408, 409, 429, or 500 and above | The SDK retries first; the worker retries with backoff while budget remains | failed_retryable; review_required with reason attempt_budget_exhausted when the budget ends |
| Configuration or access | Authentication or permission errors; an unknown model name | No. These errors are outside the SDK’s retry list. | Stop the consumer and alert. Leave the message unacknowledged so it is not marked failed. |
| Incomplete or unparsed output | status is not completed; output_parsed is missing |
One re-extraction | review_required if the second attempt is also incomplete |
| Schema-valid but failing a business check | Total mismatch; missing supplier identity; date outside the allowed window | No automatic re-extraction after the first attempt; a second call may return the same reading | review_required with the name of the failed check |
| Commit conflict | Same supplier and invoice number, different content hash | No | review_required with reason supplier_invoice_conflict |
| Duplicate of a committed invoice | Same supplier and invoice number, same content hash | No; treat as success | committed, linked to the existing payable |
Attempt-level observability and request IDs
The API reference recommends logging request IDs in production for troubleshooting. It also documents a client-supplied X-Client-Request-Id header. The example above sets that header to the attempt identifier you store, so a support request to the provider, the attempt row, and the worker log refer to the same attempt. The reference is at API Reference, Backward Compatibility and Request IDs.
Record the following for every application attempt:
- The job identifier, attempt identifier, and attempt number
- Start time, finish time, and duration
- Outcome: succeeded, transient_failure, incomplete_output, validation_failed, committed, duplicate_replay, or conflict
- Error class and HTTP status, when present
- The API request ID the server returned, and the
X-Client-Request-Idyou sent - The commit result: none, committed with a payable identifier, or conflict
- The extractor version and the model identifier from configuration
Read the returned request ID from the response in your installed SDK version; the accessor differs across versions, so confirm it in the SDK source before you depend on it. SDK-internal retries may not appear as separate rows in your database, because one attempt row can cover several HTTP requests. If you need per-retry detail, check which logging options your SDK version exposes.
Apply redaction from the start:
- Log hashes and identifiers, not invoice bodies or bank details.
- Mask supplier tax identifiers and account numbers in logs. Keep unmasked values only in restricted tables.
- Keep the original source file in access-controlled storage, so a mismatch can be checked against it.
Starting configuration
These are starting values for a workload with documents of a few pages and a queue that can hold messages for several minutes. Adjust them to your document sizes, latency, and cost limits.
Quick Recap
| Setting | Starting value | Reason |
|---|---|---|
SDK maxRetries |
1 | Covers one transient retry inside a call; the worker owns further attempts. The SDK default is two. |
SDK timeout |
120000 ms | An explicit bound. The SDK default is ten minutes. |
| Worker attempt budget | 3 total attempts | Caps time and model cost per document. Under this setting and the SDK value above, one document makes at most 6 HTTP requests. |
| Backoff between worker attempts | Exponential with jitter, from 5 seconds, capped at 2 minutes | Spreads retries across rate-limit windows rather than repeating them in lockstep |
| Queue visibility timeout | 15 minutes | Above the worst case of three attempts at 120 seconds each, plus backoff, if the SDK timeout applies per HTTP request. Otherwise the message can be redelivered while an attempt is still running. |
| Review trigger | Budget exhausted, or any business check failed | A person decides when the machine cannot resolve the conflict |
What this design does not cover
- Scanned documents: text extraction from images or poor scans is a separate step. The schema and the checks above assume the text reached the model accurately.
- Changing SDK behaviour: the openai-node repository changes over time. Pin the SDK version, and reread the configuration and structured-output documentation when you upgrade.
- Ledger idempotency: what a downstream accounting system does with a repeated write depends on that system and is outside what the SDK documents.
- Master data quality: supplier identity checks are only as good as the vendor master they resolve against.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




