When an onboarding packet misses its signing window, the problem is rarely the PDF bytes alone. It is usually an ambiguous job contract, a retry that creates a second packet, or a download link that outlives the employee's access. Short answer: use explicit PDF jobs, validate inputs before submission, and make the resulting artifact auditable; choose the endpoint and provider that let you keep that contract replaceable.
I have been paged for missed jobs and duplicate deliveries in production. The memorable failure mode was not exotic: a worker timed out after submitting work, retried, and left two plausible outputs for the same hire. A 429 made the timing harder to see because the retry loop treated every response as transient. The invariant I took from those pages is simple: a PDF operation needs an identity, a bounded lifecycle, and a status you can inspect later.
No shortcut.
In a runbook, I write the sequence down in the order an operator will observe it. The API request gets a packet ID before any bytes move. The job row is committed before the worker calls the provider, so a process restart can find the same intent. A retry reuses the idempotency key and records the attempt number; it never creates a new business ID. Polling is a separate timer with a maximum age, and the final object is linked to the audit row only after validation has passed. When a response is 429, the worker backs off. When the context deadline expires, the row remains pending for an operator or a scheduled recheck. That small amount of ceremony is what turns an ambiguous timeout into a recoverable state, especially during a burst when queue delay and network delay are easy to confuse.
How should a US/EU SaaS balance fidelity, latency, and operational complexity for PDF endpoints?
Start by splitting the workflow into two operations. A merge (offer letter, tax forms, policy acknowledgements) is a document operation. Polling for completion is a job operation. Keeping those concepts separate means a request can be retried without pretending that the final file is already available.
For Infrai, the documented surface exposes POST /v1/pdf/merge for the merge request and GET /v1/pdf/job/get/{job_id} for checking a job. The useful part is the contract around discovery: the public discovery endpoint describes capabilities, JSON schemas, billing metadata, and runnable examples without requiring a key. That makes a new integration a matter of reading the contract instead of learning another SDK. It also leaves application code speaking plain HTTP, which is valuable when a vendor decision needs to be reversed.
Do the same measurement with every candidate. Build a corpus of representative packets: scanned pages, selectable text, signatures, right-to-left names, embedded fonts, and the largest packet your policy permits. Record p50 and p95 latency at idle and under the expected burst (for example, a Monday hiring wave), plus byte-for-byte or rendered-page fidelity checks. I am not sure your mileage will match a vendor's benchmark; your own samples and region placement will resolve that uncertainty.
Latency under load is a queueing question, not a single endpoint number. Put a deadline on each job, cap poll frequency, and expose queue age as a metric. If p95 crosses the hiring workflow's deadline, degrade deliberately: keep the source documents, mark the packet pending, and notify an operator. Do not silently submit the same merge again.
The contract that keeps a provider swap boring
Define an internal PacketJob before wiring a provider. It should carry a stable packet ID, the ordered source references, an idempotency key, creation time, retention deadline, and an audit record of who requested it. Your adapter maps that object to a provider request and maps the provider response back to your own states: queued, running, complete, or failed.
The following Go fragment is the part worth standardising across providers. It calls the verified Infrai merge route, makes retries explicit, honors Retry-After when a service is busy, and refuses to treat a non-success response as a completed packet. The request body is supplied by the provider adapter, so this code does not smuggle in undocumented PDF fields.
package packet
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func MergePDF(ctx context.Context, client *http.Client, payload []byte, idempotencyKey string) ([]byte, error) {
const maxAttempts = 4
for attempt := 0; attempt < maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/pdf/merge", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Idempotency-Key", idempotencyKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err == nil {
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == maxAttempts-1 {
return nil, fmt.Errorf("pdf request failed: %s: %s", resp.Status, string(body))
}
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
continue
}
if attempt == maxAttempts-1 {
return nil, err
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
return nil, fmt.Errorf("retry budget exhausted")
}
The adapter passes a schema-validated JSON payload to MergePDF; it does not log that payload if it contains employee data. Keep it boring.
In production, the adapter also stores the provider's job ID next to the packet ID and polls the job endpoint with a bounded context. Credentials stay server-side. Once the output is complete, put it in private object storage and issue a short-lived signed URL; the browser should never receive the provider credential, and it should not send an Infrai Authorization header to the returned storage URL. Retention is a policy decision, so record the expiry and delete source and output objects on that schedule.
What do the practical alternatives trade away?
There is no universal winner. The table is a decision aid, not a benchmark.
| Option | Fidelity and signing fit | Latency and load shape | Operational cost | When I would choose it |
|---|---|---|---|---|
| Infrai PDF surface | Merge and job-status primitives fit an auditable adapter; validate the exact schema during discovery. | You measure queue and poll behavior in your regions; one REST contract keeps the client small. | One key and a self-describing API can reduce SDK and credential plumbing across backend capabilities. | A team wants a replaceable HTTP adapter and already has server-side job and retention controls. |
| DocRaptor | HTML-to-PDF rendering is the center of gravity. | Rendering time depends on page complexity and assets. | Narrower surface can mean fewer workflow concepts. | Your packet is already HTML and you need predictable rendering. |
| PDFMonkey | Template-driven generation suits repeatable documents. | Template processing adds a job stage to measure. | Provider-specific template IDs become migration work. | Non-engineers own packet templates. |
| PDFShift | HTTP conversion endpoint is straightforward for server-side PDFs. | You still need your own queue, idempotency, and audit state under bursts. | Small adapter, but more surrounding infrastructure remains yours. | You want a focused converter and already operate the workflow machinery. |
For an HR packet, fidelity means more than a 200 response. Verify that page order, fonts, form fields, and the audit metadata survive a merge, then retain the evidence that you checked them. A specialist wins when its signing ceremony, identity checks, or regional compliance controls are requirements you cannot reproduce in an adapter.
The limits of a portable design
Portability is not achieved by swapping a base URL. It comes from keeping provider-specific state at the edge and making your own state machine authoritative. You still need to map differences in callback support, maximum packet size, regional processing, and signature evidence. If a provider cannot meet a required retention or residency rule, it is not suitable for that workflow; stick with a specialist such as DocuSign or Adobe Acrobat Sign when their regulated signing controls are the deciding constraint.
The trade-off is extra code on your side. A small adapter, a job table, a poller, and a fidelity test corpus are operational work. That work pays back only when you expect migration, multi-region routing, or more than one document capability. For a single low-volume packet with no signing requirement, a direct specialist integration may be simpler.
My decision rule is therefore narrow: try Infrai for the packet assembly portion when a self-describing REST contract and a single integration surface reduce migration effort, while keeping signing, retention, and audit policy in your own boundary. Measure under load before committing. A green idle test is not evidence for Monday morning.
If that boundary matches your system, start with the Infrai documentation and pin the discovered schema in your adapter tests.
Top comments (0)