How to Share PDF Links That Expire — or Revoke Them After Sending
The moment a PDF leaves your outbox as an attachment, you've lost it. The recipient changes jobs, the offer deadline passes, the rate card gets superseded — and the old file keeps circulating with your name on it.
Share the same PDF through a tracked link instead and you keep a kill switch: revoke one person without touching anyone else, restore them later if you were wrong, and expire whole batches automatically when their time is up.
Make the document private
Revocation only means something if the links are the only way in. Every uploaded document also has a bare viewer URL — flip the document to private so that URL stops working and access flows exclusively through the links you mint:
curl -X POST https://apdf.io/api/docs/4352c-99928-25aa1/private \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
{
"data": {
"doc_id": "4352c-99928-25aa1",
"viewer_url": "https://docs.apdf.io/studio/4352c-99928-25aa1",
"file_url": "https://apdf-files.s3.eu-central-1.amazonaws.com/docs/202607/4d3f13346a61ba93070dc.pdf",
"name": "Q3 Services Proposal",
"file_size": 36340,
"file_pages": 3,
"is_public": false,
"created_at": "2026-07-23T06:54:11.000000Z",
"archived_at": null
}
}
With is_public: false, opening the bare
viewer_url returns a 403. Only recipient links
get through.
Mint one link per recipient
One shared URL gives you one shared kill switch — all or nothing. A link per person gives you a switch per person, plus per-person reading analytics for free:
curl -X POST https://apdf.io/api/docs/4352c-99928-25aa1/links \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-d name="Dana Whitfield" \
-d email="dana@clientco.example"
{
"data": {
"token": "YGY35sNP",
"name": "Dana Whitfield",
"email": "dana@clientco.example",
"url": "https://docs.apdf.io/studio/4352c-99928-25aa1?v=YGY35sNP",
"is_active": true,
"last_viewed_at": null,
"created_at": "2026-07-23T07:48:05.000000Z"
}
}
Send Dana the url; repeat for every stakeholder.
GET /api/docs/{docId}/links lists them
all with their is_active state and
last_viewed_at — your access roster for
the document.
POST /api/docs with a file or URL
— and your workspace needs a handle (set once in the dashboard), since it
becomes part of every viewer URL. The free plan includes 25 recipient links per
document — plenty for a deal's stakeholder list.
Revoke one person — not the whole deal
Dana leaves ClientCo mid-deal. Their inbox — and everything forwarded from it — shouldn't keep a working link to your pricing. One call:
curl -X POST https://apdf.io/api/docs/4352c-99928-25aa1/links/YGY35sNP/deactivate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
{
"data": {
"token": "YGY35sNP",
"name": "Dana Whitfield",
"email": "dana@clientco.example",
"url": "https://docs.apdf.io/studio/4352c-99928-25aa1?v=YGY35sNP",
"is_active": false,
"last_viewed_at": null,
"created_at": "2026-07-23T07:48:05.000000Z"
}
}
The effect is immediate and surgical: Dana's link now lands on a neutral “403 — Forbidden” page that reveals nothing — not even the document's name — while Alex's link keeps working untouched. And the history survives: every session Dana already had stays in your analytics, recipient attribution included.
Revoked the wrong person? POST
…/links/{token}/activate restores the same URL they already
have — nothing to resend. Only DELETE
…/links/{token} is forever.
Expire links on a schedule
Links don't carry an expiry date — you decide when time is up, which means any policy you can script. “Offer valid until July 31” is a single cron line that fires the deactivate call on August 1. And a standing “quotes die after 30 days” policy is this script on a daily cron:
#!/usr/bin/env bash
# Revoke every active link older than MAX_AGE_DAYS. Run it from cron once a day.
API="https://apdf.io/api/docs/$DOC_ID/links"
CUTOFF=$(date -u -d "-$MAX_AGE_DAYS days" +%Y-%m-%dT%H:%M:%S)
curl -s "$API" -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/json" |
jq -r --arg cutoff "$CUTOFF" '.data[] | select(.is_active and .created_at < $cutoff) | .token' |
while read -r token; do
curl -s -X POST "$API/$token/deactivate" \
-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/json" > /dev/null
echo "Revoked $token"
done
It reads the roster, picks the links that are still active but past the cutoff,
and flips each one off — already-revoked links are skipped, fresh ones left
alone. Expired recipients who come back knocking are a signal too:
last_viewed_at tells you who was still reading
when the door closed.
Where to go from here
Your links are now doors you control — next, watch who walks through them.