How to Track Who Read the Documents They Were Required to Read
“Required reading” usually means an email attachment and a prayer. When the auditor — or the incident report — asks who actually read the safety policy, the honest answer is a shrug: delivered isn't opened, opened isn't read, and a reply saying “done!” isn't evidence of either.
Tracked links fix the reading question; a one-checkbox acknowledgement form on the policy's last page fixes the signature question. Together they produce the thing compliance actually wants: a per-person roster of read and confirmed.
Put the signature inside the document
The policy PDF gets one extra page: an acknowledgement form with a checkbox and a name field. The field names become the payload keys, so keep them meaningful:
policy_acknowledged (checkbox) — "I have read and understood the Workplace Safety Policy 2026 (v3.1)."
full_name (text)
Upload it privately and mint one link per employee — the link is what turns anonymous page views into Jonas read pages 1–4:
curl -X POST https://apdf.io/api/docs/5ae87-45d34-6bb06/links \
-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/json" \
--data-urlencode "name=Ines Kowalczyk" \
--data-urlencode "email=ines@norda.example"
The acknowledgement that holds up
Employees read the policy in the browser and confirm on the last page — no printout, no separate survey tool. The submission arrives attributed and timestamped:
GET /api/docs/5ae87-45d34-6bb06/form-submissions
{
"doc_id": "5ae87-45d34-6bb06",
"creator_id": "NAYV1cyAyyuo",
"recipient": {
"name": "Ines Kowalczyk",
"email": "ines@norda.example"
},
"fields": [
{
"id": "18R",
"name": "policy_acknowledged",
"page": 5,
"type": "checkbox",
"value": true,
"checked": true,
"exportValue": "Yes"
},
{
"id": "24R",
"name": "full_name",
"page": 5,
"type": "text",
"value": "Ines Kowalczyk"
}
],
"created_at": "2026-07-23T09:50:13.000000Z",
"updated_at": "2026-07-23T09:50:13.000000Z"
}
And crucially, the same visit's reading session sits next to it: Ines viewed all 5 pages before checking the box. An acknowledgement backed by a recorded read is a very different artifact than a checkbox alone.
Build the compliance roster
Three endpoints — the link roster, the sessions, the submissions — joined by one script into the answer HR needs:
#!/usr/bin/env bash
# Compliance roster: who read the policy, who acknowledged, who needs a nudge.
API="https://apdf.io/api/docs/$DOC_ID"
H=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/json")
LINKS=$(curl -s "${H[@]}" "$API/links?per_page=100"); sleep 1
SESSIONS=$(curl -s "${H[@]}" "$API/analytics/sessions?per_page=100"); sleep 1
ACKS=$(curl -s "${H[@]}" "$API/form-submissions?per_page=100")
jq -n --argjson links "$LINKS" --argjson sessions "$SESSIONS" --argjson acks "$ACKS" -r '
($acks.data | map(select(.fields[] | select(.name == "policy_acknowledged" and .checked == true))
| .recipient.email)) as $acked |
($sessions.data | group_by(.recipient.email)
| map({ (.[0].recipient.email): (map(.completion_pct) | max) }) | add // {}) as $read |
$links.data[] |
.email as $e |
if ($acked | index($e)) then "\(.name): read \($read[$e])% · acknowledged ✓"
elif $read[$e] then "\(.name): read \($read[$e])% · NOT acknowledged"
else "\(.name): never opened"
end
'
$ DOC_ID=5ae87-45d34-6bb06 API_TOKEN=... ./policy-status.sh
Omar Haddad: never opened
Petra Simon: read 40% · NOT acknowledged
Jonas Petri: read 80% · NOT acknowledged
Ines Kowalczyk: read 100% · acknowledged ✓
Each line is its own follow-up: Jonas read the policy but skipped the checkbox (a one-line reminder), Petra stopped at the PPE section (a different conversation), and Omar's link was never clicked — check delivery before escalating.
Close the loop automatically
Run the roster script weekly from cron and mail the “NOT acknowledged” lines to team leads — or flip it to push: an automation on the form-submit event checks names off in your HR system the moment each acknowledgement lands. The forms tutorial wires that webhook end to end.
Where to go from here
The same building blocks, pointed at adjacent problems.
Related tutorials
Freelancers: A Proposal Workflow Without Per-Seat Tools
Proposal software is priced for sales teams; freelancers just need the signal. A one-person workflow on the free plan: links per contact reveal the 12-minute open, the internal forward, and the 21:21 return visit that camped on the price page — plus the three-slot pipeline trick and honest edges.
Ask Claude Which Deals Are Hot
The morning question isn't about one document — it's 'across everything, who should I call first?' With the workspace connected over MCP, the agent sweeps every document's sessions, ranks re-opens over deep reads over skims, and answers with evidence: the Arslans came back for the price page.
Real Estate: Share Exposés Securely and Spot Serious Buyers
Ten people request the exposé; one is a buyer. Per-prospect links tell them apart: a 22-second price peek is a looker, a return visit with 95 seconds on the floor plan and two minutes on the price page is a phone call — and the day it sells, one loop turns every circulating copy off.