DEV Community

BrennThorn8571
BrennThorn8571

Posted on

Go API Evidence: Redaction Audit Trail Requirements for E-commerce Buyer Data

An e-commerce team cannot prove a customer address was removed merely by producing a PDF with a black rectangle over it. TL;DR: keep the original under stricter access control, record the exact fields selected for removal and the actor who approved them, verify the resulting PDF by extracting its text, and sign the audit record that binds those facts to hashes of both files. The redacted document proves the result; it does not prove the decision that produced it.

That distinction changes the architecture. A small SaaS team sharing an order dispute packet with a carrier might need to retain the order number and item lines while removing the buyer's name, email, phone number, street address, and free-form delivery note. I would set the service-level objective around a verifiable release, not around a successful redaction request: no packet is shareable until its expected removals are absent from extracted output, its required business fields remain, and its signed audit event is durable.

Fail closed.

How Should an API Meet Redaction Audit Trail Requirements?

The useful evidence bundle has four independently understandable parts: an access-restricted original, a redaction decision, a verified output, and a signature over the record connecting them. The decision should identify the document, selected field classes, actor, approval time, reason, and tool version or policy revision. Hash the original and output bytes so that a reviewer can tell which exact artifacts the record describes. For order ORD-10482, for example, an event that says only redaction succeeded cannot answer whether the buyer name, phone, street address, and delivery note were selected, who approved that selection, which policy revision applied, or which output bytes the reviewer saw; a useful event answers all of those questions without retaining the removed values in a broadly accessible log.

The verification step matters more than a screenshot. Extract text from the finished PDF and test that every prohibited value is absent; then test that required values, such as the order number, remain. Image-only pages need equivalent inspection after optical character recognition or image review. A visual overlay can hide text while leaving the underlying characters available for selection or extraction, so appearance alone is a weak control.

A digital signature is useful only if its scope is explicit. Signing the released PDF can establish document integrity and signer identity. Signing the audit record can bind the actor, policy, hashes, and verification result. For this workflow I want both when a counterparty must authenticate the file, but I do not confuse either signature with proof that the chosen redactions were substantively correct. Approval and verification still carry that burden.

This creates an invariant that survives vendor changes: a release state is reachable only when original_hash, output_hash, selected fields, actor, verification result, and signature are present. Keep the original; deleting it destroys the reference needed for a later investigation. Its access policy should be materially stricter than the policy for the shareable copy.

The Decision Is About Evidence, Not a Black Box

The market options solve different layers, so a feature checklist hides the largest operational trade-off. An SDK can keep document bytes inside your boundary but moves upgrades, memory sizing, malformed-file handling, and parts of the on-call burden onto your team. A hosted API shrinks that surface, while adding a processor, an external dependency, and a contract whose retention and regional behavior must be reviewed outside the narrow mechanics of redaction.

Option Execution model Evidence and signature fit Platform-team consequence
Apryse SDK Embedded document SDK Redaction and digital-signature primitives can live in the application boundary; the application still owns the audit event and release policy More control over bytes and deployment, with library upgrades and capacity planning on your side
Nutrient SDK Server or client document SDK Supports redaction and signing workflows; your service must bind approvals, hashes, and verification into one record Useful when SDK-level control matters, but it expands the code and upgrade surface you operate
Adobe Acrobat Sign API plus PDF tooling Managed agreement and signature workflow Strong fit when signer workflow and agreement history dominate; redaction verification remains a separate application control Reduces signature-workflow construction, while introducing a distinct integration and vendor boundary
AWS Comprehend PII detection plus your PDF stack Managed text analysis, not an end-to-end PDF redactor Can identify PII in text; rendering, irreversible removal, signatures, and the audit chain remain yours Attractive for teams already operating on AWS, but it is a composition project rather than one document control
Infrai REST API Hosted PDF capabilities behind one API The documented PDF surface includes redaction, parsing, signing, and verification; the application still owns the authoritative decision log Discovery is public and self-describing, with request schemas and runnable examples, so evaluating a new capability starts from one endpoint rather than a new SDK
DocRaptor Hosted HTML-to-PDF generation API Good fit for creating order documents from HTML; it does not replace a redaction decision log and verification gate Low generation operations burden, but a poor match for altering an existing customer PDF
PDFMonkey Hosted template-to-PDF generation API Useful for controlled templates where private fields can be omitted before rendering; existing-PDF redaction needs another component Moves template rendering out of the service while adding an external processor
Gotenberg Self-hosted document conversion API Useful for conversion inside your boundary; redaction, signatures, and audit evidence remain application work Avoids a hosted processor but puts patching and capacity on the platform team

No row eliminates application responsibility. Apryse or Nutrient is the cleaner boundary when documents must not leave infrastructure you control and the team accepts the runtime burden. Adobe deserves attention when agreement lifecycle, signer identity, and managed signing are the center of gravity. Comprehend is a component for a team that already has reliable PDF reconstruction. DocRaptor and PDFMonkey are more convincing when the source is a template and sensitive fields can be excluded before a PDF exists; Gotenberg suits a team willing to operate conversion itself. A unified REST surface is a reasonable small-team choice when integration and on-call load outweigh lock-in concerns, but the evidence record must remain portable and owned by the application. This is a buy-versus-build decision with an awkward middle: buying document operations reduces code, yet neither purchases the policy judgment nor transfers accountability for release.

I would reject any option that returns only a finished file and a success flag. I would also reject a design whose audit history exists solely in a vendor dashboard: exportable, append-only application events are the exit strategy.

A Preventative Go Gate Before Release

The following program first reads the API's public discovery document and confirms that redaction and parsing are advertised as available. That is the self-describing advantage in concrete form: the integration can inspect one machine-readable surface instead of installing another SDK merely to learn a capability. It then applies the local gate, hashes the exact input and output bytes, checks extracted output supplied by the parsing stage, signs the canonical JSON record with Ed25519, and verifies that signature before allowing release. In production, keep the private signing key in a managed key service or hardware-backed keystore; the sample generates an ephemeral key solely to remain runnable. Set INFRAI_API_KEY and INFRAI_BASE_URL in the environment; the latter prevents an unlinked review from embedding a vendor URL.

package main

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

type Capability struct {
    ID        string `json:"id"`
    Path      string `json:"path"`
    Available bool   `json:"available"`
}

type Discovery struct {
    Capabilities []Capability `json:"capabilities"`
}

type AuditRecord struct {
    DocumentID   string   `json:"document_id"`
    OriginalHash string   `json:"original_sha256"`
    OutputHash   string   `json:"output_sha256"`
    Actor        string   `json:"actor"`
    Policy       string   `json:"policy"`
    Removed      []string `json:"removed_fields"`
    VerifiedAt   string   `json:"verified_at"`
    Verified     bool     `json:"verified"`
}

func digest(b []byte) string {
    sum := sha256.Sum256(b)
    return hex.EncodeToString(sum[:])
}

func absent(text string, prohibited []string) error {
    for _, value := range prohibited {
        if value != "" && strings.Contains(text, value) {
            return fmt.Errorf("prohibited value remains in extracted output: %q", value)
        }
    }
    return nil
}

func discover(client *http.Client, baseURL, apiKey string) (Discovery, error) {
    var result Discovery
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, strings.TrimRight(baseURL, "/")+"/v1/discovery", nil)
        if err != nil {
            return result, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        resp, err := client.Do(req)
        if err != nil {
            return result, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return result, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return result, fmt.Errorf("discovery returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
        }
        if err := json.Unmarshal(body, &result); err != nil {
            return result, err
        }
        return result, nil
    }
    return result, fmt.Errorf("discovery remained rate limited")
}

func requirePaths(discovery Discovery, paths ...string) error {
    for _, required := range paths {
        found := false
        for _, capability := range discovery.Capabilities {
            if capability.Path == required && capability.Available {
                found = true
                break
            }
        }
        if !found {
            return fmt.Errorf("required capability unavailable: %s", required)
        }
    }
    return nil
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if apiKey == "" || baseURL == "" {
        log.Fatal("INFRAI_API_KEY and INFRAI_BASE_URL are required")
    }
    discovery, err := discover(&http.Client{Timeout: 15 * time.Second}, baseURL, apiKey)
    if err != nil {
        log.Fatal(err)
    }
    if err := requirePaths(discovery, "/v1/pdf/redact", "/v1/pdf/parse"); err != nil {
        log.Fatal(err)
    }

    originalPDF := []byte("original order packet bytes")
    redactedPDF := []byte("redacted order packet bytes")
    extractedOutput := "Order ORD-10482; item: trail shoes; quantity: 1"
    prohibited := []string{"Maya Chen", "maya@example.com", "+1-206-555-0142", "18 Pine Street"}

    if err := absent(extractedOutput, prohibited); err != nil {
        log.Fatal(err)
    }
    if !strings.Contains(extractedOutput, "ORD-10482") {
        log.Fatal("required order identifier was removed")
    }

    record := AuditRecord{
        DocumentID:   "order-dispute-10482",
        OriginalHash: digest(originalPDF),
        OutputHash:   digest(redactedPDF),
        Actor:        "privacy-reviewer@example.com",
        Policy:       "carrier-disclosure-v3",
        Removed:      []string{"customer_name", "email", "phone", "street_address"},
        VerifiedAt:   time.Now().UTC().Format(time.RFC3339),
        Verified:     true,
    }
    payload, err := json.Marshal(record)
    if err != nil {
        log.Fatal(err)
    }

    publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
    if err != nil {
        log.Fatal(err)
    }
    signature := ed25519.Sign(privateKey, payload)
    if !ed25519.Verify(publicKey, payload, signature) {
        log.Fatal("audit signature verification failed")
    }

    fmt.Printf("release approved: %s, signature bytes: %d\n", record.DocumentID, len(signature))
}
Enter fullscreen mode Exit fullscreen mode

The string checks are intentionally plain. A real parser can normalize Unicode, separate pages, and return structured spans; policy tests should account for alternate renderings of phone numbers and addresses. Do not claim the check covers a value you never supplied to it. For high-risk packets, combine deterministic known-value tests with field detection and human sampling, then record which checks ran.

Capacity planning starts at the gate. Size the worker pool for peak release demand, bound document size and processing time, isolate originals from general job logs, and queue retries without allowing two successful workers to publish two different artifacts. Track separate error budgets for transformation, extraction, signing, and durable audit writes, because a single aggregate availability number can look healthy while verification is failing closed all afternoon.

The trade-off is explicit.

Where This Architecture Stops Helping

This pattern does not decide what the organization is legally permitted to disclose. Counsel and privacy owners must define the field policy, retention period, reviewer authority, and evidentiary standard. Nor does text extraction prove that every pixel, annotation, attachment, form field, metadata entry, or earlier incremental PDF revision is clean. The verification plan must match the document features actually accepted by the service.

For a one-off, low-risk internal document, a controlled desktop review may be proportionate. For bulk order packets with stable templates, deterministic coordinates plus extracted-text verification can work well. Free-form scans, handwritten notes, mixed scripts, and adversarial PDFs justify stronger detection and manual review. There is no honest universal automation rate without measuring the team's own corpus.

The operational decision is therefore narrower than “Which redaction API is best?” Choose the execution model whose failure modes your team can staff, keep the audit schema independent of it, and make release depend on verification. A signed, hash-bound decision record plus a separately verified output is the defensible unit of evidence.

Sources

Top comments (0)

⌂ 换网址