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"
}
FieldWhat it isNeeds publishing
public_page_urlA hosted page showing the countdown, at /api/c/{id} for countdowns or /api/t/{id} for duration timers.Yes
website_embed_htmlA responsive iframe snippet, 300×250 at natural size, that scales to its container.Yes
email_embed_htmlA linked animated GIF that renders live remaining time when the inbox opens it. No JavaScript.Yes
json_data_urlThe 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 }),
});
Publishing is reversible. Sending 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.

If you need the countdown to match your own design exactly, use 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 openedWhat renders
RunningA 30-frame animated GIF — roughly 30 seconds of ticking, then it freezes
Paused or resetA single-frame GIF at the current remaining time
EndedWhatever 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 alt text 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.
Combined with personalized timers, this becomes a per-recipient deadline rendering that recipient's own remaining time at the moment they open the message — the same number they see in your app, because both read the same timer.

What happens after zero

All three display outputs follow the expiry you configured on the timer.

behaviorHosted page and embedEmail GIF
show_messageDisplays your message in place of the clockRenders the message
hideThe countdown disappearsRenders nothing visible
redirectVisitors go to redirect_urlThe 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 needUse
A link to send someonepublic_page_url
A countdown on a page you do not fully controlwebsite_embed_html
A countdown inside an email bodyemail_embed_html
A countdown that matches your own design systemjson_data_url, rendered yourself
A countdown in a native mobile appjson_data_url, through your own backend
Mixing them is normal and correct. A launch campaign might use the email GIF in the announcement, the website embed on the landing page, and the JSON endpoint inside the product — all pointing at one timer, so moving the deadline once moves it everywhere.