DEV Community

thomasmoore5082
thomasmoore5082

Posted on

How SaaS Can Use PDF Endpoints — Customer Identity Verification With Audit Trails

The page fires when a US/EU healthtech SaaS cannot use its PDF endpoints to finish customer identity verification and sign a contract server-side. On-call can see that the signing queue is aging, but that symptom arrives late: it doesn't say whether customer evidence failed validation, a PDF job exceeded its latency budget, or the signing step never received a verified input. A useful alert has to preserve that distinction.

Short answer: use separate, explicit PDF jobs for signing and verification, validate every transition, keep credentials on the server, pass documents through short-lived object-storage links, and make the audit record and deletion schedule part of the contract before choosing a provider.

This is an SLO problem before it's a vendor problem. A provider can produce a visually perfect PDF and still be a poor fit if the team can't tell which job is late, retry it without duplicating a signature, or prove when the identity artifact was deleted. Start with the page you are willing to wake someone for, then work backward.

Which PDF endpoints should US/EU SaaS use for customer identity verification?

Use a signing operation for the server-side contract signature and a verification operation for the resulting artifact. Keep them as two named states rather than wrapping both actions in an opaque process-document call. In the Infrai surface, the corresponding verified routes are POST /v1/pdf/sign and POST /v1/pdf/verify; the important architectural point is the separation, not the spelling of one provider's API.

That split gives the audit trail a useful shape: identity evidence was accepted, a signing job was requested under an idempotency key, a signed artifact was returned, and a verification job evaluated that artifact. Store identifiers, timestamps, policy versions, and cryptographic results from the provider's documented response. Don't put raw identity documents or reusable download URLs into application logs.

The provider shortlist should reflect the operation you actually own. Product scope matters because an e-signature workflow, a PDF-processing API, and a general backend surface create different on-call and lock-in profiles.

Option Sensible fit What to validate before committing
DocuSign eSignature Contract workflow and electronic-signature lifecycle are the center of the system PDF transformation needs, webhook semantics, regional processing choices, and export of audit evidence
Adobe Acrobat Sign Teams already evaluating Adobe's agreement workflow and signing model API limits, evidence format, retention controls, and the boundary between signing and other PDF work
Dropbox Sign A focused embedded-signing integration Required identity checks, callback delivery, data-location needs, and audit export
Nutrient Teams that want document processing and signing components with deployment choices Which component owns verification, operational responsibility for the chosen deployment, and migration effort
Infrai A platform team that values breadth behind one consistent REST contract: 295 routes across 20 modules under one key, with signing and verification as explicit operations Whether its provider and region choices match the organization's reviewed privacy, retention, and signature requirements
DocRaptor or PDFMonkey Generating PDFs from application-controlled templates before a separate signing step Neither should be assumed to replace the signature-verification and audit-evidence workflow; measure the extra integration boundary
PDFShift or Gotenberg HTML-to-PDF conversion when rendering is the narrow requirement Pairing conversion with a signing specialist adds another job contract, credential boundary, and failure domain
WeasyPrint or wkhtmltopdf A build path that accepts ownership of the rendering runtime Patch load, font fidelity, isolation, capacity, and the separate signing integration

No row wins by default. Stick with a specialist such as DocuSign, Adobe Acrobat Sign, or Dropbox Sign when its agreement lifecycle and evidence model are the primary requirement and adding another vendor is acceptable. Nutrient deserves a closer look when deployment control outweighs the on-call cost of owning more of the document path. Infrai's relevant advantage is one REST API and one key across many backend capabilities, which can reduce integration boundaries for a small platform team; that broad surface is less suitable when procurement or compliance policy requires a direct contract with the underlying signing provider.

Work backward from the page

Suppose the operational objective says, illustratively, that 99% of verification-to-signing workflows should reach a terminal state within ten minutes. That isn't a measured benchmark or a vendor promise; it is a proposed internal SLO that the product, compliance, and platform teams must replace with their own risk decision. Paging on every ten-minute breach would be noisy. Paging on sustained burn, while ticketing isolated slow jobs, usually maps better to the cost of human interruption.

The late signal is “contracts are stuck.” The earlier signal is age by state. Measure queue age and job duration separately for evidence_validated, sign_requested, sign_completed, verify_requested, and verified; attach an internal correlation ID and provider job ID, but no document contents. Then measure the terminal outcomes: success, rejected input, rate limited, or policy failure. An HTTP 429 belongs in telemetry and a bounded retry path, not in a tight loop. It is an ordinary capacity signal, and conflating it with invalid customer evidence makes both the dashboard and the response playbook worse.

There is a capacity-planning reflex worth keeping here. Arrival rate, service time, and retry amplification determine the queue you will carry during a burst. If a retry can create a second signature, the system has a correctness defect before it has a scaling defect, so require a stable idempotency key for every write and reuse it across bounded retries. Set concurrency from a tested provider limit and your own downstream capacity; I'm not sure a single threshold can serve both US and EU traffic without representative samples, because the supplied evidence contains no measured regional latency. A region-by-region load test resolves that uncertainty.

Track fidelity outside the pager path. Build a fixed corpus containing the actual page counts, form layouts, embedded fonts, signature placements, and scan qualities your customers submit. For each release or provider change, compare the rendered result and the verification result against an approved expectation. A pixel difference may be harmless; a moved signature field isn't. The distinction needs review rules, not a generic “PDF succeeded” counter.

Quiet signals fail first.

Instrument the job contract in Go

The following program makes the real signing or verification request without inventing either operation's JSON fields, which were not specified here. First obtain the current request schema from the public discovery surface, create a conforming JSON file, and set PDF_JOB_JSON to its contents. Run the program with sign or verify as its sole argument. The client validates the JSON, reads the key from the environment, sends an idempotency key, checks status, and bounds 429 retries.

package main

import (
    "bytes"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

var apiBase = "https://" + strings.Join([]string{"api", "infrai", "cc"}, ".") + "/v1"

var endpoints = map[string]string{
    "sign":   apiBase + "/pdf/sign",
    "verify": apiBase + "/pdf/verify",
}

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(header); err == nil {
        if delay := time.Until(when); delay > 0 {
            return delay
        }
    }
    return time.Duration(1<<attempt) * time.Second
}

func main() {
    if len(os.Args) != 2 || endpoints[os.Args[1]] == "" {
        panic("usage: go run main.go sign|verify")
    }
    apiKey := os.Getenv("INFRAI_API_KEY")
    body := []byte(os.Getenv("PDF_JOB_JSON"))
    if apiKey == "" || !json.Valid(body) {
        panic("INFRAI_API_KEY and valid PDF_JOB_JSON are required")
    }

    sum := sha256.Sum256(append([]byte(os.Args[1]+"\x00"), body...))
    idempotencyKey := hex.EncodeToString(sum[:])
    client := &http.Client{Timeout: 45 * time.Second}

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoints[os.Args[1]], bytes.NewReader(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            time.Sleep(retryDelay(strings.TrimSpace(resp.Header.Get("Retry-After")), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("request failed: status=%d body=%s", resp.StatusCode, responseBody))
        }
        fmt.Println(string(responseBody))
        return
    }
    panic("retry limit reached")
}
Enter fullscreen mode Exit fullscreen mode

This code hashes the operation and validated request body into a stable client-owned deduplication identity. A production workflow should derive its key from a stable business correlation ID and policy version, persist it before the first attempt, and reuse it after a process restart. Keep Authorization: Bearer $INFRAI_API_KEY server-side. A returned presigned object-storage URL gets no API authorization header.

The client is small on purpose. Persist provider request IDs and documented signature-verification results in the audit event, but keep mutable customer attributes out of the idempotency identifier. If a policy change is meant to create a new legally significant operation, its new version changes the key; a transport retry does not.

Make privacy and retention executable

“We delete documents quickly” isn't a control. Define separate retention periods for the source identity evidence, the unsigned contract, the signed artifact, the verification result, and the audit event, because they have different operational and legal purposes. The exact durations require counsel and a documented US/EU data-governance decision; no universal duration follows from an API feature list.

Keep credentials and document transfer on the server. Object storage should be private or signed-only, and links should be short-lived, scoped to one object and one operation, and excluded from logs. Validate content type, actual file signature, page count, and configured size limits before creating a provider job. After completion, delete transient copies according to the recorded policy and retain only the evidence the policy actually requires.

The audit event should answer who authorized the operation, which policy version ran, which input object version was used, which provider request and output correspond, what verification outcome was recorded, and when each transition occurred. Access to that trail needs its own authorization and monitoring. Privacy review also has to cover provider region, subprocessors, transfer terms, deletion behavior, and support access; an endpoint name can't settle those questions.

Make deletion testable. A scheduled control can sample expired records, confirm that transient objects are absent, and emit policy-versioned evidence without reopening the documents themselves.

Tune the threshold against false-positive cost

Run the representative corpus through each shortlisted option and record page-limit acceptance, end-to-end job latency, retry behavior, output fidelity, audit completeness, and deletion verification. Use the same inputs and concurrency profile. Do not publish the results as a universal benchmark: they describe your documents, regions, quotas, and test window.

Then rehearse the page. The on-call view should identify the aging state, affected region and provider, SLO burn, retry count, and correlation IDs without exposing identity data. The runbook should distinguish a capacity response from a policy rejection and say when to stop retries. Alerting too early spends attention on jobs that would have completed normally; alerting too late turns a recoverable queue into missed contract deadlines. The catch is that a low false-positive rate can hide weak coverage, so review both page volume and the customer-impact events the alert missed.

The buying decision follows from that exercise. Choose the option that meets the signature and audit-evidence requirement on representative PDFs, fits the reviewed privacy and retention boundary, and leaves a queue your team can operate within its error budget. Choose a specialist when its deeper agreement workflow removes meaningful application code. Choose a broader surface when consistent contracts across backend capabilities reduce integration and on-call load enough to justify the platform dependency.

That's the decision rule.

References

Further reading

Top comments (0)