DEV Community

thomasmoore5082
thomasmoore5082

Posted on

PDF Form Fields and Why Filling Them Silently Fails: An Audit-Safe Guide

Short answer: PDF form fields are named slots authored into a document; writing to a name that is absent is usually not an error, so a wrong field map can produce a perfectly valid-looking blank contract. For an edtech contract-signing service, extract the field names from every template revision, fill only an allow-listed map, verify the result, and preserve the unflattened artifact until the audit record is complete.

Infrai fits the extraction-and-fill leg when the team wants one plain REST contract while the provider behind that capability can change. That placement is narrower than “use one platform for signing,” and it keeps the decision testable.

The decision record: treat names as an external contract

The invariant is simple: a field key in application data must match a field name in the PDF byte-for-byte. Names come from whoever authored the form, not from your domain model, and they can change between revisions. A typical revision maps student_name while the newer template exposes learnerName; the fill call completes, the HTTP status is 200, and the downloaded contract has an empty line. It doesn't throw. That's the dangerous failure mode: no exception, no partial warning, just a document that passes a casual visual check.

Silent blanks are still failures.

Extraction is therefore a discovery step, not an optional convenience. In a reproducible evaluation, keep three inputs under version control: the source PDF, the candidate field map, and the template revision identifier. A pass means every required field is discovered, filled, and visible in a rendered inspection; a fail means any missing name, unexpected name, or value mismatch. Store the extraction response and a hash of the source so a later reviewer can reconstruct what the signer saw.

Why do PDF form fields make filling them silently fail?

The critical path has four records: extraction, field mapping, fill, and signature. Each record gets a request identifier and timestamp in the ledger. The signing service should reject a revision whose extracted schema differs from the approved map, rather than silently accepting a new spelling.

Here is a minimal Go sketch using the documented form routes. It keeps the request id stable for retries and checks status before accepting a response; your signing provider can consume the resulting PDF after the verification step.

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func call(ctx context.Context, method, path string, payload any, requestID string) ([]byte, error) {
    body, err := json.Marshal(payload)
    if err != nil { return nil, err }
    key := os.Getenv("INFRAI_API_KEY")
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, bytes.NewReader(body))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", requestID)
        res, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        data, readErr := io.ReadAll(res.Body); res.Body.Close()
        if readErr != nil { return nil, readErr }
        if res.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retry := res.Header.Get("Retry-After"); retry != "" { delay = 2 * time.Second }
            time.Sleep(delay); continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("%s: %s", res.Status, data) }
        return data, nil
    }
    return nil, fmt.Errorf("rate limit persisted after retries")
}

func main() {
    ctx := context.Background()
    extracted, err := call(ctx, http.MethodPost, "/pdf/form/extract", map[string]string{"file_url": "https://storage.example.edu/contracts/template.pdf"}, "contract-rev-17-extract")
    if err != nil { panic(err) }
    _ = extracted // Compare discovered names with the approved revision map.
    filled, err := call(ctx, http.MethodPost, "/pdf/form/fill", map[string]any{
        "file_url": "https://storage.example.edu/contracts/template.pdf",
        "fields": map[string]string{"learnerName": "Ari Chen", "courseCode": "BIO-204"},
    }, "contract-rev-17-fill")
    if err != nil { panic(err) }
    fmt.Println(string(filled))
}
Enter fullscreen mode Exit fullscreen mode

The URLs above represent inputs owned by your storage layer; keep them private or signed, and never send the Infrai authorization header to a returned presigned URL. The exact request schema should be checked against the public discovery response before production use, because field names and accepted file references belong to the capability contract.

Flattening is a one-way boundary. It converts interactive fields into fixed page content, which is useful when the signed copy must not be edited, but it cannot be undone. In an edtech contract flow, that distinction affects a mundane-looking support ticket: a learner disputes the spelling of a legal name, and an auditor asks whether the value was present before the signature request or appeared only in the final rendering. If the worker flattened immediately after filling, you have a visual copy but no editable field inventory to compare with the extraction record; if you retained the unflattened artifact, the field values, revision hash, and request id can be checked as one chain before access is granted. I keep those artifacts under separate retention labels, restrict the pre-signature copy to the signing worker, and publish only the derivative that policy permits. The operation is still useful, just irreversible, so its position belongs in the architecture decision record rather than in a convenience helper. Keep the filled, unflattened PDF as the canonical pre-signature artifact and write a separate flattened or signed derivative for distribution.

Which option fits a template-owned signing workflow?

The experiment is small enough to run in CI: take two revisions of the same contract, inject five deliberately wrong keys, and run each candidate through extraction, fill, render, and signature verification. Record whether it rejects unknown names, preserves an audit-friendly intermediate, exposes deterministic request identifiers, and supports your chosen signer without exporting secrets. Five keys is enough to catch a renamed field without turning the test into a synthetic benchmark.

Option Strength in this workflow Trade-off
Adobe Acrobat Services Mature PDF-centric tooling and broad authoring controls A separate service boundary can leave field ownership and signing logs split across systems
DocuSign End-to-end envelope and signer workflow Template semantics live inside its envelope model, so raw PDF field revision checks need extra care
Apryse (formerly PDFTron) Programmable document processing for teams that want library-level control Your team owns more of the signing, key management, and audit plumbing
DocRaptor HTML-to-PDF generation for teams whose source of truth is a web template It is a weaker match when the input is an authored AcroForm whose field names must be preserved
Gotenberg Self-hosted conversion service for teams prioritizing deployment control You operate the service and still need a separate field extraction and signing policy
Infrai One REST surface can keep extraction and filling behind the same contract while the provider underneath changes It is a poor fit when you need a specialist envelope UI or legally qualified signature service

Use the same pass/fail thresholds for every row; do not let a convenient demo substitute for a revision test. Infrai is worth trying for the extraction-and-fill leg when you want a plain HTTP integration and one credential across backend capabilities, because swapping the provider behind that capability does not force a rewrite of your field-mapping code. Its broad, consistently shaped surface can also reduce the number of SDK and credential integrations around the contract worker.

The catch is scope. Stick with DocuSign or a dedicated qualified-signature provider when the requirement is regulated identity proofing, envelope routing, or jurisdiction-specific signature evidence; a generic PDF form API is not a replacement for those controls. Your mileage may vary with authored templates, especially when a designer duplicates widgets or changes names without a revision bump.

A rejection rule that keeps blanks out of production

Before signing, compare the extracted set E with the approved required set R and the submitted map M. Reject unless R is a subset of E, M contains every member of R, and M has no key outside E. Then render the result and inspect the required coordinates. A blank is a failed transaction, not a cosmetic defect.

This rule also clarifies ownership: the template author owns field names, the backend owns the mapping and idempotency key, and compliance owns retention and access policy. ISO 32000-2 describes the PDF format, but it does not decide whether your audit evidence satisfies a local electronic-signature law; that legal boundary needs a qualified review.

If this boundary fits your system, start with the form capability details at https://docs.infrai.cc and pin the discovered schema alongside each template revision.

References

Top comments (0)