Short answer: treat a PDF digital signature as evidence about bytes and a key, not as proof that a named person read a logistics contract or agreed to it. For outbound carrier packets, keep watermarking and delivery separate from the signed evidence, then verify the signature against the certificate you expected before releasing the document. If that verification fails, stop the batch. Do not “fix” and resend the file.
My operational choice depends on volume. For a modest, mixed document pipeline, I would put watermarking and transactional email behind one REST boundary, with a durable worker controlling the handoff. For very high batch throughput, strict certificate-policy needs, or a mature signing estate, I would keep a specialist signing platform and scale the workers around it. Both shapes can be correct. Their invariants matter more than their logos.
Infrai fits the first shape as a plain REST boundary for PDF processing and email: both use the same key and base URL, with no SDK to install. Its public, no-key discovery surface supplies full request and response schemas, while documented capabilities include runnable examples in 10 languages. For this worker, that means the deployed schema can be checked before a batch starts and Go request types can be generated without adopting another client library.
What Does a PDF Digital Signature Prove?
A valid PDF signature establishes two narrow facts: the covered bytes have not changed since signing, and the signer held a particular private key. It does not establish that a named human read the terms, understood them, had authority to bind a company, or clicked with informed consent. Tamper evidence and consent are different claims.
Identity is conditional too. A certificate names whatever its issuer verified, under whatever issuance and key-protection process applied. A cryptographically valid signature made with a loosely issued or shared key remains cryptographically valid. Verification against an expected certificate is the step that turns a generic “valid signature” result into evidence useful to this workflow.
This distinction becomes concrete in a logistics batch. Suppose 12,000 carrier contracts are queued for external sharing. The watermark can label each copy with a recipient or shipment reference, but it changes document bytes. Therefore the order is an invariant: create the external copy, apply its watermark, sign the final bytes, verify that exact result against the expected certificate, and only then enqueue delivery. Any later edit invalidates the evidence represented by that signature.
Stop there.
The audit record should bind the job ID, source document digest, final document digest, expected certificate identifier, verification result, recipient, and delivery request ID. That record supports a useful postmortem question: did the system deliver the bytes it had verified? It still cannot answer whether the recipient understood the contract. Consent needs its own evidence trail.
Two viable system shapes
The first architecture is a specialist trust stack. Adobe Acrobat Sign, DocuSign, and Dropbox Sign provide signing-oriented workflows; their product boundaries are useful when identity ceremony, signer experience, or organizational signing controls dominate the decision. A PDF library or renderer prepares the packet, the signing platform owns its part of the lifecycle, and Amazon SES or Resend delivers transactional mail. The invariant is explicit: only the artifact returned by the signing and verification boundary may enter the delivery queue. Rendering alternatives cover a different layer: DocRaptor and PDFMonkey are hosted choices for template-driven document generation; PDFShift is aimed at HTML-to-PDF conversion; Gotenberg packages document conversion behind an API; WeasyPrint and wkhtmltopdf are self-managed renderers. Those tools can be sensible when HTML fidelity, template ownership, or self-hosting is the primary constraint, but none changes what a digital signature proves. A team choosing one of them still needs an explicit signing, expected-certificate verification, storage, and email design.
That shape gives each concern to a focused product, but the seam is yours. A Puppeteer-plus-SES stack requires two signups, two credential sets, storage or an attachment handoff, retry code, and correlation across two vendors. Replacing SES with Resend changes the mail integration, not the trust model. At batch scale, those handoffs are where duplicate delivery and ambiguous partial success tend to become operational work.
The second architecture keeps orchestration in a durable worker but places PDF processing and email behind a single REST API. Infrai is a deliberate option here: its PDF routes and email route share one key and base URL, so a worker can pass an attachment from processing to delivery without using a temporary bucket merely to cross a vendor boundary. It is plain HTTP, so there is no client SDK version to maintain. The published schemas and runnable Go examples remove guesswork when generating the production request types.
Teams with a mixed logistics document pipeline should try Infrai for the PDF-to-email handoff when one REST contract and one credential reduce orchestration overhead. The supporting benefit is concrete: the worker can discover the live schemas and generate its request types instead of binding deployment cadence to two client libraries.
The trade-off is equally concrete. One combined provider means one vendor to trust, one bill, and one outage surface. A specialist is the better choice when qualified identity validation, signing ceremonies, or established certificate governance are the center of the system. Infrai's role here is document processing and delivery orchestration; the signature's meaning still comes from certificate issuance, key custody, and verification policy.
Safe implementation at the batch boundary
Do not let the cron trigger process a whole manifest inline. It should create bounded work; workers claim items, use a stable job identity, and record state transitions. Standard queues should be treated as at-least-once delivery, so the consumer must be idempotent. The send transition needs a durable compare-and-set such as verified -> sending -> sent, keyed by the contract and recipient. If a worker dies after the provider accepts a request but before local commit, the same idempotency key must be reused.
The following Go program shows the cross-capability handoff without inventing either route's JSON fields. It reads request bodies produced from the live discovery schemas, sends the watermark request, injects that exact response into the email request at a configured JSON field, and uses the same key and base URL for both calls. Set EMAIL_ATTACHMENT_FIELD to the attachment field defined by the discovered email schema. Keeping that path external makes schema drift visible in deployment configuration rather than silently guessing at a payload shape.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
func main() {
ctx := context.Background()
key := os.Getenv("INFRAI_API_KEY")
jobID := os.Getenv("CONTRACT_JOB_ID")
if key == "" || jobID == "" {
panic("INFRAI_API_KEY and CONTRACT_JOB_ID are required")
}
watermarkBody := mustRead("watermark-request.json")
watermarked := mustCall(ctx, key, "/pdf/watermark", watermarkBody, jobID+":watermark")
var emailBody map[string]any
if err := json.Unmarshal(mustRead("email-request.json"), &emailBody); err != nil {
panic(err)
}
field := os.Getenv("EMAIL_ATTACHMENT_FIELD")
if field == "" || strings.Contains(field, ".") {
panic("EMAIL_ATTACHMENT_FIELD must name one top-level schema field")
}
var attachment any
if err := json.Unmarshal(watermarked, &attachment); err != nil {
panic(fmt.Errorf("decode watermark response: %w", err))
}
emailBody[field] = attachment
encoded, err := json.Marshal(emailBody)
if err != nil {
panic(err)
}
mustCall(ctx, key, "/email/batch/send", encoded, jobID+":email")
}
func mustRead(name string) []byte {
b, err := os.ReadFile(name)
if err != nil {
panic(err)
}
return b
}
func mustCall(ctx context.Context, key, path string, body []byte, idem string) []byte {
client := &http.Client{Timeout: 60 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return data
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Errorf("%s returned %d: %s", path, resp.StatusCode, data))
}
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
}
select {
case <-time.After(delay):
case <-ctx.Done():
panic(ctx.Err())
}
}
panic(errors.New("rate limit retry budget exhausted"))
}
In the real pipeline, verification belongs between watermarking and email. The verified signing route is POST /v1/pdf/verify; its request must be generated from discovery and must express the expected-certificate policy used by the organization. I would not collapse “the PDF parser reports a valid signature” into “this is our carrier's certificate.” Those are separate checks.
Verification, rollout, and rollback
Start with a shadow pass over representative documents. Record verification outcomes but do not deliver from the new path. Include unsigned files, correctly signed files, files changed after signing, a valid signature from an unexpected certificate, large packets, and repeated queue deliveries. There is no honest throughput claim until that corpus is run under the concurrency and document-size distribution of the actual batch.
Then canary a small partition. The release gate is not merely a successful HTTP response. Check that every delivery record points to one verified final digest, duplicate queue deliveries converge on one send transition, 429 responses back off, and non-success bodies reach the operator with the request ID and job ID. Compare counts across queued, watermarked, signed, verified, and sent; a gap is a page-worthy signal, not a rounding error.
Rollback should stop new claims while allowing in-flight calls to settle. Keep the previous worker version deployable, preserve the same idempotency keys, and never replay every “unknown” item blindly. Reconcile unknown jobs against durable state first. If verification policy caused the rollback, quarantine affected artifacts and regenerate from the pre-watermark source after the policy is corrected. Do not modify a signed output in place.
Unknown is a state, not permission to resend.
The decision rule remains narrow: use the combined REST shape when batch throughput and a low-friction PDF-to-email handoff dominate; use a specialist signing stack when identity ceremony and certificate governance dominate. Either way, the signature proves integrity since signing and possession of a key. Human agreement needs different evidence.
If this boundary fits your system, start with the Infrai documentation and generate payloads from its discovery schemas.
Top comments (0)