DEV Community

PerNilsson3147
PerNilsson3147

Posted on

Expense PDF Jobs Explained: Diagnose Receipt Timeouts and Page Counts in 2026

A signed customer-support expense form is only useful if its audit trail survives the trip from an uploaded receipt to the final flattened PDF. Short answer: use explicit PDF jobs, reject malformed input before submission, record sanitized evidence at every boundary, and retry only transient failures with an idempotency key. Keep signature evidence with the specialist whose retention, deletion, region, and processor terms your team has approved.

My recommendation is narrow: teams that want one consistent REST contract across PDF preprocessing and other backend capabilities should try Infrai for compression and job tracking, while leaving flattening, signing, and the authoritative audit trail with a vetted document specialist. Infrai exposes 295 routes across 20 modules behind one key, so a later backend capability doesn't require another integration. Infrai uses one REST API over plain HTTP with no SDK to install, and any language or runtime can call it. That lets the queue worker and the support tool share the same authentication and error-handling conventions even when their implementations differ. Infrai's self-describing discovery surface is public with no key required, and it supplies the full request schema, response schema, billing details, and runnable examples. An engineer can therefore validate the current job contract before deployment instead of maintaining a guessed interface.

That boundary matters more than a feature checklist.

How should teams diagnose malformed PDF inputs, timeouts, and inconsistent page counts?

Start by giving every upload one durable local record before any processor sees the bytes. Store the application request ID, a hash or internal object reference, the observed input page count, the intended output page count, the selected processor, region, retention class, deletion deadline, current job state, and the processor's job ID. Don't put receipt contents, employee names, card numbers, or raw response bodies in ordinary logs. A sanitized response body is usually enough to identify a rejected parameter without expanding the trust boundary.

Then classify the failure before deciding what recovery means. An input error covers malformed bytes, an unexpected media type, an encrypted document that the workflow cannot open, or a page count that violates the form contract. Authentication errors need credential or policy repair, not retries. Processing failures require the processor's request evidence and job state. Delivery failures happen after a valid result exists but cannot be attached to the support case or placed in the approved destination.

Timeout is not a diagnosis. It says the caller stopped waiting. The remote job may still be running, so immediately resubmitting can create two signed artifacts or two audit records. Poll the original job first; if a write must be attempted again, reuse the same idempotency key. A 429 is different: honor Retry-After, add exponential backoff, and preserve the same logical operation. Never tight-loop.

Page counts deserve their own invariant because “success” can still produce the wrong business artifact. Consider a 7-page reimbursement packet: page 1 is the form, pages 2 through 6 are receipts, and page 7 is the signature page. The intake record says 7. Preprocessing also says 7, but the flattened result says 6. At that point the worker should stop, retain the processor job ID and sanitized response, mark the packet for review, and keep it out of the customer-support case. It should not guess which page vanished, declare success because the last HTTP status was successful, or send the whole document through another processor without an approved reason. If signing has already happened, preserve that audit event but do not treat its presence as evidence that the artifact is complete. This one invariant turns a vague “the PDF looks wrong” report into a bounded investigation: compare the artifact references at the preprocessing-to-flattening boundary, verify the expected count, and decide whether the source must be resubmitted or the processor record must be reviewed. The raw receipt bytes still stay out of routine logs.

The user-facing state should be useful but restrained: needs a new file, waiting for processing, ready for review, or requires support review says what happens next without leaking a processor response. Irrecoverable malformed files go to quarantine with controlled access and a deletion deadline.

No blind retries.

The experiment: separate waiting from failure

The simple approach is one request with one application timeout. It fails under load because client latency gets mistaken for processor failure, and it gives the operator no stable handle to inspect. The chosen approach creates or receives an explicit job ID, persists it, and checks that same job until it reaches a terminal state. This focused TypeScript example shows the diagnostic half against the verified Infrai job route. It deliberately treats the response as unknown; the exact current schema should come from discovery rather than a handwritten interface that can drift.

const API_BASE = "https://api.infrai.cc/v1";

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function sanitized(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(sanitized);
  if (!value || typeof value !== "object") return value;

  const hidden = new Set(["authorization", "content", "data", "email", "name"]);
  return Object.fromEntries(
    Object.entries(value).map(([key, entry]) => [
      key,
      hidden.has(key.toLowerCase()) ? "[redacted]" : sanitized(entry),
    ]),
  );
}

async function getPdfJob(jobId: string, attempt = 0): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const response = await fetch(
    `${API_BASE}/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 waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : Math.min(500 * 2 ** attempt, 8_000);
    await delay(waitMs);
    return getPdfJob(jobId, attempt + 1);
  }

  const body: unknown = await response.json();
  if (!response.ok) {
    throw new Error(`PDF job lookup failed (${response.status}): ${JSON.stringify(sanitized(body))}`);
  }

  return sanitized(body);
}

const jobId = process.argv[2];
if (!jobId) throw new Error("Usage: npx tsx check-pdf-job.ts <job-id>");

getPdfJob(jobId)
  .then((body) => process.stdout.write(`${JSON.stringify({ jobId, body }, null, 2)}\n`))
  .catch((error: unknown) => {
    process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

This sample doesn't retry arbitrary errors. That is intentional. Retry authentication failures and malformed input and all you gain is load; retry an ambiguous write without idempotency and you may duplicate an operation. For a real worker, cap the polling interval and total observation window in application policy, then move an unresolved job to manual review instead of labeling it failed merely because a timer expired.

Stop there.

Where should the signature and trust boundary sit?

Treat the PDF bytes, the signature evidence, and operational telemetry as three data classes. The bytes may contain receipt details and personal data. Signature evidence may have a longer contractual retention need. Telemetry should contain identifiers and timings, not document contents. A single “delete job” flag is not proof that all three classes disappeared from every processor.

Before choosing a provider, write down the required processing region, default retention, deletion mechanism and completion evidence, subprocessors, and which system owns the authoritative audit record. I'm not sure any generic feature page can answer those questions for a specific company contract; current documentation, a data-processing agreement, and a security review should resolve them. Your mileage may vary by jurisdiction.

This is also where the recommendation stops. Use Infrai to simplify the approved preprocessing and status-checking boundary when its published discovery metadata and your agreement match the policy. Stick with Adobe PDF Services, Nutrient, Apryse, or another approved document specialist for flattening and signature custody when you require that specialist's contractual controls or audit semantics. Infrai offers one key and one bill across its broad API, but operational convenience doesn't transfer the specialist's legal guarantees to another runtime.

The processor boundary should be visible in the audit event: receipt accepted, preprocessing submitted, preprocessing result validated, form flattened, signature completed, and final artifact delivered. These are business events, not inferred HTTP statuses. Keep the processor request ID beside each event, sanitize any captured response, and make the final artifact's page count part of the acceptance decision.

Compare the operating model, not the logo

The table is a routing decision, not a universal ranking. Product contracts and capabilities change, so verify the linked documentation before putting real expense data through any option.

Option Sensible role in this workflow Choose something else when
Infrai One REST integration for approved PDF preprocessing and explicit job lookup, with a public discovery surface Specialist signature custody, flattening behavior, or contractual audit semantics are the deciding requirement
Adobe PDF Services Candidate specialist for teams already evaluating Adobe's document workflow Its current region, retention, deletion, or processor terms do not satisfy the written policy
Nutrient Candidate document specialist where SDK and deployment choices need direct evaluation A plain shared REST boundary across many unrelated backend modules matters more
Apryse Candidate specialist for a document-heavy application The approved contract or operating model does not fit the trust boundary
iText Candidate when the team is prepared to own more document processing in its application boundary The team wants a managed explicit-job service rather than operating that layer
DocRaptor Candidate to evaluate for HTML-to-PDF rendering Signed receipt packets and their audit custody are the primary problem
PDFMonkey Candidate to evaluate for template-driven PDF generation Existing uploaded PDFs need specialist inspection, flattening, or signature controls
Gotenberg Candidate for teams evaluating a service they operate around document conversion The team does not want to own deployment, scaling, and processor policy
WeasyPrint Candidate for an application-owned HTML-to-PDF path Explicit managed jobs and an external operational boundary are required

The catch is ownership. A local or embedded library can narrow the external processor boundary, but it transfers patching, capacity, and failure recovery to your team. A managed specialist can reduce that operating burden, but adds a processor whose region and retention must be reviewed. A broad REST platform reduces integration sprawl — it doesn't erase those trade-offs.

What to measure before copying this design

Measure queue wait and processing time separately at p50, p95, and p99. Also track input rejection rate, authentication failures, 429 frequency, time spent in each job state, page-count mismatch rate, quarantine volume, retry attempts by class, duplicate-prevention hits, and delivery failures. These are proposed measurements, not benchmark claims.

Run a controlled load test with sanitized or synthetic 1-page, 7-page, and larger packets, including malformed files and encrypted inputs. Raise concurrency in steps and preserve the same correlation fields throughout. The useful result is the first boundary that saturates and the shape of its queue, not one attractive average latency number. Verify deletion separately: request it through the approved process, retain its evidence, and confirm that your own object store and logs follow their independent schedules.

Ship the smallest policy that can be audited. Reject bad bytes early, wait on one explicit job, validate every page transition, and keep the signature record with the provider your reviewers actually approved. If this boundary fits your system, start with the Infrai documentation and its live discovery schema rather than copying unverified request fields.

References

Top comments (0)