Create a countdown with the REST API

POST /timers is the only endpoint that consumes your monthly allowance, and the only one whose choices you cannot change later — the timer type is permanent once set. Everything else about a timer can be edited for free afterwards. This page covers every field, what each type requires, and the validation errors that catch people first.

The smallest working request

A name, a type, and the timing field that type needs. The Idempotency-Key header is required — a create without one is rejected.

Request

curl -X POST https://countdownshare.com/api/v1/timers \
  -H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-2030" \
  -d '{
    "name": "Product launch",
    "type": "fixed",
    "deadline_at": "2030-01-01T14:00:00Z"
  }'

201 Created

HTTP/1.1 201 Created

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Product launch",
    "type": "fixed",
    "status": "draft",
    "revision": 1,
    "deadline_at": "2030-01-01T14:00:00.000Z",
    "remaining_seconds": 2692800,
    "timezone": "UTC",
    "expiry": { "behavior": "show_message" },
    "metadata": {},
    "outputs": null,
    "created_at": "2029-12-01T10:00:00.000Z",
    "updated_at": "2029-12-01T10:00:00.000Z"
  },
  "request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}
Note status: "draft" and outputs: null. A new timer is private until you publish it, so there is nothing public to link to yet. That is the safe default while you are still wiring things up — send publish: true when you actually want it live.

Every field

The Required column names the types a field is mandatory for. Fields marked “No” are optional for all four.

FieldTypeRequiredDescription
namestringYes1–200 characters. Your internal label — it is not shown publicly unless your display uses it.
typestringYesfixed, duration, recurring, or personalized. Permanent.
deadline_atstringfixedISO 8601. Must carry Z or a numeric UTC offset.
duration_secondsintegerduration, personalized1 to 31,536,000 seconds (one year).
recurrenceobjectrecurringfrequency and local_time, plus whatever the chosen schedule needs.
external_user_idstringpersonalized1–255 characters. Your identifier for the recipient.
timezonestringNoIANA identifier. Defaults to UTC. Only changes behaviour for recurring timers.
publishbooleanNoPublish immediately. Defaults to false. Production only.
expiryobjectNoWhat the public page and embeds do after the timer ends.
metadataobjectNoUp to 50 of your own key-value pairs, returned with the timer.

What each type requires

The four minimum bodies, side by side. Everything else is optional on top of these.

Fixed and duration

fixed — one shared deadline

{
  "name": "Product launch",
  "type": "fixed",
  "deadline_at": "2030-01-01T14:00:00Z"
}

duration — elapsed time, controllable

{
  "name": "Checkout reservation",
  "type": "duration",
  "duration_seconds": 900
}

Recurring and personalized

recurring — repeats on a schedule

{
  "name": "Weekly deploy freeze",
  "type": "recurring",
  "recurrence": {
    "frequency": "weekly",
    "days_of_week": ["friday"],
    "local_time": "16:00"
  },
  "timezone": "Europe/Berlin"
}

personalized — per-recipient deadline

{
  "name": "Trial ends",
  "type": "personalized",
  "duration_seconds": 1209600,
  "external_user_id": "customer_8f21c"
}
A recurring timer always needs frequency and local_time. Weekly schedules add days_of_week, monthly schedules add day_of_month, and custom schedules add an rrule from the DAILY, WEEKLY, and MONTHLY subset of RFC 5545. Full detail on recurring timers.

Deciding what happens at zero

The expiry object controls the hosted page and embeds after the countdown ends. It has no effect on webhooks — those are driven by rules, separately.

behaviorAlso sendResult
show_messagemessage (optional, ≤500 chars)The default. Displays your message in place of the clock.
hideThe timer disappears. Useful when an expired countdown should leave no trace on the page.
redirectredirect_urlSends visitors to another URL — an "offer closed" page, or the next campaign.
Set this at creation even if you think the countdown will never be seen after it ends. Somebody always opens the email a day late, and the default of a bare finished clock with no explanation is the one outcome nobody intends.

Metadata, and what it is for

Up to 50 key-value pairs stored with the timer and returned verbatim on every read. Values may be strings, numbers, booleans, or null.

A fully specified create

{
  "name": "Spring sale ends",
  "type": "fixed",
  "deadline_at": "2030-03-31T23:59:59-04:00",
  "timezone": "America/New_York",
  "publish": true,
  "expiry": {
    "behavior": "redirect",
    "redirect_url": "https://example.com/sale-ended"
  },
  "metadata": {
    "campaign_id": "spring-2030",
    "segment": "returning",
    "internal_owner": "growth"
  }
}

The useful pattern is to store what connects the timer back to your own system — the order ID, the campaign, the customer segment, the internal owner. Then a timer that shows up in a list six months later can be traced to what created it without a lookup table.

Metadata is not searchable and not indexed. It is a place to keep context, not a substitute for your own database. If you need to find timers by a value, store the timer ID against the record on your side.

The validation errors people hit first

All return HTTP 400 unless noted. The message names the offending field.

What you sentCodeWhy
deadline_at without Z or an offsetvalidation_errorA bare local timestamp does not identify an instant. Send 2030-01-01T14:00:00Z.
type: "fixed" with duration_secondsvalidation_errorA fixed timer counts to a date. Use type "duration" for elapsed time.
type: "recurring" without local_timemissing_fieldBoth frequency and local_time are always required for a schedule.
timezone: "EST"invalid_timezoneAbbreviations are ambiguous. Use America/New_York.
publish: true with a cs_test_ keyvalidation_errorPublishing is Production only. Sandbox timers stay private.
No Idempotency-Key headermissing_fieldRequired on create. See idempotency.
A reused key with a changed bodyidempotency_conflictHTTP 409. The key already identifies a different request.
A create past your monthly allowancequota_exhaustedHTTP 429. details names the metric and reset time.
quota_exhausted is a 429 like a rate limit but it does not clear by waiting a few seconds — the allowance resets with the billing cycle. Distinguish the two by code, not by status. See error codes.

After creation

Store data.id against whatever the timer represents. Then: