How to Get Alerted When a Client Re-Opens Your Proposal
The first open of a proposal doesn't mean much — it's a courtesy click, often minutes after your email lands. The signal that matters is the re-open: the client came back. They're comparing numbers, re-reading the scope, or showing it to whoever signs off. That's the moment a call actually lands.
And it's exactly the moment you never see. In this tutorial you'll build an alert that stays silent on the first view and fires the instant a client returns.
Put the proposal behind a recipient link
Upload the proposal as a tracked document and mint one link per client contact. The link carries their identity, so every visit downstream arrives with a name on it.
curl -X POST https://apdf.io/api/docs \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-d file="https://your-server.com/q3-services-proposal.pdf" \
-d name="Q3 Services Proposal"
curl -X POST https://apdf.io/api/docs/4352c-99928-25aa1/links \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-d name="Alex Moreau" \
-d email="alex@clientco.example"
{
"data": {
"token": "CMgHcYVC",
"name": "Alex Moreau",
"email": "alex@clientco.example",
"url": "https://docs.apdf.io/studio/4352c-99928-25aa1?v=CMgHcYVC",
"is_active": true,
"last_viewed_at": null,
"created_at": "2026-07-23T06:54:11.000000Z"
}
}
Send Alex the url — from here on, every visit
is a signal you can catch. Both steps also work in the dashboard.
Fire a webhook on every reading visit
The Document Opened trigger fires once per reading session — which makes each webhook exactly one visit. Wire it up:
- Go to Automations in the dashboard and click New Automation.
- Under “When this happens…” pick Document Opened. Scope it to your proposal or all documents.
- Under “Then do this…” paste your Webhook URL (step 4's receiver) and save.
Understand what arrives — once per visit
Every visit delivers the same payload shape: the document, the recipient, and the session. Note that the session block is young — this fires the moment they open, before any reading has happened:
{
"automation": {
"id": "b2423-a1c37-de039",
"name": "Proposal opened"
},
"document": {
"doc_id": "4352c-99928-25aa1",
"name": "Q3 Services Proposal",
"viewer_url": "https://docs.apdf.io/studio/4352c-99928-25aa1",
"file_pages": 3
},
"trigger": {
"matched_event": "document:loaded",
"conditions_evaluated": false
},
"event": {
"session_id": "bQf7sLw3Nc9dGh5j",
"viewer_id": "aMxV3nKj9qLp2wSe",
"recipient": {
"name": "Alex Moreau",
"email": "alex@clientco.example"
},
"data": null
},
"session": {
"total_duration_ms": 0,
"pages_viewed": 0,
"completion_rate": 0
},
"timestamp": "2026-07-23T06:54:42+00:00"
}
A first open and a re-open look identical on the wire — distinguishing them is your receiver's one job: count visits per document and recipient, and only act from visit two.
Alert only on the re-open
A complete receiver in plain Python — standard library only. It keeps a tiny visit counter on disk, stays silent on visit one, and posts to your Slack incoming webhook from visit two on:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.request import Request, urlopen
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/XXX/YYY/ZZZ"
STATE_FILE = Path("visits.json")
def visit_number(doc_id: str, email: str) -> int:
"""Count this open per document and recipient — 1 on the first visit, 2+ on re-opens."""
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
key = f"{doc_id}:{email}"
state[key] = state.get(key, 0) + 1
STATE_FILE.write_text(json.dumps(state))
return state[key]
class ReopenAlerter(BaseHTTPRequestHandler):
def do_POST(self):
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
doc = payload["document"]
recipient = payload["event"]["recipient"] or {}
visits = visit_number(doc["doc_id"], recipient.get("email", "unknown"))
if visits >= 2:
text = f"🔥 {recipient.get('name', 'Someone')} just re-opened \"{doc['name']}\" (visit #{visits}) — strike while it's hot: {doc['viewer_url']}"
urlopen(Request(SLACK_WEBHOOK_URL, data=json.dumps({"text": text}).encode(), headers={"Content-Type": "application/json"}))
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
HTTPServer(("0.0.0.0", 4790), ReopenAlerter).serve_forever()
python3 reopen_alert.py
Deploy it anywhere with a public URL and paste that URL into the automation from step 2. Alex's first open stays quiet; the moment he comes back, your channel hears:
🔥 Alex Moreau just re-opened “Q3 Services Proposal” (visit #2) — strike while it's hotWhere to go from here
The re-open is one play from a bigger book — the same signals power all of them.