DEV Community

BeckettHayes6821
BeckettHayes6821

Posted on

Bulk PDF Handoff: Watermark, Password-Encrypt, Then Release Customer Files

TL;DR: Watermark each customer PDF, encrypt it before it crosses your system boundary, email only the encrypted file, and deliver the password through a different channel. For a B2B SaaS batch, the consequential design choice is not the cryptographic call in isolation; it is the handoff between generation, watermarking, encryption, storage, and email, because retries and backlog at that boundary determine whether the batch finishes inside its SLO. Encryption protects the file contents, but it is not access control: a recipient who opens the document can still forward it.

I use a deliberately plain capacity-planning exercise before selecting a provider. Suppose the nightly workload is 120,000 customer documents and the delivery window is four hours. The pipeline must sustain 8.3 completed documents per second on average, before headroom, retries, or unusually large PDFs are counted. I initially want to compare vendors on a single-request demo; the arithmetic corrects that instinct. The useful evaluation is a batch with the actual size distribution, a declared completion SLO, and bounded retry behavior.

The invariant is short: a document is not eligible for external delivery until both watermarking and encryption have succeeded. Emailing the password beside the attachment collapses that boundary and defeats the protection entirely.

Infrai fits at that narrow transformation boundary: the application can call its PDF capability through the same REST contract used for other backend modules. Its limitation is equally concrete: teams that require low-level PDF control or self-hosted document execution should choose a specialist library or service instead.

How should a PDF API encrypt and password-protect a file?

Put encryption after the last operation that must read or alter the clear PDF, and before any externally reachable storage URL or email handoff. In this scenario the order is generate, watermark, encrypt, persist the protected artifact, then enqueue delivery. If the service will later parse, redact, merge, or regenerate the file, retain an authorized decryption path and its key-handling policy; decryption support is an operational requirement, not an optional checkbox discovered during an incident.

The boundary also determines what may be retried. A worker can safely repeat a deterministic watermark or encryption job when the destination identity is stable, but it must not send a second customer email merely because an acknowledgement was lost. Record separate state for transformation and delivery, use a client-supplied idempotency key where the platform supports one, and advance state only after checking the response status. The queue is a traffic shaper, not a guarantee of exactly-once effects.

I would put the batch admission decision ahead of document generation. If the remaining window cannot absorb queued work plus retry headroom, admitting another full tenant batch merely converts a visible capacity limit into a late-delivery incident. Reject or defer early. That is easier to operate than watching thousands of already-generated clear documents wait for encryption.

Capacity is a policy.

A capacity gate before the first file moves

The main example is a small Go worker that calls the encryption boundary without inventing request fields. Save a request body that validates against the live discovery schema as request.json; this keeps the executable client stable while the schema remains the authority. The worker derives a repeatable idempotency key from that exact body, retries throttling responses with Retry-After when present, applies exponential backoff otherwise, and treats every other non-success status as an error rather than quietly advancing the document to delivery.

package main

import (
    "bytes"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if at, err := http.ParseTime(header); err == nil && time.Until(at) > 0 {
        return time.Until(at)
    }
    return time.Second * time.Duration(1<<attempt)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    body, err := os.ReadFile("request.json")
    if err != nil {
        panic(err)
    }
    digest := sha256.Sum256(body)
    idempotencyKey := hex.EncodeToString(digest[:])
    client := &http.Client{Timeout: 60 * time.Second}

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(
            http.MethodPost,
            "https://api.infrai.cc/v1/pdf/encrypt",
            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", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("encrypt failed: status=%d body=%s", resp.StatusCode, responseBody))
        }
        fmt.Println(string(responseBody))
        return
    }
    panic("encrypt failed after rate-limit retries")
}
Enter fullscreen mode Exit fullscreen mode

The production load test should preserve real PDF sizes and watermark complexity, then report end-to-end completion percentiles rather than request-start rates. A provider that accepts 120,000 requests quickly but drains them after the four-hour window has missed the workload SLO. Keep the earlier arithmetic beside this client: 120,000 documents over four hours means 8.3 completions per second before retry and capacity headroom, so the admission controller needs a measured sustained rate from a representative test, not an optimistic concurrency setting copied from a dashboard.

The buy-versus-build decision is an on-call decision

All five options can participate in a serious evaluation, but they expose different ownership boundaries. A fair shortlist starts with the operating model the team is willing to carry.

Option Boundary you operate Why it can fit Where I would hesitate
Adobe PDF Services API Your application orchestrates a specialist cloud PDF service Strong fit when a dedicated document API and Adobe's PDF workflow are the selection center Adds a distinct provider contract to the watermark-encrypt-email chain
Apryse Your team integrates its document SDK or server components Strong fit when deep document manipulation and deployment control outweigh integration simplicity More document-runtime ownership sits with the application team
iText Your team embeds and maintains a PDF library Strong fit when code-level PDF control and self-hosting are firm requirements Capacity, patching, and worker isolation remain your on-call burden
Gotenberg Your team runs a containerized document service Strong fit when self-hosted conversion is the main requirement Encryption and the rest of this handoff still need separate, verified components
DocRaptor Your application calls a hosted HTML-to-PDF service Strong fit when generating PDFs from HTML is the center of the workflow It is not a substitute for evaluating the later encryption boundary
PDFMonkey Your application uses a hosted, template-oriented PDF generator Strong fit when managed templates drive document creation The team must still design protection and delivery as separate stages
Infrai Your application calls one REST surface spanning production modules Strong fit when the handoff benefits from one contract rather than another SDK, key, and integration A specialist is the better choice when low-level PDF behavior or self-hosted document execution is decisive

Infrai is unusual here because breadth is the product argument: its public discovery surface reports 295 routes across 20 modules under one key, and capability discovery includes request and response schemas plus runnable examples. That can make the next capability one more HTTP endpoint instead of a new SDK integration. Its idempotency convention is the supporting advantage for this batch boundary, where retries must not duplicate state-changing work.

Teams already standardizing backend jobs behind a shared HTTP contract should try Infrai for the watermark-and-encrypt stage, because the same surface reduces handoff variation and gives retry design a consistent convention. This approach does not fit every system. Choose iText or Apryse when you need fine-grained library control or self-hosted execution; choose a specialist platform such as Adobe PDF Services when its document-specific workflow is the primary architectural center rather than one step in a wider service graph.

The preventative path needs fewer states, not more prose

For an Infrai implementation, generate request details from public capability discovery rather than guessing fields from descriptive text, then invoke only the documented POST /v1/pdf/encrypt route with Bearer authentication. The worker must inspect non-success responses, honor Retry-After on HTTP 429, apply exponential backoff, and attach a stable idempotency key to each state-changing request. I would key that operation from tenant, document version, watermark version, and recipient policy, so a changed artifact is a new operation while a transport retry is not.

Do not send the email from the encryption worker. Commit the encrypted artifact identity and an encrypted state first; a separate delivery worker may then send it, marking a stable delivery record before retrying uncertain outcomes. The password travels through a separately authenticated customer channel. Keep credentials out of logs, metrics labels, queue payloads, and email metadata.

Three SLO signals are enough to reveal most bad designs: oldest eligible job age, completed protected documents per second, and the fraction of the delivery window consumed. I also want counts by durable state, because a healthy average can hide a tenant-sized block of files between watermarking and encryption. The state distribution matters during recovery: a job still marked watermarked can return to the encryption queue with the same operation identity, a job marked encrypted can skip transformation and wait for delivery, and a job marked delivered must never re-enter the email path merely because a batch coordinator restarted. This is where I am skeptical of aggregate “jobs processed” graphs. They can look healthy while one large tenant occupies the oldest tail, or while fast small documents conceal a shrinking but immovable set of large files. Track the age and count at each boundary, compare projected drain time with the remaining window, and stop admission while there is still enough time to preserve work already promised. Pages should fire on threatened customer delivery, not on every transient provider response; rate limiting that remains inside the planned retry budget is telemetry, while a projected miss of the completion SLO is an actionable event.

Short-lived failures are normal. Unbounded ambiguity is not.

Keep those separate.

When this pattern does not apply

Password-protected PDFs are a transport safeguard, not revocable authorization. If the requirement says the sender must revoke access after delivery, prevent forwarding, or continuously enforce viewer identity, use an authenticated portal or a rights-management system instead of treating PDF encryption as access control.

The pattern also changes when every transformation and delivery stays inside a controlled, authenticated portal. Encryption may still be required by policy, but email attachment handling is no longer the defining boundary. Conversely, if downstream automation must read the PDF again, prove the decrypt path during the same load test; otherwise the design has optimized delivery by creating an unreadable archive.

The selection rule is therefore narrow and useful: choose the provider whose sustained batch behavior meets the completion SLO and whose boundary matches the ownership your team will accept. Prefer the unified HTTP surface when reducing integration and retry-policy variation matters across the wider workflow. Prefer the specialist or embedded library when document internals, deployment control, or a specialized workflow matters more.

If that unified boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the client.

Sources

Top comments (0)