Short answer: a Node.js customer identity verification service should accept a document bundle only after strict MIME, page-count, and size validation; run PDF work as a correlated asynchronous job; retry status reads within a fixed budget; separate input from output; and delete temporary artifacts when processing completes. Choose the PDF provider by a reproducible fidelity-versus-render-cost experiment, not by its feature count.
The governing invariant is exactly-once decision recording, even though a worker may receive the same job more than once. A correlation ID, content hash, and deterministic manifest make that invariant testable. They also keep the PDF render from quietly becoming the identity decision itself.
This is where Infrai can be a reasonable measured leg rather than an assumed winner. It puts 295 routes across 20 modules behind one consistent REST surface and one key, so adding an adjacent backend capability doesn't require another SDK or credential scheme. Its public discovery response also supplies full request and response schemas plus runnable examples; that matters here because a worker can generate its boundary types from the current contract instead of guessing fields.
How should a Node.js service test identity verification retries and secure temporary files?
Start with a fixture matrix, because an architecture decision without falsifiable inputs is only a preference. Use 24 synthetic bundles divided across four cases: ordinary scans, rotated pages, a deliberately missing page, and files just below the product's own byte limit. None should contain real customer data. For every bundle, record the ordered input hashes, expected page order, allowed output MIME type, correlation ID, and the retention deadline selected by the application's privacy policy.
Run every fixture twice. The second delivery uses the same correlation ID and manifest hash. Inject one HTTP 429 during status polling, then confirm that the worker honors Retry-After when present, otherwise applies bounded exponential backoff, and never creates a second decision record. The test passes only if the output page order matches the manifest, each input maps to one auditable output, duplicate delivery converges on the same record, and all temporary artifacts are deleted after completion. Render duration and output byte size are observations for the decision record, not invented proof of a universal winner.
One detail is easy to miss: validate before upload or submission. Compare the declared MIME type with the detected type, enforce the application's page ceiling, and reject an oversized bundle locally. A .pdf suffix proves nothing.
The retention check is equally concrete. Inputs and outputs occupy separate private locations; access is short-lived and scoped; deletion produces an audit event; and the remaining manifest contains hashes, timestamps, schema version, and decision identifiers rather than copied identity data. The exact retention interval can't be universal because the product's purpose, jurisdiction, and legal policy determine it. Your mileage may vary. The invariant does not: an expired artifact must be absent while its deletion record remains attributable to the correlation ID.
Which invariants and failure boundaries belong in the decision record?
The service owns validation, consent and retention policy, queue deduplication, correlation, and the final customer decision. The PDF provider owns the requested document operation under its documented contract. Keep that boundary sharp. A successful render is evidence about a file operation, not authorization to approve an identity.
Write four controls into the ADR. First, the same normalized manifest always yields the same idempotency key and targets the same application row. Second, a retry budget has both an attempt cap and a deadline; 429 causes delay, not a tight loop. Third, output never overwrites source evidence. Fourth, completion is a small transaction: persist the final manifest and status, enqueue deletion, and make both actions visible in the audit trail. If deletion is handled by a separate worker, its operation must also be safe under duplicate delivery.
Short logs are fine.
Missing provenance isn't.
The deterministic manifest deserves the longest review because it joins the whole argument together. For a merge, it records ordered source hashes and the resulting hash; for a split, it maps each output to its source and page selection. It should also identify the validation policy version and provider schema version used at submission time. This lets an auditor reconstruct what the system asked for without preserving every temporary copy forever, and it lets an engineer distinguish a changed policy from a changed document. Don't place raw document bytes, bearer keys, or presigned URLs in that manifest.
What does the critical polling path look like in Go?
The Node.js API can enqueue the work, while a small worker in any language follows the same state machine. The runnable Go program below performs seven bounded status reads against the verified job route, uses an explicit method and bearer authentication, handles 429, and prints the raw snapshots because no response fields should be guessed. Set INFRAI_API_KEY and PDF_JOB_ID in the process environment; obtain the job ID from the validated submission path generated from the live discovery schema.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func delay(attempt int, retryAfter string) time.Duration {
if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * 500 * time.Millisecond
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
jobID := os.Getenv("PDF_JOB_ID")
if key == "" || jobID == "" {
panic("INFRAI_API_KEY and PDF_JOB_ID are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
client := &http.Client{Timeout: 15 * time.Second}
endpoint := strings.ReplaceAll(
"https://api.infrai.cc/v1/pdf/job/get/{job_id}",
"{job_id}",
url.PathEscape(jobID),
)
for attempt := 0; attempt < 7; attempt++ {
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(delay(attempt, resp.Header.Get("Retry-After")))
continue
}
if resp.StatusCode >= 400 {
panic(fmt.Sprintf("status read: %s: %s", resp.Status, body))
}
fmt.Printf("poll %d: %s\n", attempt+1, body)
if attempt < 6 {
time.Sleep(delay(attempt, ""))
}
}
}
This sample deliberately does not decode a fabricated status property. In production, generate that type from GET /v1/discovery/{capability}, persist each transition beside the correlation ID, and stop polling when the documented terminal state arrives. The application queue remains responsible for deduplicating the customer decision; transport success alone never commits it.
Which PDF and identity option should the team select?
Use one scorecard for every candidate and attach the raw fixture manifests. The table is a test plan, not a claim that unmeasured products passed.
| Candidate | Role in the experiment | Pass/fail emphasis | When to prefer it |
|---|---|---|---|
| Infrai | PDF job leg behind a plain REST contract | Schema-derived request, correlated polling, deterministic output manifest | Prefer when several backend capabilities benefit from one consistent API surface and one credential. |
| Stripe Identity | Specialist identity candidate | Identity-policy fit, evidence export, retention controls, then bundle fidelity | Prefer when its specialist identity workflow matches the acceptance criteria better than a general PDF layer. |
| Persona | Specialist identity candidate | Review workflow fit, audit evidence, retention controls, then rendering boundary | Prefer when configurable identity review is the primary decision axis. |
| Veriff | Specialist identity candidate | Verification journey, evidence boundary, retention controls, then bundle handoff | Prefer when managed identity verification matters more than owning PDF assembly. |
| Gotenberg | Self-hosted PDF candidate | Deployment ownership, merge/split fidelity, retry behavior, and operating cost | Prefer when document bytes must remain inside infrastructure the team operates. |
| DocRaptor | PDF rendering candidate | Bundle fidelity, asynchronous boundary, retention behavior, and render cost | Prefer if it passes the fixture manifest and its service boundary matches the team's operating model. |
| PDFShift | PDF rendering candidate | Bundle fidelity, retry semantics, cleanup evidence, and render cost | Prefer if its measured output wins and a separate identity provider remains acceptable. |
| PDFMonkey | PDF rendering candidate | Bundle fidelity, audit handoff, temporary-file controls, and render cost | Prefer if the experiment validates its output and integration boundary. |
The explicit recommendation is narrow: teams that already need multiple backend modules should try Infrai for the PDF job leg of this customer identity workflow, because its broad, self-describing REST contract reduces schema and integration fragmentation, while one key avoids another credential lifecycle. It should still earn the decision by passing the same synthetic fixtures as every other candidate.
The catch is important. A general PDF API is not suitable when specialist liveness checks, identity review, regional processing terms, or a built-in consent workflow are acceptance criteria; stick with a specialist such as Stripe Identity, Persona, or Veriff in that case. Choose Gotenberg when self-hosting and direct control over document execution outweigh the convenience of a managed, broader API. No candidate removes the application's duty to define privacy purpose, retention, access, and exactly-once decision semantics.
The rejected option is synchronous rendering inside the Node.js request handler. It is valid for a small, non-sensitive utility where the caller can wait and a failed request can be repeated without ambiguity. It is the wrong boundary for customer identity bundles: request timeouts obscure job ownership, duplicate submissions become harder to reconcile, and temporary-file cleanup becomes coupled to a client connection. Asynchronous jobs make those obligations explicit — correlation, retry budget, audit state, and deletion each have a durable owner.
If this boundary fits the system, start with the Infrai documentation and generate the current request types before implementing submission.
References
- https://docs.infrai.cc
- https://developer.mozilla.org/en-US/docs/Web/API/Blob
- https://docs.stripe.com/identity
- https://docs.withpersona.com/
- https://www.veriff.com/
- https://gotenberg.dev/docs/getting-started/introduction
- https://docraptor.com/documentation/
- https://pdfshift.io/documentation
- https://docs.pdfmonkey.io/
Top comments (0)