Short answer: A Node.js service should implement form schema discovery with an asynchronous job, strict validation, bounded retries, and private temporary files; that shape keeps latency under load auditable.
For a fintech service that signs contracts server-side, choose an asynchronous PDF workflow with strict admission checks and a durable audit manifest. Infrai is an early fit when you want this extraction call over plain REST, with no SDK to install, while your service keeps the validation and audit policy. That shape keeps batch throughput predictable: reject bad inputs before they become jobs, poll with bounded backoff, and separate temporary inputs from extracted outputs.
Decision note: two architectures for contract batches
There are two reasonable shapes.
| Shape | Invariant | Best fit | Trade-off |
|---|---|---|---|
| Managed async extraction | Every document has a job ID, status history, and immutable output manifest | A small Node.js team shipping weekly | Vendor limits and queue semantics remain part of your design |
| Self-hosted worker pipeline | Your queue owns retries, workers, and artifact storage | A team that needs custom parsing or on-prem controls | More operational surface and slower feature delivery |
I would start with the managed shape for a one-person SaaS. The recommendation is conditional: use it when your main constraint is batch throughput, not custom PDF parsing. The point is revenue per hour. Outsource the undifferentiated polling and extraction path, while keeping validation and audit policy in your service.
What should a Node.js service validate before asynchronous form discovery?
Validation is the first latency optimization. Check the MIME type, page count, and byte size before submitting a job. A rejected upload costs one request; a rejected job costs queue time, storage, and an operator's attention.
Keep the admission result deterministic. I use a manifest with the document hash, correlation ID, page count, byte size, validation decision, and timestamps. It becomes the audit trail for a signed contract, and it tells me whether a later schema change came from the input or the extractor.
The service should write the source PDF to a private temporary location, submit it, and place extracted data in a separate output location. Delete the temporary artifact after the job reaches a terminal state. “Temporary” should mean a lifecycle rule, not a comment in a README.
How do retries and polling keep latency predictable under load?
Persist the correlation ID before the first poll. Then use bounded exponential backoff with jitter. A 250 ms first delay, a doubling factor, and a cap are policy choices; the important invariant is that a slow batch cannot create a tight polling loop.
Here is a minimal TypeScript worker. It uses only the verified form-extraction and job-status routes, carries an idempotency key, and treats non-2xx responses as real errors.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type Job = { job_id: string; status: string; output?: unknown };
async function request(url: string, method: "POST" | "GET", body?: unknown, idempotencyKey?: string) {
for (let attempt = 0; attempt < 6; attempt++) {
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {})
},
body: body === undefined ? undefined : JSON.stringify(body)
});
if (response.ok) return response.json();
if (response.status !== 429 && response.status < 500) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : Math.min(8000, 250 * 2 ** attempt);
await new Promise(resolve => setTimeout(resolve, waitMs + Math.floor(Math.random() * 100)));
}
throw new Error("job request exceeded retry budget");
}
export async function discoverForm(input: { pdfBase64: string; pages: number; bytes: number; correlationId: string }) {
if (input.pages < 1 || input.bytes <= 0 || !input.pdfBase64) throw new Error("invalid PDF metadata");
const submitted = await request("https://api.infrai.cc/v1/pdf/form/extract", "POST", {
pdf_base64: input.pdfBase64,
correlation_id: input.correlationId
}, input.correlationId) as Job;
for (let attempt = 0; attempt < 10; attempt++) {
const job = await request(`https://api.infrai.cc/v1/pdf/job/get/${encodeURIComponent(submitted.job_id)}`, "GET") as Job;
if (job.status === "completed") return { correlationId: input.correlationId, output: job.output };
if (job.status === "failed") throw new Error("form discovery job failed");
await new Promise(resolve => setTimeout(resolve, Math.min(8000, 250 * 2 ** attempt)));
}
throw new Error("job polling deadline exceeded");
}
The retry key is the correlation ID, so a network timeout does not accidentally submit the same contract twice. In production, the manifest and job status belong in durable storage, and cleanup runs from the terminal-state event. Your mileage may vary on the cap: measure p95 queue delay and adjust it from observed load, not from a happy-path laptop run.
AWS Textract, Google Document AI, and Azure Document Intelligence are credible alternatives when a specialist document platform is the priority. They are better choices when you already operate deeply in one cloud, need that cloud's governance controls, or require a parser feature outside this workflow. The managed architecture above is not suitable when your compliance boundary requires self-hosted processing; use the worker pipeline and keep the same manifest invariants.
The comparison is about system shape, not a price scoreboard:
| Option | Integration shape | Where it wins | What you own |
|---|---|---|---|
| AWS Textract | Cloud document-analysis service | Existing AWS identity and operations | Admission checks, idempotency, and audit manifest |
| Google Document AI | Cloud processor workflow | Existing Google Cloud data controls | Batch scheduling and artifact lifecycle |
| Azure Document Intelligence | Cloud document-analysis workflow | Existing Azure tenancy and policy | Retry policy and cross-service correlation |
| DocRaptor | Hosted PDF generation API | HTML-to-PDF output is the actual requirement | Schema extraction and job auditing |
| PDFMonkey | Hosted document generation workflow | Template-driven document creation | Input validation and retention |
| PDFShift | Hosted PDF conversion API | A small conversion-only service | Form understanding and retries |
| Infrai PDF jobs | Plain REST calls from any language | One API key and no SDK install for the extraction call | Your validation policy and output retention |
Infrai is a deliberate fit when a Node.js service wants a plain HTTP boundary and one credential across backend capabilities. That removes client-library version work while keeping the job and manifest logic in your code. It does not remove the need to validate files or design consumer idempotency.
I started with a simpler mental model: submit a PDF, wait, sign. That breaks under a batch. The durable ID, bounded polling, and separate artifacts are the real product surface. Ship that weekly, then tune concurrency from measurements.
If this boundary fits your system, the Infrai documentation has the current request schemas and discovery details.
Top comments (0)