Upload a new version of a document
Replace the file behind an existing document so every live link instantly serves the update.
Versions sit inside a document. The document id and every link pointing at it stay the same; only the file bytes change. Uploading a new version makes it primary in the same transaction, so the next viewer to open any existing link sees the new file. Older versions remain queryable and can be promoted back at any time.
Use this when you need to push a corrected deck, a redlined contract, or an updated pricing sheet without re-sharing URLs.
# 1. Find the document id (skip if you already have it)
papermark documents list | grep "Acme pitch"
# 2. Upload the replacement file as a new version
papermark documents versions add doc_abc123 ./pitch-v2.pdf
# → Version 2 added: dv_xyz789
# 3. Confirm the primary moved
papermark documents versions list doc_abc123The newly uploaded version becomes primary automatically. All
existing share links now serve pitch-v2.pdf on the next view.
To roll back to an earlier cut:
papermark documents versions promote doc_abc123 dv_oldver1The previous primary is demoted in the same transaction.
Same three-step upload as a fresh document — presign, PUT, register —
but the register step targets /v1/documents/{id}/versions instead
of /v1/documents.
TOKEN=pm_live_…
DOC_ID=doc_abc123
# 1. Get a presigned upload URL
UPLOAD=$(curl -sX POST https://api.papermark.com/v1/documents/upload-url \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"file_name":"pitch-v2.pdf","content_type":"application/pdf","content_length":2048576}')
UPLOAD_URL=$(echo "$UPLOAD" | jq -r '.upload_url')
UPLOAD_ID=$(echo "$UPLOAD" | jq -r '.upload_id')
# 2. PUT the file bytes to S3. Forward every key in
# required_headers exactly as returned — omitting one yields 403.
# Today the presign returns Content-Type + Content-Disposition;
# if more keys appear, forward those too.
HEADER_ARGS=()
while IFS=$'\t' read -r k v; do
HEADER_ARGS+=(-H "$k: $v")
done < <(echo "$UPLOAD" | jq -r '.required_headers | to_entries[] | "\(.key)\t\(.value)"')
curl -X PUT "$UPLOAD_URL" \
--upload-file ./pitch-v2.pdf \
"${HEADER_ARGS[@]}"
# 3. Register as a new version
curl -X POST "https://api.papermark.com/v1/documents/$DOC_ID/versions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"upload_id\":\"$UPLOAD_ID\"}"Alternative: skip steps 1–2 and have Papermark fetch the file from a public URL.
curl -X POST "https://api.papermark.com/v1/documents/$DOC_ID/versions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"source_url":"https://cdn.acme.com/pitch-v2.pdf"}'The response includes version_number and is_primary: true.
Conversion / pre-processing kicks off in the background — viewers
hitting the link in the next few seconds may briefly see the prior
rendering while the new one is built.
Required scope: documents.write. To list or fetch existing
versions: documents.read.
To roll back:
curl -X PATCH "https://api.papermark.com/v1/documents/$DOC_ID/versions/dv_oldver1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_primary":true}'The pitch deck at document id
doc_abc123needs to be replaced with the new file athttps://cdn.acme.com/pitch-v2.pdf. Upload it as a new version and tell me the version number.
The agent calls add_document_version with the document id and
source_url, then reports back the version_number so you can
roll back later if needed.
No public URL? Use the presigned flow. add_document_version
accepts an upload_id — the same handle upload_document takes —
so a private file never has to be exposed at a public link:
Here's the updated deck (attached). Push it as a new version of document
doc_abc123— don't create a new document.
Over the HTTP transport the agent chains
request_document_upload → PUTs the bytes straight to storage from
its code-execution sandbox → add_document_version with
document_id + upload_id. Over the stdio transport it can skip
the presign and pass file_path directly. Either way the bytes
never pass through the model context and nothing is published to a
public URL.
So add_document_version takes exactly one of upload_id
(a completed presigned upload), file_path (stdio only — the local
alternative to upload_id), source_url (public HTTPS URL the API
fetches), or external_url (repoint a link/notion document —
that one only, and only for those document types).
To roll back:
List the versions on document
doc_abc123and promote version 1 back to primary.
The agent chains list_document_versions → confirms the version id
with you → promote_document_version.
What viewers see
Anyone opening an existing share link after the new version is promoted lands on the new file. No re-share, no broken bookmarks. View analytics continue accumulating on the same link; per-version view counts are tracked separately if you need to know who saw which.
Versioning model in one paragraph
Each document has one primary version (is_primary: true) and
zero or more historical versions. Adding a new version always makes
it primary and demotes the previous primary in a single
transaction — there is never a window where two versions are
primary. Promoting an older version does the same swap in reverse.
Older versions are retained until the document itself is deleted.
Common pitfalls
- 403 on the S3 PUT: you dropped one of
required_headersfrom the presign response. Pass them all back verbatim. - The link still serves the old file: the rendered preview is cached for a few seconds while the new version is converted. Force a refresh, or wait 5–10 seconds.
- You meant to upload a different document: there's no in-place edit. Add another new version with the correct file — the wrong one stays in history but is no longer primary.