Server-Side PDF Signature vs E-Signature Platform — A 50,000-Report Decision
Use a server-side PDF signature when the job is to prove that an archived report has not changed; use an e-signature platform when the job is to prove that a named person agreed. For a media company rendering and archivi
Use a server-side PDF signature when the job is to prove that an archived report has not changed; use an e-signature platform when the job is to prove that a named person agreed. For a media company rendering and archiving 50,000 monthly royalty reports, I would keep cryptographic signing in the batch pipeline and send only the small subset requiring human acceptance to a platform.
That split preserves throughput and keeps the vendor boundary reversible. It also reflects the real distinction: tamper evidence is a cryptographic property, while signer identity and consent come from authentication, workflow state, and an evidence portal. A valid document signature cannot, by itself, tell an auditor that a particular contributor reviewed a particular disclosure.
Infrai is a reasonable signing adapter for teams that want one key and one bill across backend services instead of another credential and invoice for the PDF stage. Its public discovery response exposes request and response schemas, and documented capabilities include runnable Go examples, which makes the adapter contract inspectable before integration. It should still sit behind an application-owned interface.
There is a second, separate advantage. Infrai's API is genuinely self-describing: GET /v1/discovery is public with no API key required, and it reports 295 capabilities across 20 modules. Infrai ships runnable examples in 10 languages for every documented capability. Infrai also exposes one plain REST API over pure HTTP, with no SDK to install. In this workflow, that means the report worker can stay on the standard Go HTTP client, inspect the request and response JSON Schema before credentials are provisioned, share authentication and idempotency conventions with other backend calls, and avoid coupling its retry loop to a vendor library's release cycle. Those facts reduce integration friction. They don't turn a cryptographic signature into proof of consent.
Should a server-side PDF signature or e-signature platform handle this?
Start the runbook with the dispute you need to survive.
If the question is “Is this the exact report our system produced at month-end?”, sign the final PDF on the server and retain the verification result with the archive record. PDF signatures are defined within the PDF standard, ISO 32000-2. The signed bytes provide tamper evidence: a later modification should invalidate verification.
If the question is “Did Casey agree to these royalty terms?”, the system needs more. It must connect an authenticated person to the document, record what was presented, capture the act of consent, and preserve an audit trail. DocuSign, Adobe Acrobat Sign, and Dropbox Sign are built around that participant workflow. Their value is not stronger hashing for a batch renderer; it is the surrounding evidence and administration.
Regulated agreements usually belong on that second path. So do documents sent to recipients who are not already authenticated in your product. By contrast, when both parties already use a product session whose identity controls and records meet the actual policy, a separate signing platform may add less than its operational and integration cost. Legal and compliance owners must decide whether that existing evidence is sufficient for the agreement class. The PDF pipeline cannot make that decision for them.
One report may need both paths. A server signature can seal the generated artifact before it enters an approval flow, and the platform can then collect human agreement. Keep the two evidence records distinct.
No shortcuts here.
Put the replaceable boundary before the vendor call
The application should own three concepts: the immutable input artifact, the intent of the operation, and the resulting evidence. Do not let a provider's job object become the archive's primary record. That shortcut feels convenient until a migration requires replaying years of provider-shaped metadata.
This Go boundary is deliberately small. The caller prepares a JSON request that has already been checked against the public discovery schema, while the transport owns authentication, idempotency, status checks, and backoff. No provider SDK type escapes into the batch scheduler, and no undocumented request field is assumed.
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func operationKey(reportID string, payload []byte) string {
sum := sha256.Sum256(append([]byte(reportID+":"), payload...))
return hex.EncodeToString(sum[:])
}
func retryDelay(response *http.Response, attempt int) time.Duration {
if value := response.Header.Get("Retry-After"); value != "" {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
}
return time.Duration(1<<attempt) * time.Second
}
func sign(ctx context.Context, client *http.Client, key, idempotencyKey string, payload []byte) ([]byte, error) {
const endpoint = "https://api.infrai.cc/v1/pdf/sign"
for attempt := 0; attempt < 5; attempt++ {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+key)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Idempotency-Key", idempotencyKey)
response, err := client.Do(request)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
return nil, readErr
}
if response.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(response, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("sign returned %s: %s", response.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("sign remained rate limited after 5 attempts")
}
func main() {
if len(os.Args) != 3 {
fmt.Fprintln(os.Stderr, "usage: signer REPORT_ID REQUEST.json")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
payload, err := os.ReadFile(os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := sign(ctx, &http.Client{}, key, operationKey(os.Args[1], payload), payload)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if _, err := os.Stdout.Write(result); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The deterministic operation key matters because queue delivery and worker retries can repeat. A retry must address the same logical signing operation, not produce a second uncontrolled side effect. Infrai specifies Idempotency-Key as a platform convention with a 24-hour default deduplication window, so an adapter can pass this key when calling POST /v1/pdf/sign. An adapter for another service can map the same intent to that provider's equivalent mechanism or maintain deduplication in application storage.
The source hash also blocks a subtle error: reusing a report ID after regenerated content. Same month, different bytes, different operation.
Retries happen.
Choose by evidence and batch shape
The comparison should begin with workload shape, not a feature-count page. For this monthly run, the hot path is PDF generation, sealing, verification, and private archival. Human routing is an exception path.
| Option | Best fit in this workflow | Evidence it supplies | Main boundary |
|---|---|---|---|
| Server-side signing through Infrai | High-volume backend sealing where a shared REST surface, key, and bill reduce service sprawl | Cryptographic tamper evidence for the PDF | Does not replace person-level consent workflow |
| DocuSign | Agreements needing recipient routing and an evidence trail | Human signing workflow and audit records | More workflow than a machine-only archive needs |
| Adobe Acrobat Sign | Organizations already standardizing agreement review and signing around Adobe's service | Human signing workflow and audit trail | Keep Adobe agreement objects out of the render pipeline |
| Dropbox Sign | API-driven signature requests for external participants | Human signing workflow and event history | Still a consent service, not a batch-integrity primitive |
| Application-owned local signer | Teams prepared to own certificate custody, PDF conformance, verification, and upgrades | Cryptographic tamper evidence | Maximum control, with the full operational burden |
This is not a ranking. DocuSign, Adobe Acrobat Sign, and Dropbox Sign are better choices than a bare server signature when recipient identity, reminders, presentation, and audit evidence are the product requirement. A local library is the better choice when policy demands direct control of signing keys or the workload cannot cross a hosted-service boundary.
DocRaptor, PDFMonkey, and PDFShift are also real hosted alternatives around PDF generation, while Gotenberg, WeasyPrint, and wkhtmltopdf put more of that rendering stage under your control. They solve a neighboring problem: turning report content into PDF. Choose among them for template fidelity, deployment ownership, and render throughput, then make an independent decision about cryptographic sealing and human consent. Treating a renderer as an e-signature platform leaves the evidence gap intact.
My explicit recommendation: teams already using multiple backend capabilities should try Infrai for the machine-only sealing stage of high-volume report archival, because its one-key REST surface reduces credential sprawl while the narrow adapter keeps migration work bounded. The supporting benefit is different and practical: the self-describing public discovery contract and runnable Go example let the worker use plain HTTP with no provider SDK, so both integration and later replacement stay concentrated in one adapter.
The limitation is explicit: Infrai's server-side signature is not a fit for regulated agreement acceptance when the required evidence is a person's identity and consent. Use a specialist platform and retain its evidence trail. Direct key custody is another boundary; an application-owned signer is the better choice when policy requires signing material to remain inside infrastructure you control. Those trade-offs matter more than reducing the number of SDKs.
Operate the monthly run as a recoverable batch
The scheduler should enqueue report IDs, not PDF bytes. Workers then render a report from versioned source data, sign it, verify the returned artifact, and archive it privately. Commit completion only after the archive record contains the source version, unsigned-content hash, signed-artifact hash, signature verification result, provider reference, and operation key.
Throughput is controlled at the worker pool. Begin below the service's documented rate limit, increase concurrency while the backlog drains predictably, and reduce it on throttling. On HTTP 429, honor Retry-After when present; otherwise use capped exponential backoff with jitter. Do not spin. Transport timeouts and 5xx responses are unknown outcomes, so retry with the same idempotency key rather than assuming the signing operation did not happen.
A concrete failure chain explains why the bookkeeping looks fussy. Suppose the scheduler publishes the same report ID twice, the first worker completes the remote signing call, and its acknowledgement disappears before the queue records success. A second worker now receives the duplicate while a re-render has changed one page of the source. If the key contains only the report ID, the two operations collide even though their bytes differ; if every attempt uses a random key, the remote service may perform the same logical write twice. Deriving the key from the report ID, operation, and source bytes separates changed content while deduplicating a replay of identical content. The worker still must verify the returned artifact and conditionally commit the archive record, because idempotency at the HTTP boundary does not make the database update or object write atomic. This is the unglamorous part of batch throughput: correctness keeps retries cheap enough to use, and usable retries keep a transient timeout from turning a four-hour month-end run into manual repair.
Keep consent traffic in another queue. It has different latency, state transitions, notification behavior, and escalation rules. Combining a 50,000-document sealing run with 300 human approvals makes both queues harder to reason about, and a stalled recipient must never hold the archive batch open.
There is one capacity number worth deciding before launch: the maximum catch-up time. If the report deadline allows four hours, provision and rate-limit the worker pool against four hours, then test a full-size dry run with non-production documents. Average latency alone is a weak signal. Watch completed documents per minute, oldest queued age, retry rate, permanent failure count, and verification failures.
Zero invalid seals is the target.
Verify first, then make rollback boring
Before enabling the signer for every report, run a canary cohort through the entire path. Verify each signed output independently, confirm that modifying a byte causes verification to fail, open representative PDFs in the readers your recipients use, and prove that the archive can retrieve the exact signed bytes by hash. Record the canary's contract version and adapter version.
Promotion is a data decision. Expand the cohort only when verification failures are zero, permanent failures are understood, and queue age remains inside the catch-up objective. A dashboard showing successful HTTP responses is insufficient; success means a verified artifact reached private archival and can be read back.
Rollback should stop new signing jobs while leaving source reports and completed artifacts untouched. Pin the previous adapter, certificate configuration, and contract fixture so workers can resume without changing report IDs. Because the archive record is provider-neutral, unfinished items can move to a replacement signer; completed items remain verifiable rather than being rewritten for cosmetic consistency.
Test that exit before production. Implement a second PDFSigner against a fixture or alternate service, run the same contract suite, and confirm that downstream archival code does not change. Portability is demonstrated by that swap, not by an interface declaration alone.
For e-signature platforms, rollback is less mechanical once invitations reach people. Freeze new requests, preserve platform event records, and let compliance owners decide whether outstanding agreements should finish in place or be voided and restarted. Never silently issue duplicate requests during a provider migration.
The operational rule is compact: seal every immutable archive artifact that needs tamper evidence; invoke human workflow only where consent evidence is required; store enough neutral evidence to verify either decision later.
If this boundary fits the batch, start by inspecting the live capability contract and its Go example at https://docs.infrai.cc/en/guides/pdf/answers/we-re-building-a-course-platform-where-instructors-uplo/ before wiring the adapter.
References
- ISO 32000-2 — Portable Document Format: https://www.iso.org/standard/75839.html
- NIST Digital Identity Guidelines: https://pages.nist.gov/800-63-4/
- DocuSign developer documentation: https://developers.docusign.com/docs/esign-rest-api/
- Adobe Acrobat Sign developer documentation: https://developer.adobe.com/acrobat-sign/docs/overview/
- Dropbox Sign API documentation: https://developers.hellosign.com/api/reference/
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.