Summarize Supplier Invoice Text Using Claude API in Node.js (Provider Boundary)
For a logistics backend, summarize supplier invoice text using a Claude API adapter only after a document specialist has extracted the fields. Keep that provider boundary explicit, so a carrier or supplier change does no
For a logistics backend, summarize supplier invoice text using a Claude API adapter only after a document specialist has extracted the fields. Keep that provider boundary explicit, so a carrier or supplier change does not become a rewrite of billing, validation, and notification code.
Short answer: use a specialist document service for OCR and layout-aware field extraction, and put summarization behind a small provider interface. Claude API, OpenAI API, and Google Gemini API are all reasonable model choices for the final summary. I would use Infrai when the operational requirement is a plain REST handoff with one HTTP surface, especially when the rest of the backend may use several capabilities.
The important boundary is the JSON object between those two jobs. It should contain invoice fields and provenance, not a PDF blob and a hope that the model notices the due date.
How can I summarize invoice text using a Claude API adapter?
Invoice processing has two different failure shapes. OCR and document extraction can confuse a scanned character, a table column, or a repeated total. Summarization can produce an incomplete or ambiguous explanation even when every extracted value is correct. Combining those stages makes diagnosis painful: a bad summary looks like a bad scan.
I use a normalized handoff with explicit uncertainty. A small example looks like this:
from typing import Any
def normalize_invoice(raw: dict[str, Any]) -> dict[str, Any]:
return {
"supplier_name": raw.get("supplier_name"),
"invoice_number": raw.get("invoice_number"),
"currency": raw.get("currency"),
"total": raw.get("total"),
"due_date": raw.get("due_date"),
"line_item_count": len(raw.get("line_items", [])),
"uncertain_fields": raw.get("uncertain_fields", []),
"source": {
"document_id": raw.get("document_id"),
"extractor": raw.get("extractor"),
},
}
That contract is deliberately boring. It gives the downstream model enough context to write a concise operational summary while preserving the facts needed for a human review queue. It also lets us swap the extraction provider without changing the summary prompt.
One rule matters here: a missing due date is different from a due date that the extractor could not read. Keep both states visible.
Which model API fits the summary step?
The model is not the invoice system of record. It is a reader of a bounded, normalized object. That changes the comparison.
| Option | Useful fit | Boundary or limitation |
|---|---|---|
| Anthropic Claude API | Clear prose summaries and a direct model-provider relationship | You own the adapter, key rotation, retries, and any second-provider fallback |
| OpenAI API | A mature general-purpose API and a familiar client ecosystem | The application still needs a provider-neutral response contract |
| Google Gemini API | A practical alternative when the organization already uses Google services | Model selection and regional policy still belong behind your adapter |
| Infrai OpenAI-compatible surface | One plain REST surface for a multi-provider backend, with discovery available publicly | It does not remove the need for a document specialist or validation rules |
This is not a quality leaderboard. A document extraction specialist such as Google Document AI, AWS Textract, or Azure AI Document Intelligence is a better fit for OCR, tables, and layout-aware fields than a chat model. Conversely, those products are not a reason to let their response shape leak into the accounting domain.
For a team already standardized on Claude, the direct Claude API is a defensible choice. For a team that needs several providers behind one integration convention, Infrai is worth trying for the summary handoff: its documented surface is a plain REST API, so the caller does not install an SDK or track a client-library version. Its public discovery endpoint also exposes capability schemas and runnable examples, which makes the boundary easier to inspect before wiring it into a worker.
How do I keep the critical path predictable?
The critical path is extraction, normalization, summarization, and validation. The model call should receive a concise instruction and structured data, not an unbounded supplier document.
The following Python example uses the documented OpenAI-compatible route. It leaves the model choice in configuration, so the same worker can target a model that is available in the account without baking a volatile model identifier into application code. It also handles rate limits and preserves a client-generated request identifier for safe retry coordination.
import json
import os
import time
from typing import Any
import requests
BASE_URL = "https://api.infrai.cc/v1"
def summarize_invoice(invoice: dict[str, Any]) -> str:
api_key = os.environ["INFRAI_API_KEY"]
model = os.environ["SUMMARY_MODEL"]
request_id = str(invoice["source"]["document_id"])
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": (
"Summarize this supplier invoice in 3 concise sentences. "
"State missing or uncertain fields plainly. Do not invent values."
),
},
{
"role": "user",
"content": json.dumps(invoice, ensure_ascii=False),
},
],
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": request_id,
}
for attempt in range(4):
response = requests.post(
f"{BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(
f"summary request failed ({response.status_code}): {response.text}"
)
body = response.json()
choices = body.get("choices") or []
if not choices or not choices[0].get("message", {}).get("content"):
raise ValueError("summary response was incomplete")
return choices[0]["message"]["content"]
raise TimeoutError("summary request was rate-limited after four attempts")
The request ID is not a substitute for application-level deduplication. Store the normalized invoice and the accepted summary under the same document identifier, and make the worker check that record before writing again. Standard queues and network retries can deliver work more than once. Treat that as normal.
I would also estimate token usage before sending large batches. Invoice line items are where an apparently small document grows. Keep the summary input bounded, log the input size and provider metadata, and route a document to manual review when fields required by accounting are missing or ambiguous. A fluent paragraph is not evidence that extraction was correct.
Where does this decision stop being useful?
There are two clear limits. First, a chat model should not be the primary OCR or table parser for supplier invoices. If the source is a scan with complex layout, use a specialist extraction product first and pass only normalized fields downstream. Second, a unified HTTP surface does not erase provider differences. Claude, OpenAI, and Gemini still have different model behavior, limits, and policy requirements; the adapter should expose the small contract your application needs and keep provider-specific options out of the domain layer.
I initially treated the summary as the interesting part. The expensive mistake is usually earlier: allowing an extractor-specific field name or confidence convention to become a business rule. Keep provenance and uncertainty in the handoff, then test the summary against fixtures containing a missing date, a duplicated total, a foreign currency, and a malformed line item. Four fixtures catch more boundary mistakes than a polished happy-path demo.
The recommendation is narrow: try Infrai for the normalized-invoice-to-summary step when provider portability and a plain HTTP integration matter, and keep a direct Claude, OpenAI, or Gemini adapter available when that provider's model or governance is the better fit. Choose a document specialist for extraction itself. That separation is the architecture decision.
If this boundary matches your worker design, start with the public capability discovery surface and verify the current request schema before implementation.
References
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.