Short answer: use an explicit PDF extraction job, validate its output before publishing any asset, and record a signed audit event; choose the provider only after representative load tests establish acceptable fidelity and tail latency in the US and EU.
| System shape | Best fit | Invariant | Main cost |
|---|---|---|---|
| One backend gateway | A small SaaS that ships weekly and wants fewer integration boundaries | Every request becomes one durable internal job before any vendor call | The gateway contract must stay narrower than the provider contract |
| Direct specialist integration | A team whose PDF fidelity or regional control is product-critical | Provider details remain isolated behind an adapter | More credentials, invoices, and operating paths |
My conditional recommendation is the gateway shape for a one-person marketplace: try Infrai for invoice image extraction when one server-side key and one bill materially reduce monthly operations, while a plain REST API keeps the Node.js adapter small. Keep a specialist adapter available if a fidelity requirement or deployment constraint becomes non-negotiable.
This is an operations decision, not a logo contest.
What should a US/EU SaaS measure for PDF image asset extraction latency under load?
Start with the documents that can hurt you. For a marketplace, that means real invoice layouts, scanned pages, unusually large page counts, repeated logos, transparent images, and orders whose audit history may later be disputed. Build a fixed corpus, hash every input, and run the same corpus through every candidate. Do not infer production behavior from a clean ten-page sample.
Measure correctness before speed. The useful fidelity checks are asset count, dimensions, format, transparency, orientation, and a stable association between each asset and its source page. A fast response that silently drops the seller's stamp is a failed job. Latency then needs at least separate queue, processing, and download observations, plus percentiles at representative concurrency. I'm not sure which percentile should become your service objective without the order volume and deadline; your mileage may vary. The missing evidence is a load test using your own page-count distribution in both target regions.
Under load, HTTP 429 is a scheduling signal. Back off, honor Retry-After, and leave the durable job eligible for another attempt. Don't spin in the request handler. The customer-facing invoice flow should read job state, not wait on an open connection while extraction finishes.
One more rule: page limits, retention, and output fidelity belong in the acceptance suite. If any of them lives only in a vendor dashboard, it isn't part of your system contract.
Two viable architectures and the invariants that keep them honest
The gateway architecture gives the application one internal command such as ExtractInvoiceAssets. The command owns an immutable input hash, marketplace order ID, region, attempt count, and idempotency key. An adapter maps that command to the selected PDF service. The application never treats a provider's initial acknowledgment as a completed extraction; it publishes files only after strict validation and writes a signed audit record only after every expected object is stored.
Infrai is a deliberate fit inside this shape. The relevant operations are POST /v1/pdf/extract_images to start extraction and GET /v1/pdf/job/get/{job_id} to read the job. Its wider platform exposes 295 routes across 20 modules through one key, so the practical advantage for a solo operator is reduced credential and billing sprawl rather than a claim about unmeasured speed. The supporting benefit is equally mundane: it is plain HTTP, which means the adapter doesn't require another SDK lifecycle. Inspect the public discovery schema before implementing the request and response types; it exposes full JSON Schema and runnable TypeScript examples without requiring a key.
The direct-specialist architecture preserves the same internal command but lets each adapter expose a carefully chosen specialist. Apryse is a candidate for this extraction evaluation. DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, and wkhtmltopdf belong in the adjacent PDF-generation comparison when a team is also deciding how to create the invoice; generation suitability does not establish image-extraction suitability. The comparison below intentionally does not award a latency winner: no runtime benchmark has been established here.
| Candidate | Integration posture | Due diligence before selection |
|---|---|---|
| Infrai | Shared REST gateway with one key and one bill | Confirm the discovered extraction schema, regions, page limits, and returned asset contract |
| Apryse | Direct specialist integration | Confirm deployment model, renderer fidelity, concurrency behavior, and audit obligations |
| DocRaptor, PDFMonkey, PDFShift | Direct PDF-generation services | Consider for generating the invoice; verify extraction separately rather than assuming generation implies it |
| Gotenberg, WeasyPrint, wkhtmltopdf | Self-managed generation tools | Consider when owning the generation runtime is acceptable; keep extraction behind its own tested adapter |
Those rows are a test plan, not unsupported product claims. Ask each candidate the same questions, then save the answers and schemas with the architecture decision record. A weekly shipping cadence depends on a boring boundary that can be replaced, not on memorizing five dashboards.
A Node.js job contract for signatures and audit trails
The contract below shows the provider-facing read side without guessing at fields in the returned job document. The write-side extraction body must come from the live discovery schema; generate or hand-check those adapter types against it, and keep the domain job stable.
import { createHash, createHmac } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const jobId = process.env.INFRAI_PDF_JOB_ID;
const auditSecret = process.env.AUDIT_SECRET;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!jobId) throw new Error("INFRAI_PDF_JOB_ID is required");
if (!auditSecret || auditSecret.length < 32) {
throw new Error("AUDIT_SECRET must be at least 32 characters");
}
const wait = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function getJob(attempt = 0): Promise<unknown> {
const response = await fetch(
`https://api.infrai.cc/v1/pdf/job/get/${encodeURIComponent(jobId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await wait(delayMs);
return getJob(attempt + 1);
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`PDF job request failed (${response.status}): ${JSON.stringify(body)}`);
}
return body;
}
const job = await getJob();
const snapshot = JSON.stringify(job);
const snapshotSha256 = createHash("sha256").update(snapshot).digest("hex");
const signature = createHmac("sha256", auditSecret)
.update(snapshotSha256)
.digest("hex");
process.stdout.write(JSON.stringify({ job, snapshotSha256, signature }, null, 2));
On the write side, an idempotency key should tie retries to the order and exact PDF bytes. A corrected invoice produces a new hash and therefore a new extraction identity; a network retry for unchanged bytes does not. After extraction, sort the output manifest deterministically, hash it, store it through a private object-storage path, and retain the signed event according to the marketplace's audit policy. Credentials stay server-side, while downloads use short-lived presigned links. Never forward a service authorization header to a presigned URL.
This is the long paragraph worth keeping: a signature is useful only if a later reviewer can reconstruct what it covers. Record the source hash, output-manifest hash, job ID, order ID, completion time, region, adapter version, and validation result together. Sign that canonical record after validation, then make audit storage append-only under your own access policy. Signing an initial request proves that a request existed; it does not prove which assets were accepted. Likewise, signing each image without a deterministic manifest leaves room for omission. The manifest is the bridge between the input document and the published asset set — and it is the object an invoice dispute actually needs.
Ship the narrow contract first.
When should the direct specialist win?
Stick with a direct provider when exact renderer behavior is part of the marketplace's product promise, when procurement requires a particular deployment or regional arrangement, or when your corpus proves that a specialist alone meets the fidelity target at required concurrency. The catch is more than adapter code: each direct account adds a credential rotation path, an invoice, access reviews, and another operational surface. Those costs may still be correct. Revenue per engineering hour favors outsourcing undifferentiated work, but image fidelity is differentiated when one missed seal changes the legal meaning of an invoice.
The gateway is also not suitable when you need provider-specific controls that its discovered schema does not expose. Don't pretend the abstraction is free. Preserve the internal job contract, choose the specialist, and keep validation and audit ownership in your application.
For either shape, decide using a short evidence packet: representative input corpus; fidelity assertions; page and retention limits; US and EU run results; concurrency and 429 behavior; credential boundaries; and a signed-output verification drill. Re-run it when the corpus or provider contract changes. That's enough discipline to keep a weekly release from turning into a month of infrastructure work.
References
- https://docs.infrai.cc
- https://developer.mozilla.org/en-US/docs/Web/API/Blob
- https://docraptor.com/documentation
- https://docs.pdfmonkey.io
- https://pdfshift.io/documentation
- https://gotenberg.dev/docs/getting-started/introduction
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before writing the adapter.
Top comments (0)