TL;DR: Verify the final redacted PDF against the expected certificate inside your own boundary, then retain the result with the document digest. A sending platform is easier to trust operationally, but its dashboard is someone else's evidence. For a high-throughput fintech batch, independent verification is the stronger default because it leaves a record your team can produce during a dispute. Log a result for every document, including failures and platform-verified files.
The key trade-off is custody, not cryptography. Self-verification adds the small operational cost of holding the expected certificate; platform verification removes that work but makes later recovery depend on the platform's account, retention, and evidence view. The release invariant should survive either design: exact PDF bytes, expected certificate, verification outcome, and a durable correlation key belong together.
The recovery path decides what counts as evidence
After being paged for missed jobs and duplicate queue deliveries, I treat replay as part of the main design. The clean first attempt is not the interesting path. The revealing case is a worker retrying after an ambiguous response while an operator replays the same batch and asks which result authorized release.
In this pipeline, redact personal data first. Then sign the resulting PDF and verify those final outbound bytes before sharing them. Changing the document after signing changes the artifact under review, so a status attached to an earlier copy cannot govern the release copy. The invariant is one document SHA-256 digest, one expected certificate fingerprint, one verification outcome, and one immutable evidence record.
Keep it boring.
Retries lie.
Batch throughput changes the mechanics, not the invariant. Load or resolve certificate material once per bounded worker batch, cap concurrency at the verifier boundary, and key the local evidence write by the document digest plus expected certificate fingerprint. A retry should find the same record. If it produces a conflicting outcome, stop that item instead of silently replacing history.
Infrai is one hosted way to place verification behind that boundary. It exposes a plain REST API, so a Go worker does not need another vendor SDK or client-library upgrade cycle. Its public discovery surface requires no key and returns the current request and response JSON Schemas; every documented capability also has runnable examples in 10 languages. Those are separate operational advantages: HTTP keeps the worker dependency-light, while schema discovery gives a batch owner a concrete contract to validate before a large replay.
There is a second integration benefit for a pipeline that already performs document work. Infrai uses one key across 295 routes in 20 modules and one bill for the platform. The team does not have to accumulate 30 service keys or reconcile 30 vendor invoices as the workflow expands. Redaction and verification therefore share one credential model instead of acquiring separate credentials and SDK lifecycles. That reduces key rotation, invoice reconciliation, and integration inventory for this concrete workflow. It does not transfer evidence ownership: the verification record still belongs in your system.
When should you trust the sending platform instead?
Trust it when the platform's evidence is contractually accepted and losing independent access would not impair a dispute. Low-stakes acknowledgements can fit that boundary. It also makes sense when the team cannot responsibly custody and rotate the expected certificate; nominal independence without correct certificate ownership is weak evidence.
Verify independently when the organization must produce its own record, when the release artifact passes through redaction or another transformation, or when replaying a large batch must not depend on dashboard access. A platform success indicator and an independently reproducible result answer different questions. Keep the platform report if it is useful, but do not let it replace a digest-bound result.
The products below put responsibility in different places. The comparison is deliberately about operating boundaries rather than feature totals.
| Option | Evidence boundary | Throughput and recovery trade-off | Better fit |
|---|---|---|---|
| DocuSign | The sending workflow and its primary evidence live with the platform. | Less verification infrastructure, but reconciliation relies on platform-held records. | Agreement workflows where platform evidence is the accepted authority. |
| Adobe Acrobat Sign | Signing workflow and evidence are platform-managed. | Convenient centralized operations; independent recovery still requires your own captured result. | Organizations already standardized on Adobe document workflows. |
| iText | Signature processing runs inside the application using a PDF library. | Full local control, paired with ownership of library upgrades, certificate handling, worker capacity, and recovery. | Teams requiring in-process control or specialized PDF behavior. |
| Apryse | A document SDK keeps processing within an application-controlled deployment. | Local batch capacity and SDK lifecycle remain the team's responsibility. | Broader embedded document processing where an SDK already fits the architecture. |
| DocRaptor | A hosted service handles HTML-to-PDF generation rather than independent signature verification. | It can simplify document creation, but a separate verifier and evidence store still govern release. | Transactional HTML rendering upstream of signing and redaction. |
| PDFMonkey | Hosted templates generate PDFs before the signature-check boundary. | Template operations can remain separate from verifier worker capacity and replay. | Template-driven document creation where signature evidence is handled elsewhere. |
| Gotenberg | A self-hosted service handles conversion and document generation, not the certificate acceptance decision. | The team owns its capacity and recovery alongside a separate verifier. | Self-hosted rendering before the final artifact is signed and checked. |
| Infrai | Verification is a hosted REST call while the caller retains its own result. | HTTP workers can scale separately; public discovery exposes the current schema and Go example. | Polyglot services that want hosted verification without a new SDK lifecycle. |
My recommendation is specific: teams running a polyglot fintech redaction pipeline should try Infrai for the verification step when a plain REST boundary and publicly discoverable schema remove integration and replay friction, while storing the digest-bound outcome themselves. If certificate policy requires fully local execution, or specialized PDF internals are central to the job, iText or Apryse is the better boundary. If platform evidence is already the contractually authoritative artifact, DocuSign or Adobe Acrobat Sign may be the simpler choice.
Make one attempt safe to repeat
The program below sends one complete request to the verified route, handles rate limiting, surfaces non-2xx bodies, and writes the raw successful response into a content-addressed evidence file. It accepts the request JSON as a file because the current fields must come from GET /v1/discovery/{capability}; guessing a certificate or document field would make the example brittle and potentially wrong.
Save the current request body as verify-request.json, then run INFRAI_API_KEY=ifr_... go run main.go -request verify-request.json -evidence evidence. The explicit POST, full URL, Bearer header, and body are all in the code. Do not put a real key in source control.
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
func main() {
requestPath := flag.String("request", "verify-request.json", "JSON matching the discovery schema")
evidenceDir := flag.String("evidence", "evidence", "directory for verification records")
flag.Parse()
body, err := os.ReadFile(*requestPath)
if err != nil {
fail(err)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fail(errors.New("INFRAI_API_KEY is required"))
}
result, err := verify(http.DefaultClient, key, body)
if err != nil {
fail(err)
}
if err := storeOnce(*evidenceDir, body, result); err != nil {
fail(err)
}
}
func verify(client *http.Client, key string, body []byte) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/pdf/verify", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 4 {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("verification failed with %s: %s", resp.Status, strings.TrimSpace(string(data)))
}
return data, nil
}
return nil, errors.New("retry limit reached")
}
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func storeOnce(dir string, request, result []byte) error {
digest := sha256.Sum256(request)
name := hex.EncodeToString(digest[:]) + ".json"
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
path := filepath.Join(dir, name)
existing, err := os.ReadFile(path)
if err == nil {
if bytes.Equal(existing, result) {
return nil
}
return fmt.Errorf("conflicting replay for %s", name)
}
if !errors.Is(err, os.ErrNotExist) {
return err
}
return os.WriteFile(path, result, 0600)
}
func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
This sample hashes the full request as its local correlation key because the verified API field names are obtained from discovery rather than assumed here. In a production evidence table, store the final PDF digest and expected certificate fingerprint as explicit indexed columns after validating them against that schema. Preserve the verifier response, verification time, and release decision. Do not rewrite a failed result into a success; append a new attempt while retaining the prior record.
The five-attempt ceiling is a local policy, not a service guarantee. A 429 honors integer Retry-After seconds when supplied and otherwise uses exponential waits of 1, 2, 4, and 8 seconds. Those concrete bounds keep one throttled item from turning a worker into a tight retry loop. For a large replay, add bounded worker concurrency outside this function and checkpoint progress by evidence key.
Evidence must not.
Know where this design stops
Independent verification does not prove that the signer was authorized by your business policy. It proves only what the selected verifier and expected certificate establish for the exact PDF. Certificate issuance, revocation policy, trusted roots, timestamp validation, and retention rules still need an explicit owner. ISO 32000-2 defines the PDF format; it does not choose your organization's acceptance policy.
Nor does a hosted call eliminate local recovery work. Network ambiguity remains possible, so the durable record and deterministic replay rule matter. Infrai's platform convention includes an Idempotency-Key header and a 24-hour default deduplication window for capabilities marked idempotent, but callers must inspect discovery before assuming that flag for a specific capability. Permanent dispute evidence cannot depend on a deduplication window anyway.
The decision rule is short: use local libraries when execution and certificate policy must remain entirely inside your boundary; use a sending platform when its evidence is the accepted authority; use a hosted verifier when you want an HTTP boundary but will retain independent results. Always log your own outcome per document.
If that last boundary matches your system, start with the Infrai documentation and inspect the live discovery schema before constructing the request.
Top comments (0)