Retrieve remaining countdown time

GET /timers/{timer_id}/status is the endpoint you call when you render. It computes the remaining time on the server from an authoritative clock, which means a browser with the wrong system time, a phone that was asleep for an hour, a server-rendered page, and an email GIF all agree on the same number. That agreement is the reason to use a countdown API at all rather than subtracting two dates in JavaScript.

The call

No parameters and no body. It is a cheap read that does not count against your timer allowance — only against the per-minute rate limit.

Request

curl https://countdownshare.com/api/v1/timers/$TIMER_ID/status \
  -H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY"

200 OK

{
  "data": {
    "status": "scheduled",
    "server_time": "2029-12-28T14:00:00.000Z",
    "target_at": "2030-01-01T14:00:00.000Z",
    "timezone": "UTC",
    "remaining": {
      "total_seconds": 345600,
      "days": 4,
      "hours": 0,
      "minutes": 0,
      "seconds": 0
    },
    "progress": 0.87,
    "ended": false
  },
  "request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}
FieldTypeMeaning
statusenumscheduled · running · paused · ended. The live clock state.
server_timestringOur clock at the moment of the response. Anchor your local ticking to this.
target_atstring | nullThe instant being counted toward. Null for an unstarted duration timer.
timezonestringThe timer's display zone.
remaining.total_secondsintegerSeconds left. 0 once ended.
remaining.days / hours / minutes / secondsintegerThe same value broken down, so you do not have to.
progressnumberHow far through the countdown, 0 to 1.
endedbooleanTrue once the countdown has finished. Test this, not total_seconds.

The four status values

This is the live clock state, and it is not the same thing as the timer's status field of draft, published, or archived — that one describes publication.

scheduled

Not counting yet. A fixed timer whose deadline is in the future, or a duration timer that has never been started.

running

Counting down right now.

paused

A duration timer that was paused. Remaining time is frozen and will resume from exactly there.

ended

Reached zero. remaining.total_seconds is 0 and ended is true.

Branch on ended rather than on remaining.total_seconds === 0. They agree today, but the boolean is the field that carries the meaning, and it stays correct if the numeric representation ever gains precision.

Why not just subtract two dates

Because Date.now() is the visitor's clock, and the visitor's clock is not yours.

The failure mode

// The bug this endpoint exists to prevent.
//
// Date.now() is the VISITOR'S clock. It can be wrong by minutes,
// wrong by years, or deliberately changed to beat your deadline.
const remaining = deadline - Date.now();   // <- do not trust this

In practice this breaks in three distinct ways. Ordinary drift on a machine with no NTP sync produces a countdown that is minutes out. A device that was asleep resumes with a stale clock and jumps. And on anything with money attached, a visitor who sets their system clock forward walks straight past a deadline that only existed in their browser.

Reading the deadline from a server does not fix this on its own — you also have to compare it against a server clock. That is why server_time is in the response: it lets you measure the skew between the two clocks once and count against a corrected baseline.

Rendering without hammering the API

You do not need a request per second. Fetch the authoritative time once, measure the skew, then tick locally. The UI stays smooth at 60fps and you spend one request.

Fetch once, tick locally

// Fetch once, then tick locally against an offset.
// One request, a smooth UI, and the server stays the authority.

async function startCountdown(timerId, onTick) {
  const response = await fetch(`/api/countdown/${timerId}`);  // your proxy
  const { remaining_seconds, server_time } = await response.json();

  // How far the visitor's clock is from ours, measured once.
  const skew = Date.now() - new Date(server_time).getTime();

  const endsAt = Date.now() - skew + remaining_seconds * 1000;

  const id = setInterval(() => {
    const left = Math.max(0, Math.round((endsAt - (Date.now() - skew)) / 1000));
    onTick(left);
    if (left === 0) clearInterval(id);
  }, 1000);

  return () => clearInterval(id);
}

How often to re-sync

SituationApproach
A page loadFetch once on mount. Local ticking from there is accurate enough for any human-visible countdown.
A long-lived tabRe-sync on the visibilitychange event. A tab restored after hours is the case worth handling.
A duration timer someone else can pauseRe-sync every 15–30 seconds, or use a webhook rule so you are told instead of asking.
Anything transactionalNever trust the client value at the decision point. Re-check server-side before honouring the offer.
The last row is the one that matters. Client-side countdowns are presentation. The moment a countdown gates something real — a discount, a booking, an auction bid — verify ended on the server as part of processing the action, not in the browser that submitted it.

Server rendering

If you render on the server, the countdown can be correct in the first byte of HTML with no loading state at all.

Next.js React Server Component

// React Server Component — the countdown is correct in the
// first byte of HTML, with no loading state and no client fetch.
export default async function OfferBanner({ timerId }) {
  const response = await fetch(
    `https://countdownshare.com/api/v1/timers/${timerId}/status`,
    {
      headers: { Authorization: `Bearer ${process.env.COUNTDOWNSHARE_API_KEY}` },
      cache: "no-store",   // the entire point of this call is freshness
    },
  );

  const { data } = await response.json();

  if (data.ended) return <ExpiredNotice />;

  return (
    <CountdownClock
      initialSeconds={data.remaining.total_seconds}
      serverTime={data.server_time}
    />
  );
}
cache: "no-store" is not optional here. A cached status response is a countdown showing a number from whenever the cache was filled — which on a static route could be at build time. If you need caching for load reasons, cache for a handful of seconds and reconcile on the client.

When not to call this endpoint

Two cases where something else is a better fit.

You are waiting for it to end

Polling to detect zero wastes requests and adds latency. Attach a webhook rule and be called at the moment it happens — including at intermediate points like one hour remaining. Webhooks overview.

You just need to show a clock

A published timer has a hosted page, a responsive iframe, and an email-safe animated GIF — all public URLs, no key, no code. Embeds and hosted pages.

And if you need the timer's definition rather than its clock — the revision, expiry, metadata, or outputs — call GET /timers/{timer_id} instead. The status endpoint deliberately returns only what changes with time.