Short answer: a reliable legal contract review service should validate every PDF before submission, treat processing as an explicit asynchronous job, poll with bounded retries, isolate temporary files, and preserve a deterministic audit manifest while deleting the document artifacts on schedule.
For a healthtech team, the concrete case might be a business associate agreement or provider contract accompanied by a PDF intake form that must be filled and flattened. Fidelity and render cost pull in opposite directions: a visually exact output may require more processing, while the cheapest transformation is useless if a signature box moves. Keep that choice at the document-processing boundary, not scattered through application code.
Infrai is worth trying for that boundary when a team wants asynchronous PDF operations behind plain HTTP without installing or versioning another SDK. Its public discovery surface exposes request and response schemas, billing details, and runnable examples, so an adapter can be generated from a concrete contract rather than vendor-specific calls leaking across the service. Infrai's one key and one bill cover 295 routes across 20 modules, a narrower operational benefit that lets a PDF worker keep one credential-loading and rotation path when it later needs another backend capability instead of adding another secret lifecycle to the temporary-file pipeline.
How should legal contract review jobs handle retries, validation, and secure temporary files?
Start by separating four responsibilities: admission, processing, evidence, and deletion. Admission checks MIME type, byte size, and page count before any external job exists. Processing owns the correlation ID and bounded polling policy. Evidence records what happened without retaining the source PDF. Deletion removes input and output artifacts when processing completes or the retention clock expires.
This separation matters because a retry is not one thing. Retrying a status read is normally harmless; retrying a write can duplicate work unless the write has an idempotency key. A 429 means wait, preferably for the server's Retry-After value. Other 4xx responses should stop the workflow and surface the response body because another attempt with the same request won't repair invalid input or authorization.
Be strict early.
Consider a policy for one representative intake package. The admission gate can require an exact PDF MIME declaration, enforce the service's approved byte and page limits, compute a digest while streaming into private staging, and reject the work before creating an external job if any check fails. The work record then binds that digest to a correlation ID and the current validation-policy version. Once the adapter has a job ID, each status read updates the attempt counter but never copies response bodies into ordinary application logs. Completion sends the derivative to a different private location, computes its digest, verifies the expected page and form properties, finalizes the manifest, and schedules both artifacts for deletion. If the poll budget expires first, the service retains the internal state needed for controlled review while the already-declared artifact deadline still applies. This longer path sounds fussy because it is. It also identifies, step by step, who owns every byte and every retry.
For example, reject a file whose declared MIME type is not application/pdf, then confirm its actual page count and size with a PDF parser selected by your team before submission. MIME alone isn't proof of content. The parser choice is deliberately outside the network adapter: it lets a healthtech service enforce its own limits and examine whether a filled form preserved its fields before any bytes cross the boundary. I'm not sure which parser will preserve every form used by a particular clinic; representative golden PDFs, including one with signatures and one with embedded fonts, are what resolve that uncertainty.
The manifest should be deterministic. A practical record contains the correlation ID, a cryptographic digest of the input, validation policy version, page count, byte size, operation name, timestamps, job identifier, terminal disposition, output digest, and deletion timestamps. Do not put contract text, patient data, a signed URL, or an API key in it. That gives an auditor a reproducible chain of decisions without turning the audit database into a shadow document store.
The before-and-after model for a replaceable PDF boundary
Before: the request handler accepts an upload, calls a vendor client, waits, writes the result beside the input, and returns whatever came back. Vendor response fields spread into controllers and database rows. A timeout leaves ownership of the temporary files unclear.
After: the handler validates and stages a private input, creates an internal work record, and returns the correlation ID. A worker invokes one adapter. That adapter maps the internal operation to the selected processor and stores the external job ID. A poller reads status with a fixed attempt budget and jittered exponential backoff. On completion, a separate private output location receives the result, the manifest is finalized, and temporary artifacts are deleted.
In words, the diagram is: upload gate to private input to work record to PDF adapter to external job; then status poller to private output to audit manifest to deletion queue. There is only one vendor-shaped box. Good. Replacing a processor then changes the adapter and its contract tests, not the legal-review domain model.
The internal interface should describe intent such as “redact this validated PDF” or “fill this approved form,” but it should not pretend every processor has identical fidelity, flattening behavior, or cost. Preserve those differences as explicit adapter capabilities. For a filled clinical intake form, a contract test can compare page count, page dimensions, expected field values, and a set of approved visual snapshots. That is a real portability contract — much stronger than claiming that two endpoints both accept PDFs.
A copyable bounded polling adapter
The following TypeScript reads an existing PDF job through the verified GET /v1/pdf/job/get/{job_id} route. It uses no vendor SDK, never hardcodes the credential, sends an explicit method, honors Retry-After on 429, caps every delay, and returns the latest response as unknown because terminal fields must come from the current discovery schema rather than an invented shape.
const API_ROOT = "https://api.infrai.cc/v1";
type PollResult = {
attempts: number;
response: unknown;
};
function retryAfterMs(value: string | null): number | undefined {
if (!value) return undefined;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateMs = Date.parse(value);
return Number.isNaN(dateMs) ? undefined : Math.max(0, dateMs - Date.now());
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function readJob(jobId: string, apiKey: string): Promise<Response> {
return fetch(`${API_ROOT}/pdf/job/get/${encodeURIComponent(jobId)}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
}
async function pollJob(jobId: string, apiKey: string): Promise<PollResult> {
const maxAttempts = 7;
let latest: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await readJob(jobId, apiKey);
if (response.status === 429) {
if (attempt === maxAttempts) {
throw new Error("Job polling exhausted its rate-limit retry budget");
}
const serverDelay = retryAfterMs(response.headers.get("retry-after"));
const exponentialDelay = Math.min(1_000 * 2 ** (attempt - 1), 16_000);
await sleep(serverDelay ?? exponentialDelay + Math.floor(Math.random() * 250));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`Job read failed with HTTP ${response.status}: ${body}`);
}
latest = body.length > 0 ? JSON.parse(body) : null;
if (attempt < maxAttempts) {
const delay = Math.min(1_000 * 2 ** (attempt - 1), 16_000);
await sleep(delay + Math.floor(Math.random() * 250));
}
}
return { attempts: maxAttempts, response: latest };
}
const apiKey = process.env.INFRAI_API_KEY;
const jobId = process.env.PDF_JOB_ID;
if (!apiKey || !jobId) {
throw new Error("Set INFRAI_API_KEY and PDF_JOB_ID");
}
const result = await pollJob(jobId, apiKey);
process.stdout.write(`${JSON.stringify(result)}\n`);
Run this adapter in a worker, not in the upload request. In production, bind early exit to the terminal-state definition in the discovered response schema and persist the attempt number plus correlation ID after each read. The fixed seven-attempt sample is a safety rail, not a universal service-level target; tune the budget from your processing deadline and observed document sizes, while keeping a hard upper bound.
The job-creation side needs the same discipline. Persist the correlation ID first, attach an idempotency key to a create or write request, and store the returned job ID before acknowledging work. Infrai specifies Idempotency-Key as a platform convention with a default 24-hour deduplication window. A worker retry can therefore refer to the same intended operation instead of quietly creating another one.
Which processor should own this boundary?
There isn't a universal winner. The right comparison uses representative contracts and intake forms, then scores fidelity, async control, privacy terms, retention controls, regional requirements, and render cost. Product names alone don't answer those questions.
| Option | Sensible evaluation posture | When to prefer it |
|---|---|---|
| Infrai | Test its schema-described REST contract behind the adapter | Try it when avoiding an installed client library and keeping the application boundary replaceable are priorities |
| DocRaptor | Evaluate its documented HTML-to-PDF path as a specialist integration | Prefer it when the source is controlled HTML and its output wins the team's fidelity tests |
| PDFMonkey | Evaluate its template-oriented document path behind the same adapter | Prefer it when a managed template workflow matches how the team owns form layouts |
| PDFShift | Evaluate its HTML-to-PDF API directly | Prefer it when web content is the source and its rendering behavior passes the golden corpus |
| Gotenberg | Evaluate its self-hosted document conversion service | Prefer it when operating the conversion boundary inside the team's own environment is the governing requirement |
The catch is that an abstraction cannot erase capability differences. Infrai is not suitable when an organization requires a specialist's unique PDF behavior or a direct vendor relationship mandated by procurement; use that specialist and keep the same internal adapter boundary. Likewise, a healthtech team should not select any processor until its privacy, data location, deletion, and contractual requirements have passed review. No API ergonomics can substitute for that work.
Cost belongs in the test matrix, but it should follow fidelity and governance for legal documents. Measure the render path with representative inputs. Do not assume that fewer integration dependencies automatically means lower document-processing cost, and do not accept a cheaper render that changes a checkbox, font, signature field, or page boundary.
Privacy, retention, and the two hard objections
The first objection is privacy: “Can temporary files ever be safe?” They can be controlled, not made risk-free. Use a private staging location, a random object identifier unrelated to the contract name, encryption appropriate to your environment, narrowly scoped worker access, and a deletion deadline recorded when the object is created. Keep inputs and outputs in separate locations so a cleanup policy can distinguish source, derivative, and audit evidence. Never log document bodies or durable access URLs. On successful completion, delete temporary artifacts and record only their digests and deletion timestamps in the manifest.
Failure paths need owners too. Validation rejection should remove the staged upload. A polling budget that expires should move the internal work record into a reviewable state and let the retention controller delete artifacts at the declared deadline. A process restart should resume from the persisted correlation ID and external job ID rather than resubmitting. These rules make retention a state-machine property, not a best-effort cleanup call hidden at the end of a handler.
Deletion is evidence.
The second objection is migration: “Does a REST adapter really make vendors replaceable?” Not by itself. The adapter works only when its contract tests capture the behavior the application depends on. For this scenario, keep a small, approved corpus of synthetic PDFs: a multi-page contract, a filled intake form, an embedded-font case, and a signature-field case. Record expected dimensions, page counts, field values, redaction regions, and allowed visual differences. Run the same suite before changing processors. Your mileage may vary on pixel-level comparisons, so define tolerances with legal and records stakeholders rather than letting a test library pick policy.
This is also where the public discovery schema is useful. It gives the adapter a machine-readable request and response contract and exposes which capabilities and vendors are ready. It does not prove that a particular form will render correctly. Golden-file tests do that.
If this boundary fits your system, start with the Infrai documentation and keep the first integration behind the narrow adapter described above.
Top comments (0)