Most answers to “what is a webhook vs API” stop at the definitions. This one follows a single job from start to finish. You sent each contact on a deal their own link to a proposal, and you want to know the moment one of them opens it.
At Synergetics, Chip Bellfort handles this by sending at 11:47 p.m. and refreshing the link's analytics until about one in the morning. That is polling, performed by a person pushing a button.
Software can poll too, and for some jobs it should. This job has a better fit: HTMLvault sends a recipient.viewed event to your URL when the view happens. Below you'll find both approaches in plain terms, a side-by-side table, the event documented end to end, and its limits.
What is a webhook vs API, in plain terms
An API (application programming interface) is a set of addresses a program calls to read data or make a change. HTMLvault's REST API lets a script create links, update them and read their analytics. With an API, your code starts every exchange. To learn whether a prospect opened their link, your script asks, gets an answer, waits, and asks again. That loop is called polling.
A webhook works the other way round. You give HTMLvault a URL, and when a specific event occurs, HTMLvault sends an HTTP POST to that URL describing it. Nobody asks. The message was pushed because the event was triggered.
In proposal terms, polling is refreshing your email every fifteen minutes and a webhook is a notification. The difference is a delay of up to one polling interval, plus a bunch of clicks that come back empty.
Webhook vs API side by side
The table runs the proposal example through both approaches: a script that checks 30 proposal links every 15 minutes, and a webhook on the same links. Each row asks one question and answers it for both, and the bottom row is the takeaway: poll the API for reports and history, and use a webhook to start the next step the moment a view happens.
Who calls whom. With polling, your code sends a request authenticated with an API key and HTMLvault answers. With a webhook, HTMLvault makes the call and your endpoint answers. Every other row follows from that switch.
Speed and wasted calls. A poller only finds out about a view at its next check, so the worst-case delay equals the interval. Checking 30 proposal links every 15 minutes through an eight-hour day takes 960 requests and can still be 15 minutes late. Cut the interval to one minute and the delay shrinks, but the count rises to 14,400 requests, almost all of which return nothing new. A webhook has no interval to tune.
Failure. If a poller crashes, the next run asks again and catches up, because the data is still in HTMLvault. If a webhook receiver is down, the delivery has to be attempted again, so the same event can reach you twice. Webhooks: Real-Time Link Events for Your Automation Stack covers how deliveries and retries work. For this post, one consequence is enough: your handler must cope with repeats.
History. A webhook only reports what happens after you register it. The API can answer for any period your account still keeps, so backfill and reporting stay on the pull side.
How the recipient.viewed webhook works
What triggers it. A recipient link is a tracked link made for one named person, so each contact on a proposal gets their own URL and their own analytics. An AI tool connected through the MCP server (Claude, ChatGPT, Gemini or Cursor) creates them with the create_recipient_links tool. When someone opens one, HTMLvault fires recipient.viewed and sends it to your webhook URL.
Use it instead of link.viewed when the link belongs to a person. A link view tells you a page was opened. A recipient view tells you whose link it was, and a follow-up task needs that to reach the right contact and the right owner.
What the payload carries. Each delivery is a JSON body that names the event and identifies the recipient link that was viewed. Treat it as a trigger, not a report. The engagement detail lives in the link's analytics: total and unique views, repeat visits, scroll depth, time on page, country and city, device and browser, referrer and channel. You read it with get_analytics over MCP or through the REST API. Map fields from a real delivery, never from a guess. The receiver code below is written that way on purpose.
Plan. Webhooks start on Pro ($12 a month, or $10 a month billed annually), which allows up to five.
Wire recipient.viewed into a CRM task with Zapier or Clay
Most teams never write a receiver. They point the webhook at a tool that already has one. The goal in this webhook example: when a contact opens their proposal, a task appears in the CRM, assigned to the account owner and due today.
Zapier
- Create a Zap with the Webhooks by Zapier trigger and choose Catch Hook. Copy the URL Zapier generates.
- Register that URL as an HTMLvault webhook for
recipient.viewed. - Create a recipient link addressed to yourself and open it in a browser. A real delivery reaches Zapier, and the trigger step then lists the actual fields to map.
- Add a Filter step that continues only when the event name is
recipient.viewed. That keeps the Zap correct if other events are sent to the same URL later. - Add a CRM search step that looks for an open follow-up task for this contact, and continue only if none exists. This step makes the Zap safe to run twice.
- Add your CRM's create-task action. Put the recipient in the subject, set today as the due date, make the account owner the assignee, and add the link reference to the notes so the rep knows which proposal was opened.
Clay
Clay can receive the same delivery into a table through a webhook source, one row per event. From there it can enrich the row with company and owner data and send it on to your CRM. Use Clay when the follow-up needs context the event doesn't carry. Use Zapier when the task just needs to exist.
A minimal webhook receiver in code
If you'd rather run the receiver yourself, it's a single route. These rules apply in any language:
- Accept an HTTP POST with a JSON body at a public HTTPS URL.
- Acknowledge events you don't handle with a 2xx status and do nothing else.
- Build an idempotency key from the event (here, link plus recipient) and skip the work if that key was already handled. Idempotent means running it twice has the same effect as running it once.
- Do the work, record the key, then return 2xx. If the work fails, return 5xx and don't record the key.
- Take field paths from a real delivery. Never guess them.
// recipient.viewed receiver: Node 18+, Express
import express from "express";
const app = express();
const handled = new Set(); // use a database table in production
// Placeholders on purpose: copy each path from a real delivery.
const FIELD = {
event: "PATH_TO_EVENT_NAME",
link: "PATH_TO_LINK_ID",
recipient: "PATH_TO_RECIPIENT",
};
const read = (body, path) =>
path.split(".").reduce((value, key) => value?.[key], body);
app.post("/hooks/htmlvault", express.json(), async (req, res) => {
const body = req.body;
if (read(body, FIELD.event) !== "recipient.viewed") {
return res.sendStatus(204); // not this handler's event
}
const key = `${read(body, FIELD.link)}|${read(body, FIELD.recipient)}`;
if (handled.has(key)) {
return res.sendStatus(200); // repeat delivery or repeat open
}
try {
await createCrmTask({
subject: `Follow up: ${read(body, FIELD.recipient)} opened the proposal`,
due: new Date().toISOString().slice(0, 10),
});
handled.add(key);
res.sendStatus(200);
} catch (err) {
res.sendStatus(500); // failed: key stays unrecorded
}
});
app.listen(3000);
createCrmTask stands in for your CRM's API call. This key allows one task per recipient per proposal. If a return visit three days later should create a new task, add the date to the key. Replace the in-memory set with a database table before running this anywhere real, because a restart empties it.
Limits, and when to use the API instead
- The endpoint must be reachable from the internet. HTMLvault can't post to
localhostor to a server behind your VPN. Use the hosted URL Zapier or Clay gives you, deploy the receiver, or test through a tunnel that exposes a local port. - Handlers must be safe to run twice. A delivery can repeat, and a contact can open the link again. Either way, the second run should find its key and stop.
- A webhook has no memory. Opens from before you registered it never arrive, and a long outage on your side can leave gaps. Pull those once through the API and reconcile them against what your CRM already holds.
- Reporting is a pull job. The weekly view of which proposals were read, how far and for how long comes from per-link analytics, not from a pile of events.
- Pro allows five webhooks. Send several events to one receiver or Zap and route them inside it, instead of using up an endpoint per workflow.
The API covers history and reporting; recipient.viewed covers the moment itself. When a contact opens their proposal, the follow-up task is already in the CRM with an owner and a due date before the rep has opened a single dashboard.
