Guides / Seal and stamp a PDF
Seal and stamp a PDF
A plain seal records a digest you computed; a stamped seal also puts the proof ON the document. You upload the PDF, Rox reserves the record identity, draws a compact seal stamp with a scannable QR onto the first page, hashes the stamped output, and seals that digest exactly like any other record. Anyone holding a printout can scan the stamp and land on the public verification page. Same uniform sealing contract as everywhere else: validated and either rejected immediately or accepted with 202, then exactly one terminal outcome, anchored or failed.
1 · Know what the server does with your file
- PDF only, 50MB cap. The upload must start with the %PDF- magic bytes; anything else, including encrypted PDFs, is rejected with 415. Image formats arrive in a later milestone.
- The digest covers the STAMPED bytes, not your upload. The server stamps first, then computes SHA-256 over the stamped output and seals that. The stamped file is therefore the document of record; your original upload will not verify against this record.
- The original is never persisted. It lives in request memory only. The stamped file is stored just long enough for you to download it (next step).
2 · Build and sign the multipart request
POST /v1/seal/stamp is multipart/form-data: text fields record_type (required), external_ref, metadata and field_hashes (both JSON-encoded text fields, both optional), plus the PDF under the field name file. Two things trip integrators up:
- The JWS signs the raw multipart bytes. The canonical input is RRv1\nPOST\n/v1/seal/stamp\n{sha256_hex(raw multipart body)}\n{ts}\n{nonce}, where the hashed bytes are the encoded multipart payload, boundaries and all, exactly as sent on the wire. Build the body yourself with a boundary you chose, hash it, sign it, send those exact bytes.
- X-RR-Timestamp is unix milliseconds. Sending seconds puts the timestamp far outside the 300 second window and every request fails with RR-1003; this exact mistake has burned a live integration. In Node it is Date.now() as is.
The Node SDK (@rox-register/sdk 0.1.1+) does all of this for you: sealStamp builds the multipart body itself as one buffer, hashes those exact bytes for the JWS, and sends them verbatim.
Integrating without the SDK? The raw request looks like this; the API reference has the equivalent curl.
Either way, send an Idempotency-Key: a retry with the same key returns the original record with Idempotent-Replay: true and a freshly presigned URL to the original stamped bytes. Because stamping is not byte-deterministic, replays are never digest-compared, so the key alone decides: use one key per document, never reuse a key for a different file.
3 · Store the 202, and download the stamped file NOW
The acceptance carries the usual seal fields plus two stamp-specific ones:
- record_id, reference, status (pending), and salt_hex: returned once, never again. Store the salt with the document; verification by digest needs it.
- content_digest_hex: the SHA-256 of the stamped bytes, computed server-side. This is the digest that gets anchored.
- stamped_file_url: a presigned download URL valid for 24 hours. The stored stamped file itself expires from Rox storage after 1 day, so download it promptly and keep it in your own systems; it is the file that verifies and the file you print. The RECORD and its proof are permanent regardless, only the file copy expires. If you miss the window, the record still anchors and verifies, but the stamped bytes are gone; stamp a fresh copy under a new record if you need the file again.
4 · What is on the page: the scan contract
The stamp is a compact seal card on the first page of the PDF: a QR code, the full public reference, the sealing method line and the scan domain. The QR encodes https://scan.roxregister.com/r/{reference}#s={salt_hex}. Scanning it lands on the public verification page for the record, verdict first, no account needed. The salt rides in the URL fragment: browsers never transmit fragments to servers, so the salt cannot appear in server or proxy logs; the scan page reads it client-side and uses it for the byte-exact file check. Placement is configured per record type (qr_position in the record type's verify fields, set in your Platform): bottom-right (default), bottom-left, top-right or top-left; it is not a request field.
5 · Wait for the terminal outcome
The 202 means validated and accepted, not anchored. The record anchors asynchronously and reaches exactly one terminal outcome: anchored (with anchor coordinates and proof) or failed. Consume it push-style via the record.anchored / record.failed webhooks (see the webhooks guide), or poll the record. Build on the outcome, not on any expectation of when it lands.
6 · Verify the stamped file, like a stranger would
Verification is public: no key, no login. Remember which bytes were sealed: the stamped output. Hash your stored stamped copy and check it with the salt, or just scan the stamp on a printout and drop the file on the verification page.
New to the sealing model? Start with your first seal for keys and the JWS basics. Then subscribe to webhooks so outcomes push to you, or commit per-field hashes with selective disclosure; the field_hashes text field works on stamping too. Need the document SIGNED before it is sealed and stamped? Run the whole ceremony with send a document for signature.