Document Timestamp
This walkthrough takes an existing PDF through preparation, a timestamp request, and validation. The API receives a digest and returns a timestamp response. The downloadable script handles PDF preparation and token embedding.
A document timestamp provides evidence that the covered document state existed at the TSA's asserted time. It does not identify a human signer.
To timestamp a signature while creating it, see Signature Timestamp.
1. Set up access and tools
Obtain your client ID and allowlist your server's public outbound IP in eID Easy admin: My Webpages → EDIT → Allowed IP addresses. See the original timestamping guide for the access instructions and Postman example.
You need Python 3.10+, OpenSSL 3+ available as openssl, curl 7.76+, and an unencrypted PDF with at least one page.
Download timestamping-examples.zip and extract it. It contains timestamp_pdf.py and a sample original.pdf; you can also use your own PDF. In the extracted directory, install the PDF library:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'pyhanko==0.35.1'
2. Prepare the PDF
python timestamp_pdf.py prepare original.pdf work
Use a new directory for work. This creates:
work/prepared.pdf: the PDF with an invisible document timestamp field and reserved token space.work/prepared.json: its SHA-256 digest, byte ranges and reservation metadata.
Keep these files unchanged until completion. The digest covers the prepared PDF's /ByteRange, excluding the reserved /Contents value. Hashing the original PDF or the entire prepared PDF produces the wrong digest.
The script uses an incremental update to retain existing PDF bytes. If the PDF already has signatures, also validate those signatures and their modification permissions.
3. Generate and send the request
python timestamp_pdf.py request work
This creates work/request.tsq: a binary RFC 3161 request using SHA-256, requesting the TSA certificate, and omitting the optional nonce. To inspect it:
openssl ts -query -in work/request.tsq -text
Send it using your configured client ID:
export CLIENT_ID='YOUR_CLIENT_ID'
curl --silent --show-error --fail-with-body \
"https://id.eideasy.com/api/timestamp/${CLIENT_ID}" \
--header 'Content-Type: application/octet-stream' \
--data-binary @work/request.tsq \
--output work/response.tsr
The body must be the binary request file. Stop if curl fails. The saved response may contain a JSON error instead of a timestamp; check its message and Case ID before continuing.
4. Validate the response and embed the token
python timestamp_pdf.py complete work
The script includes the public SK TSA root and intermediate from SK ID Solutions, so no separate certificate files are needed. It checks the TSA status, digest, signature, certificate chain and token capacity, then creates:
work/timestamp-token.der: the extracted CMS timestamp token.work/timestamped.pdf: the completed PDF.
The full HTTP response is a TimeStampResp wrapper. Only its CMS token belongs in the PDF's /Contents; the script inserts it without changing the covered bytes. Do not embed the full response or Base64 text, or rewrite the PDF after preparation.
5. Validate the completed PDF
python timestamp_pdf.py validate work/timestamped.pdf
For a successful result, expect intact=True, valid=True, trusted=True, and coverage=SignatureCoverageLevel.ENTIRE_FILE. Validation checks the newest document timestamp using the bundled TSA chain and available trust store, and may fetch revocation information.
Open work/timestamped.pdf in a PDF viewer and inspect its Signatures panel. A digest mismatch means the covered bytes differ. If the TSA is unknown, configure trust in your validator; the script also accepts optional --ca-file and --chain-file arguments. A document timestamp alone does not establish long-term validation or qualified status.
Troubleshooting
| Problem | What to check |
|---|---|
| Request denied | Client ID and public outbound IP allowlist. |
| HTTP 500 | Error message and Case ID in work/response.tsr; stop before completion. |
| Digest mismatch | Use the matching prepared PDF, metadata, request and response; do not change covered bytes. |
| Token too large | Prepare again in a new directory with --token-capacity 32768, then request a fresh timestamp. |
| Unknown TSA or missing revocation data | Trust roots, intermediate certificates and OCSP/CRL access. |
grantedWithMods | Inspect the response and policy before using complete --accept-mods. |
Optional: use a prepared PDF or an existing digest
To skip preparation, download prepared.pdf and its matching prepared.json into work, then continue from step 3. The expected digest and byte ranges are in the JSON file.
If another component already supplies the prepared PDF's Base64 SHA-256 digest:
python timestamp_pdf.py request work --digest-base64 'YOUR_BASE64_SHA256_DIGEST'
SHA-256 is 32 raw bytes or 44 characters in padded Base64. Decode the digest text rather than hashing it again. The digest must belong to the exact prepared PDF you will complete.