Webinar countdown API
A webinar is a deadline with a marketing job attached. It needs a countdown on the registration page, reminders at several points before it starts, a live countdown inside those reminder emails, and something that opens the room at zero. All four come from one timer — and if the webinar runs weekly, from one timer forever rather than one per week.
One-off or recurring
The first decision, and it changes everything downstream.
| One-off (fixed) | Weekly demo (recurring) | |
|---|---|---|
| Timers needed | One per event | One, forever |
| Against your monthly allowance | One per event | One, ever |
| Landing page embed | New URL each event | Same URL, always correct |
| Rules | Recreated per event | Attached once, with delivery: repeat |
| After it passes | Ends | Rolls to the next occurrence |
Creating the timer
A single webinar
Fixed timer
// A single webinar: one fixed timer.
const timer = await call("/timers", {
method: "POST",
headers: { "Idempotency-Key": `webinar_${webinar.id}` },
body: JSON.stringify({
name: webinar.title,
type: "fixed",
deadline_at: webinar.startsAt, // ISO 8601 with an offset
timezone: "Europe/London", // the zone it was scheduled in
publish: true,
expiry: {
behavior: "redirect",
redirect_url: webinar.roomUrl, // send latecomers straight in
},
metadata: { webinar_id: webinar.id },
}),
});A weekly demo
Recurring timer
// A weekly demo that runs every Thursday at 14:00 Berlin time.
// One timer, forever — not 52 timers a year.
const timer = await call("/timers", {
method: "POST",
headers: { "Idempotency-Key": "weekly_product_demo" },
body: JSON.stringify({
name: "Weekly product demo",
type: "recurring",
recurrence: {
frequency: "weekly",
days_of_week: ["thursday"],
local_time: "14:00",
},
// Occurrences are computed in this zone, so 14:00 stays 14:00
// through a daylight saving change instead of drifting.
timezone: "Europe/Berlin",
publish: true,
}),
});The reminder ladder
Four rules on one timer
// Reminders, then the room. Note the delivery setting.
const MILESTONES = [
{ name: "24 hours before", seconds: 86400 },
{ name: "1 hour before", seconds: 3600 },
{ name: "10 minutes before", seconds: 600 },
];
for (const milestone of MILESTONES) {
await call(`/timers/${timer.id}/webhook-rules`, {
method: "POST",
body: JSON.stringify({
name: milestone.name,
webhook_destination_id: DESTINATION_ID,
condition: {
field: "remaining_seconds",
operator: "less_than_or_equal",
value: milestone.seconds,
},
// "repeat" for a recurring webinar, "once" for a one-off.
// A "once" rule on a recurring timer fires on the first
// cycle and then never again.
delivery: webinar.recurring ? "repeat" : "once",
}),
});
}
await call(`/timers/${timer.id}/webhook-rules`, {
method: "POST",
body: JSON.stringify({
name: "Doors open",
webhook_destination_id: DESTINATION_ID,
condition: { field: "status", operator: "equals", value: "ended" },
delivery: webinar.recurring ? "repeat" : "once",
}),
});The delivery setting is the detail that decides whether a recurring webinar keeps working. A once rule fires on the first cycle and stays silent forever afterwards — which looks exactly like a broken integration a fortnight later.
Handling each milestone
One endpoint, four branches
async function handleWebinarEvent(event) {
const webinarId = event.timer.metadata.webinar_id;
const registrants = await db.registrants.forWebinar(webinarId);
switch (event.rule.name) {
case "24 hours before":
return sendReminder(registrants, "tomorrow");
case "1 hour before":
return sendReminder(registrants, "in an hour");
case "10 minutes before":
// The one that actually moves attendance.
return sendPushAndSms(registrants);
case "Doors open":
await db.webinars.update(webinarId, { roomOpen: true });
return sendJoinLink(registrants);
}
}Branching on rule.name rather than re-deriving thresholds from remaining_seconds keeps the logic in one place. The rule already decided what counts as “an hour before”; comparing numbers again is a second place to get it wrong.
Countdowns inside the reminders
A reminder that says “starts in 1 hour” is wrong the moment it sits unread. The GIF renders when the recipient opens it.
Reminder email
// Every reminder carries a live countdown, so an email opened
// three hours after it was sent shows three hours less.
const outputs = await call(`/timers/${webinar.timerId}/outputs`);
await sendEmail({
to: registrant.email,
subject: `${webinar.title} starts soon`,
html: `
<p>Hi ${registrant.firstName},</p>
<p>Starting in:</p>
${outputs.email_embed_html}
<p><a href="${webinar.roomUrl}">Join the room</a></p>
`,
});For a recurring webinar the same outputs stay valid across every cycle, because the timer never changes — the countdown simply points at the next occurrence. See embeds and hosted pages.
The registration page
Drop website_embed_html onto the landing page and the countdown is live, correct against the server clock, and immune to a visitor whose device clock is wrong. For a recurring webinar it keeps counting to whichever session is next, with nothing to update between cycles.
Setting expiry.behavior to redirect with the room URL also handles the person who arrives five minutes late — they land on the countdown page and are sent straight into the session rather than seeing a finished clock.
Common questions
One-off or recurring timer for a weekly webinar?
Recurring, almost always. One timer counts to the next occurrence, rolls over when it passes, and keeps the same ID and the same embed URLs forever. Creating a fresh fixed timer each week means 52 timers against your allowance, 52 IDs to track, and a landing page embed to update every week.
Why did my reminders stop after the first week?
The rules are set to "once". On a recurring timer that means they fire on the first cycle and never again. Change delivery to "repeat" and they become eligible on every subsequent occurrence. This is the single most common issue with recurring webinar setups.
What about attendees in different timezones?
A fixed deadline is one instant, so everyone counts to the same moment wherever they are. For a recurring webinar the timezone field does real work: occurrences are computed in that zone, so a 14:00 Berlin demo stays at 14:00 local through daylight saving rather than drifting to 13:00 in March.
Can the countdown redirect people into the room?
Yes — set expiry.behavior to "redirect" with the room URL. Anyone who lands on the hosted countdown page after the start time goes straight in, which is better than showing them a finished clock and letting them work out what to do next.
Related
Set up a webinar that starts in five minutes
Sandbox is free with any account. Create the timer, attach the ladder, and watch all four callbacks arrive in order.