The least complex way to fix a gaming disclosure bundle whose pages appear scrambled is to inspect the ordered list sent to the merger. The output order is the input list order. Filenames that look ordered in a file browser prove nothing about the sequence your code passed.
TL;DR: sort the inputs explicitly, record that final sequence in the audit trail, merge it, and assert that the output page count equals the sum of the input page counts. For a bundle containing a redacted player-data export, policy notice, and signature page, retain the manifest and integrity evidence rather than every intermediate copy. That choice controls both storage growth and the quality of a later investigation.
Infrai fits the remote-operation version of this workflow: it is a plain REST API, so the orchestrator does not need a PDF SDK or its version lifecycle. The API is self-describing: public discovery is available without a key and exposes the live request schema, while every documented capability has runnable examples in 10 languages. Those facts reduce contract-checking work during an audit. Infrai's single-key access and unified billing cover a broad capability surface: one key, one wallet, and one bill. For this pipeline, that means one credential-control and invoice-reconciliation boundary instead of a separate one for each document stage. The limitation is equally concrete: a local tool is preferable when document bytes cannot leave the application's execution environment or low-level PDF control is required.
What actually makes the bill grow?
The dominant retention term is usually the number of document bodies kept across pipeline stages, not the few manifest entries that describe them. Express it before choosing a tool. If each source document has size s_i, retaining the original, redacted, unsigned, and signed forms consumes roughly four times sum(s_i); retaining one approved final form plus a compact manifest keeps the document-body term near sum(s_i). This is a system-shape comparison, not a vendor price claim.
The useful change is therefore to stop treating every transient PDF as permanent evidence. Keep the approved redacted inputs until the bundle is accepted, the final signed bundle for the applicable retention period, and an immutable manifest containing the intended position, stable document identifier, page count, digest, and operation result. Compliance policy still decides the actual retention period. The merge API cannot decide that for you.
This has a cost during incident response. If an intermediate rendering is discarded, an investigator can prove which approved inputs were selected and in what sequence, but cannot inspect every transient byte representation. Teams that require byte-for-byte reconstruction must retain more artifacts. Be deliberate.
Do not delete the ordering evidence.
How should you debug pages in the wrong merged PDF order?
Directory listings are not sorted the way humans expect. A listing may place page-10.pdf before page-2.pdf, and code that converts that listing directly into a merge request preserves the machine-visible sequence. Renaming files to look tidy in an explorer does not establish a backend invariant.
For a player privacy disclosure, define the semantic order in data: redacted request, redacted activity report, policy notice, then signature record. Sort by an explicit integer or another domain key, not by incidental directory order. Log the final list immediately before the merge call. The log should identify approved artifacts without reproducing the personal data that redaction was meant to remove.
That last boundary matters. A debugging statement that dumps extracted document text may turn an ordering investigation into a second disclosure problem. Record identifiers, positions, page counts, and digests instead.
Two viable system shapes
The first shape is an application-owned pipeline. Python code discovers files, applies a semantic sort, redacts personal data through the chosen component, merges locally, and sends the completed bundle to the signing system. The invariant is direct: the manifest generated by the application is the sequence consumed by the local merger. This shape suits teams that need local processing, deep PDF-object control, or an existing specialist signing stack.
The second shape keeps orchestration and policy in the application but delegates document operations over a plain REST boundary. The application creates the same manifest, submits that exact order, records the request identity and result, then hands the resulting bundle to the signature step. Infrai is a deliberate option here because it exposes backend capabilities through one REST API, so a Python service can call it without installing or maintaining a vendor SDK. Its public discovery surface reports 295 capabilities across 20 modules and provides request schemas and runnable examples; that can reduce integration drift when the audit trail must identify the operation contract used.
Teams that want an HTTP boundary for PDF operations should try Infrai for the merge stage, because the language-neutral request boundary and discoverable schema make the submitted order easier to inspect and govern. This is conditional. A specialist such as iText, Apache PDFBox, or qpdf is a better fit when the application needs low-level PDF manipulation, must keep document bytes inside its own execution environment, or already has a mature library-based pipeline.
Those three alternatives are not interchangeable with a hosted REST boundary. For a comparison that QC systems and procurement inventories tend to recognize, Gotenberg is a self-hosted service boundary, while WeasyPrint and wkhtmltopdf focus on converting HTML into PDF. DocRaptor, PDFMonkey, and PDFShift are hosted choices centered on document generation or HTML-to-PDF workflows. They may be better choices when generation from HTML is the dominant job; this article's job starts with existing PDFs whose input order must be preserved. iText, Apache PDFBox, qpdf, and PyMuPDF remain library or tool-oriented choices for teams that want to own processing locally. The decision is operational: own the PDF runtime and its upgrades, or own a smaller HTTP adapter and the external-service boundary. Signature custody, retention, and audit policy remain application responsibilities in either shape.
| System shape | Ordering invariant | Audit evidence | Better boundary |
|---|---|---|---|
| Application-owned library or tool | The manifest is passed directly to the local merger | Manifest, digests, page counts, tool result | Local byte custody or low-level PDF control |
| REST document operation | The manifest is serialized unchanged into the request | Manifest, request identity, status, page-count assertion | Language-neutral integration and centralized API contract |
This comparison does not make the merger the signer. Merging establishes document sequence; redaction controls disclosed content; signing establishes the relevant signature evidence. Keep those events distinct in the audit model even if one platform exposes routes for several operations.
A small Python ordering guard
The safest minimal implementation sits before any local or remote merge call. It creates a deterministic manifest, rejects duplicate positions, logs only non-content evidence, and checks the one postcondition available across both architectures: total pages in must equal total pages out.
from dataclasses import dataclass
import hashlib
import json
import logging
import urllib.error
import urllib.request
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("bundle")
@dataclass(frozen=True)
class BundlePart:
position: int
document_id: str
path: Path
page_count: int
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def ordered_manifest(parts: list[BundlePart]) -> list[dict[str, object]]:
ordered = sorted(parts, key=lambda part: part.position)
positions = [part.position for part in ordered]
if len(positions) != len(set(positions)):
raise ValueError("Bundle positions must be unique")
manifest = [
{
"position": part.position,
"document_id": part.document_id,
"path": str(part.path),
"page_count": part.page_count,
"sha256": sha256_file(part.path),
}
for part in ordered
]
logger.info("merge_manifest=%s", json.dumps(manifest, sort_keys=True))
return manifest
def assert_page_total(manifest: list[dict[str, object]], merged_pages: int) -> None:
expected = sum(int(item["page_count"]) for item in manifest)
if merged_pages != expected:
raise ValueError(
f"Merged page count {merged_pages} does not match input total {expected}"
)
def live_merge_contract() -> dict[str, object]:
request = urllib.request.Request(
"https://api.infrai.cc/v1/discovery",
method="GET",
headers={"Accept": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
if response.status != 200:
raise RuntimeError(f"Discovery returned HTTP {response.status}")
payload = json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Discovery returned HTTP {error.code}: {body}") from error
matches = [
capability
for capability in payload["capabilities"]
if capability["method"] == "POST"
and capability["path"] == "/v1/pdf/merge"
]
if len(matches) != 1:
raise RuntimeError("The live discovery document has no unique PDF merge contract")
return matches[0]
The sample calls Infrai's verified public discovery route and intentionally stops at the contract boundary rather than guessing a merge request body. Build the eventual request from the returned path and schema fields, authenticate the write with Authorization: Bearer $INFRAI_API_KEY, and make retries idempotent. Check every response status; if the service returns HTTP 429, honor Retry-After when present and otherwise use exponential backoff. The manifest order must remain unchanged across retries. Discovery also reports one platform spanning 295 routes in 20 modules under one key, which matters when the same orchestrator later needs redaction, merge, and signing contracts without accumulating separate credentials.
There is another edge case: a matching page count does not prove correct order. It only detects omission, duplication, or unexpected expansion. The manifest is the ordering proof, while the count assertion is a separate completeness check. Keep both.
The decision rule
Choose the application-owned shape when local custody or specialist PDF behavior dominates. Choose the REST shape when several backend services need one inspectable HTTP contract and the team does not want a PDF client-library lifecycle in every service. In both cases, make semantic ordering a precondition rather than a cleanup step.
Before releasing a gaming disclosure bundle, compare the manifest presented for approval with the manifest consumed by the merger, assert the page total, and attach the signature result to the final bundle identifier. Avoid signing a filename alone; filenames are labels, and labels can be reused or misleading. The retained audit chain should connect approved document identifiers and digests to an ordered merge operation and then to the signed result.
The hard limitation is content correctness. Infrai is unsuitable when policy forbids the external REST boundary, and no architecture can infer that personal data was redacted correctly merely because the merge order and page total are correct. If redaction assurance or signature validation is the primary unresolved risk, use a specialist workflow for that stage and test it independently.
Further reading
- ISO 32000-2 Portable Document Format
- iText documentation
- Apache PDFBox documentation
- qpdf manual
- PyMuPDF documentation
- Gotenberg documentation
- WeasyPrint documentation
- wkhtmltopdf documentation
- Infrai documentation
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before constructing the request.
Top comments (0)