Embeds, hosted pages, and email GIFs
GET /timers/{timer_id}/outputs returns every way to display a timer without building a front end for it: a hosted page, a responsive iframe, an animated GIF that works inside an inbox, and the JSON endpoint if you would rather render it yourself. One timer produces all four, and they all read the same server clock — which is the part that is genuinely difficult to assemble from separate tools.
The call
Request
curl https://countdownshare.com/api/v1/timers/$TIMER_ID/outputs \
-H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY"200 OK — a published Production timer
{
"data": {
"public_page_url": "https://countdownshare.com/api/c/550e8400-...",
"website_embed_html": "<iframe src=\"...\" width=\"300\" height=\"250\" ...></iframe>",
"email_embed_html": "<a href=\"...\"><img src=\".../api/email-countdown/550e8400-...?format=gif\" ...></a>",
"json_data_url": "https://countdownshare.com/api/v1/timers/550e8400-.../status"
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}| Field | What it is | Needs publishing |
|---|---|---|
public_page_url | A hosted page showing the countdown, at /api/c/{id} for countdowns or /api/t/{id} for duration timers. | Yes |
website_embed_html | A responsive iframe snippet, 300×250 at natural size, that scales to its container. | Yes |
email_embed_html | A linked animated GIF that renders live remaining time when the inbox opens it. No JavaScript. | Yes |
json_data_url | The status endpoint for this timer, for building your own interface. | No |
When three fields are null
The three display outputs are populated only for published Production timers. They are null otherwise — including for every Sandbox timer, always.
An unpublished timer
{
"data": {
"public_page_url": null,
"website_embed_html": null,
"email_embed_html": null,
"json_data_url": "https://countdownshare.com/api/v1/timers/550e8400-.../status"
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}
# Three nulls means the timer is not published, or you are in Sandbox.
# json_data_url is always present — it needs no publication.Two causes, in this order of likelihood: the timer is still a draft, or you are holding a cs_test_ key. Publishing is a Production-only capability, so no Sandbox timer will ever return a public URL no matter what you send.
Publishing
// Publish at creation…
await call("/timers", {
method: "POST",
headers: { "Idempotency-Key": "launch-2030" },
body: JSON.stringify({
name: "Product launch",
type: "fixed",
deadline_at: "2030-01-01T14:00:00Z",
publish: true,
}),
});
// …or later, with the current revision.
await call(`/timers/${timerId}`, {
method: "PATCH",
headers: { "If-Match": String(timer.revision) },
body: JSON.stringify({ publish: true }),
});publish: false takes the page and embeds offline while leaving the timer and its data intact — which is the right move for a campaign that needs to disappear quickly, rather than deleting it.The hosted page
A complete page at a public URL, with no key required. Send someone the link and they see the countdown — useful for a campaign that needs a destination but does not warrant building one.
The path differs by type: countdowns live at /api/c/{timer_id} and duration timers at /api/t/{timer_id}. Read the URL from outputs rather than constructing it, since the pattern is not part of the contract.
The website embed
An iframe snippet you can drop into any HTML — a CMS block, a landing page builder, a Shopify section, a WordPress post. It is 300×250 at natural size and scales to fill its container, so a wrapper with a width is usually all the styling it needs.
Because it is an iframe rendering our page, it stays in sync with the server clock and updates on start, pause, resume, and reset without you doing anything. The trade-off is that its appearance comes from the timer's configuration, not from your stylesheet — the API does not expose colour, font, or layout controls.
json_data_url and render it yourself. The status endpoint gives you the authoritative remaining time and the server clock to anchor against.The email GIF
This is the output most competing APIs cannot produce. Email clients strip JavaScript, so a live countdown in an inbox has to be an image — and it has to be generated at the moment the recipient opens the message, not when you sent it. What email_embed_html gives you is an animated GIF that does exactly that, wrapped in a link to the hosted page.
What email_embed_html contains
<a href="https://countdownshare.com/api/c/550e8400-...">
<img src="https://countdownshare.com/api/email-countdown/550e8400-...?format=gif"
width="600"
alt="Product launch"
style="display:block;max-width:100%;height:auto;border:0">
</a>The ?format=gif parameter is always present. The underlying endpoint can also serve a static PNG, but email_embed_html never links to it — an API-created timer's email output is a GIF every time, with no setting to change that.
Dropping it into an email
const { data: outputs } = await call(`/timers/${timerId}/outputs`);
await sendEmail({
to: recipient.email,
subject: "Sale ends soon",
html: `
<p>Only a little time left:</p>
${outputs.email_embed_html}
<p><a href="https://example.com/sale">Shop the sale</a></p>
`,
});
// The GIF is rendered when the inbox fetches it, not when you
// send. Open it tomorrow and it shows tomorrow's remaining time.How it behaves
| Timer state when opened | What renders |
|---|---|
Running | A 30-frame animated GIF — roughly 30 seconds of ticking, then it freezes |
Paused or reset | A single-frame GIF at the current remaining time |
Ended | Whatever the expiry behavior specifies |
What to expect in practice
- It works without JavaScript, which is why it renders in Outlook, Gmail, and Apple Mail alike.
- Clients that block images by default show your
alttext until the recipient allows images. Write the surrounding copy so the email still makes sense without the countdown. - The GIF reflects the state at fetch time. It cannot update after it has been rendered — a message opened twice fetches twice and shows the correct time both times.
- Pair it with a link. The GIF tells people the deadline is close; the link is what they act on.
What happens after zero
All three display outputs follow the expiry you configured on the timer.
| behavior | Hosted page and embed | Email GIF |
|---|---|---|
show_message | Displays your message in place of the clock | Renders the message |
hide | The countdown disappears | Renders nothing visible |
redirect | Visitors go to redirect_url | The GIF renders the ended state; the link still points where you put it |
Set this before a campaign goes out, not after. An expired countdown with no configured behaviour shows a finished clock and no explanation, and somebody always opens the email a day late. It is editable at any time — see update and delete.
Choosing an output
| You need | Use |
|---|---|
A link to send someone | public_page_url |
A countdown on a page you do not fully control | website_embed_html |
A countdown inside an email body | email_embed_html |
A countdown that matches your own design system | json_data_url, rendered yourself |
A countdown in a native mobile app | json_data_url, through your own backend |