Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Schema-constrained output and the OpenAI Node SDK’s retries make a single extraction call more reliable, but neither one stops a supplier invoice from being booked twice. Duplicate payables are prevented in your own code and database: a stable identity for each invoice, an attempt record written before each model call, validation before any commit, and a uniqueness rule the write path cannot bypass. The SDK’s retry settings and its idempotencyKey request option act on individual HTTP requests, so their reach ends where your database transaction begins.

What the SDK handles, and where it stops

Schema-constrained parsing

OpenAI’s JavaScript/TypeScript SDK is intended for server-side JavaScript environments, including Node.js. The OpenAI developer quickstart demonstrates a Responses API call. For structured extraction, the openai-node Structured Outputs documentation shows responses.parse() with a schema helper, and the parsed object is returned as output_parsed.

The schema has to fit the strict JSON Schema subset the API supports. In practice, every property is required, and a field that may be absent is declared as required and nullable. Amounts are kept as strings on purpose, so the value stays exactly as printed until your validation step converts it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { z } from 'zod';

const LineItem = z.object({
  description: z.string(),
  quantity: z.string(),
  unit_price: z.string(),
  line_total: z.string(),
});

export const SupplierInvoice = z.object({
  supplier_tax_id: z.string().nullable(),
  supplier_name: z.string(),
  invoice_number: z.string(),
  invoice_date: z.string(),
  currency: z.string(),
  subtotal: z.string().nullable(),
  tax_total: z.string().nullable(),
  total: z.string(),
  line_items: z.array(LineItem),
});

Schema validation confirms field names, types and nesting. It does not confirm that a value is the one printed on the invoice: a well-formed object with a transposed total passes the schema. The semantic checks later in this guide cover that gap.

#1 Best Overall
Sale
Epson Workforce ES-50 Compact & Lightweight Mobile Document Scanner
  • 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

Check the response before you read any field. An incomplete response may come back without parsed output, so test the response status and the presence of output_parsed first, and treat anything else as a failed attempt.

import OpenAI from 'openai';
import { zodTextFormat } from 'openai/helpers/zod';

const client = new OpenAI({ maxRetries: 2, timeout: 120_000 });

export async function extractInvoice(invoiceText: string, clientRequestId: string) {
  const response = await client.responses.parse(
    {
      model: process.env.INVOICE_EXTRACTION_MODEL!,
      input: [
        { role: 'system', content: 'Extract the fields of this supplier invoice exactly as printed. Use null for a nullable field that is absent.' },
        { role: 'user', content: invoiceText },
      ],
      text: { format: zodTextFormat(SupplierInvoice, 'supplier_invoice') },
    },
    { headers: { 'X-Client-Request-Id': clientRequestId } },
  );

  if (response.status !== 'completed' || !response.output_parsed) {
    throw new Error('incomplete_or_unparsed');
  }
  return { parsed: response.output_parsed, responseId: response.id };
}

Confirm the helper and option names against the package version you install. The Structured Outputs page and the SDK’s request-options.ts source are the reference points.

Transport retries

The SDK’s configuration documentation states:

“The client retries temporary connection errors and HTTP 408, 409, 429, and 500-or-higher responses twice by default.”

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The source is OpenAI’s Client Configuration page in openai-node. The same page documents maxRetries and timeout, with a default request timeout of ten minutes; the timeout is expressed in milliseconds. The code above sets maxRetries to 2 (the default, stated explicitly) and the timeout to 120 seconds. That value is a starting point for single-page invoices, not a recommendation for every workload. Long scanned batches need a longer timeout and a queue deadline that accounts for it. These are the defaults the SDK documentation states; they can change between releases, so confirm them in the version you pin.

Rank #2
Sale
Brother DS-640 Compact Mobile Document Scanner, (Model: DS640)
  • 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)

The SDK retries requests. It does not know whether your worker already wrote a payable, and it does not retry your database or accounting calls.

The idempotencyKey option

The openai-node request options source exposes an idempotencyKey option, described there as a unique key for the request. It is useful for a single request. It is not an exactly-once mechanism for a pipeline, because it cannot cover the model call, your local transaction and an external accounting write as one unit. What a key protects depends on what the endpoint documents for it, so read the endpoint documentation for your version. Keep the business guarantee in your database, as described below.

Budget retries across layers, once

Three layers can retry the same document. Each one is reasonable on its own, and together they multiply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Layer What it retries Default or setting in the sources Where you control it
SDK Temporary connection errors; HTTP 408, 409, 429 and 500 and above Two retries, so up to three HTTP requests per call; ten-minute request timeout Client options maxRetries and timeout
Worker Job attempts your classifier marks as retryable Not set by the SDK Your job runner’s attempt cap
Queue or orchestrator Redelivery after a crash or an expired visibility timeout Not stated by the SDK Queue configuration

With the SDK defaults and a worker cap of three attempts, one document can reach the API nine times. If each request runs to the ten-minute timeout, one document can hold a worker for 90 minutes before it is marked failed, and queue redelivery can add to that. The worst case needs every request to time out. A rate-limit response that clears after backoff costs far less.

Rank #3
Canon imageFORMULA R40II Office Document Scanner - Duplex Scanning, Easy Setup, Scans a Wide Variety of Documents, Scans to Cloud
  • 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.

Make one layer own the total budget:

  1. Let the SDK absorb short transient errors with a small maxRetries value.
  2. Let the worker own the attempt cap, and count queue redeliveries against that same cap.
  3. Set the request timeout from measured extraction times for your typical page counts in your own environment.
  4. Set the queue visibility timeout longer than one job attempt’s worst case, or extend it while the job runs.

With maxRetries: 2, a worker cap of 3 and a 120-second timeout, one job attempt can spend up to six minutes in requests (three requests at 120 seconds each), and one document can make at most nine requests, or about 18 minutes of request time, before backoff. Those are the numbers to check against your queue deadline.

Give every invoice a stable identity

Use three identifiers, each with a clear job:

  • Job ID: a UUID created once at ingestion and reused by every attempt for that document.
  • Document hash: a SHA-256 of the source file bytes, scoped to the tenant. It catches the same file arriving twice.
  • Business key: tenant, supplier identifier and invoice number. It catches the same invoice arriving in a different file, such as an emailed PDF and a portal download.

Normalise the business-key parts before you compare them: trim whitespace, use one case rule for invoice numbers, and strip punctuation from tax IDs. Record those rules in code, because a mismatch in normalisation is the most common reason two rows for one invoice appear.

Business keys also need a reuse rule. Suppliers do reuse invoice numbers, and corrected invoices sometimes keep the original number. Treat a business-key match with different validated content as a conflict routed to review. Do not overwrite the existing record, and do not skip the new one silently.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Persist job state and attempts before the model call

Write the job row and the attempt row before the API call, not after it. If the process dies mid-call, the attempt row shows an attempt that started and never finished, which is the evidence you need when you investigate.

Rank #4
Sale
ScanSnap iX2500 Wireless or USB High-Speed Document Scanner, Black
  • 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

Use these job states:

  • received: file stored, job ID and document hash assigned.
  • extracting: an attempt row exists and the API call is in flight.
  • extracted: the parsed output is stored exactly as received, so later steps never call the model again for this job.
  • validated: the shape and business checks passed.
  • committed: the payable row exists and references the job ID.
  • review_required: a check failed or a business-key conflict occurred, and a person decides.
  • failed_retryable: transient failures used up this attempt, and the worker reschedules it.
  • failed_terminal: the attempt cap was reached or the error is not retryable, and the job is alerted.
CREATE TABLE extraction_attempts (
  job_id uuid NOT NULL,
  attempt_no integer NOT NULL,
  trigger text NOT NULL,
  started_at timestamptz NOT NULL,
  finished_at timestamptz,
  outcome text,
  error_class text,
  http_status integer,
  client_request_id text NOT NULL,
  openai_request_id text,
  commit_result text,
  PRIMARY KEY (job_id, attempt_no)
);

The primary key on (job_id, attempt_no) means two workers that try to claim the same attempt number cannot both succeed. The one whose insert fails backs off and rereads the job.

Validate before anything is committed

Shape checks

  • The response status is completed and output_parsed is present.
  • Every required field exists in the parsed object, with nulls only where the schema allows them.
  • The raw parsed output is stored in the job’s extracted state before any further step runs.

Business checks

These run in your code against the parsed values and your own rules. The SDK does not perform them.

Check Rule to implement On failure
Supplier identity supplier_tax_id or a vendor-master match is present and maps to exactly one supplier record review_required
Invoice number Non-empty after trimming review_required
Dates ISO 8601 date that parses, inside the window your policy allows review_required
Currency ISO 4217 code that matches the supplier’s contract currency or is explicitly allowed for that supplier review_required
Line arithmetic quantity multiplied by unit_price equals line_total at the currency’s minor unit, after your rounding rule review_required, with the line index recorded
Subtotal Sum of line totals equals subtotal when both are present review_required
Total subtotal plus tax_total equals total; a missing subtotal or tax value triggers review rather than an inferred value review_required
Duplicate business key Business key exists with a different content hash review_required, as a conflict

If your invoices carry discounts, shipping or rounding lines, add those terms to the total equation before you use it. Otherwise every such invoice lands in review, which looks like a model failure but is a rule gap.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Convert amounts to integer minor units only after these checks pass. A locale-dependent format such as 1.234,50 should go to review unless the supplier’s locale is known. Guessing the decimal separator is the silent failure these checks exist to catch. A syntactically valid object, or a confidence value the model reports about itself, does not replace these checks.

Best Value
Sale
Epson Workforce ES-400 II High-Speed Color Duplex Desktop Document Scanner
  • 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
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Commit idempotently

The write path is where duplicates are actually prevented. The payables table carries a uniqueness rule on the business key, and the commit runs in one transaction.

CREATE TABLE payables (
  payable_id uuid PRIMARY KEY,
  tenant_id text NOT NULL,
  supplier_key text NOT NULL,
  invoice_number text NOT NULL,
  content_hash text NOT NULL,
  source_job_id uuid NOT NULL,
  total_minor bigint NOT NULL,
  currency char(3) NOT NULL,
  UNIQUE (tenant_id, supplier_key, invoice_number)
);

INSERT INTO payables (payable_id, tenant_id, supplier_key, invoice_number, content_hash, source_job_id, total_minor, currency)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (tenant_id, supplier_key, invoice_number) DO NOTHING
RETURNING payable_id;
  1. Start a transaction and lock the job row with SELECT ... FOR UPDATE.
  2. If the job is already committed, return the existing payable ID and stop. Record commit_result = already_committed.
  3. Run the insert above, using the content hash computed from normalised validated fields.
  4. If a row came back, set the job to committed and record commit_result = created.
  5. If no row came back, read the existing payable. A matching content hash means committed with commit_result = already_committed. A different hash means review_required with commit_result = conflict.
  6. Commit the transaction.

A local commit does not cover a separate accounting system. If the payable is posted to another service, record a pending_external state before the call. Use the external system’s reference or idempotency field if its documentation provides one. If it does not, a retry must first query the external system for your payable ID rather than posting again. Confirm what that system’s API documents about references and duplicates before you choose the mechanism.

Classify failures before retrying

Failure Signal Who retries Action
Connection error or timeout Error after the request was sent or never answered SDK first, then the worker with backoff Retry until the attempt cap, then failed_retryable or failed_terminal
Rate limit or server error HTTP 408, 409, 429 or 500 and above SDK first, then the worker Same as above
Other 4xx responses Responses outside the SDK’s retry list, such as 400, 401 or 403 Nothing automatically Stop, alert, and fix the request, configuration or credentials; do not loop
Incomplete or unparsed output Status is not completed, or output_parsed is absent One bounded retry If it repeats, review_required
Schema-valid, business-invalid A check from the table above fails No automatic retry on the same input review_required with the check codes recorded
Commit fails after validation Database timeout after extracted was saved Retry the commit only Reuse the stored output; make no new model call
Replay of a committed job Job is already committed None Return the existing payable and log already_committed

A consistently invalid invoice should not be retried indefinitely. The attempt cap ends the loop, and the source document stays attached to the review item so a person can see what the model was given.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Log every attempt with request IDs

The OpenAI API reference recommends logging request IDs in production for support troubleshooting. Its Backward Compatibility and Request IDs section also describes X-Client-Request-Id as a client-supplied identifier. Set that header to a value that names the job and attempt, such as 4f1c2e9a-attempt-2. Your application log, the attempt row and any support ticket can then be joined on one string. Confirm the length and character limits the API applies to this header before you standardise the format.

Log these fields for each attempt:

  • Job ID, document hash, tenant, attempt number and trigger (initial, redelivery or manual replay).
  • Start time, end time and elapsed milliseconds.
  • Outcome, error class and HTTP status when one exists.
  • The request ID the API returned and the X-Client-Request-Id you sent.
  • Validation check codes that failed. Log the code and the field name, not the value.
  • Commit result: created, already_committed, conflict or not_reached.

Do not log invoice text, bank details or full tax IDs. Where support needs a reference, log the document hash or the last four characters of an identifier. Restrict access to the attempt table the same way you restrict the payables table.

SDK-level retries happen inside one call, so the attempt row records the final outcome and elapsed time rather than each internal request. If you need per-request records, instrument the client and check what the installed version exposes.

Quick Recap

Bestseller No. 3
Canon imageFORMULA R40II Office Document Scanner - Duplex Scanning, Easy Setup, Scans a Wide Variety of Documents, Scans to Cloud
Canon imageFORMULA R40II Office Document Scanner - Duplex Scanning, Easy Setup, Scans a Wide Variety of Documents, Scans to Cloud
Easy Setup: Simply connect to your computer using the supplied USB-C cable.; Bundled Software: Includes easy-to-use Canon CaptureOnTouch scanning software.
$247.00

Troubleshooting branches

  • Two payables exist for one invoice: confirm the unique constraint is in the deployed migration, then compare the normalised business-key parts of the two rows. A casing or whitespace difference is the usual cause.
  • An attempt row has no request ID: the call failed before an HTTP response arrived. Compare elapsed with your configured timeout and read error_class.
  • Every redelivery shows already_committed: expected for replays. There should be exactly one created result per payable.
  • Jobs stuck in extracting: the worker stopped mid-call. A sweeper should move jobs with no finish time past the visibility timeout to failed_retryable.
  • Committed rows with no matching attempt: a write path bypassed the attempt table. Route every commit through the transaction described above.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.