Quick start
- Create a key on the API keys page of your dashboard.
- Create a timer:
curl https://api.timerbeam.com/api/timers \
-H "Authorization: Bearer tb_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"label": "Black Friday",
"deadline": "2026-11-27T23:59",
"timezone": "Europe/Budapest",
"template": "cards",
"store": true
}'You get the timer back — its image URL and the ready-to-paste email HTML:
{
"id": "k7Qm2xPa9Lz3",
"imageUrl": "https://api.timerbeam.com/s/k7Qm2xPa9Lz3.gif",
"html": "<a href=\"…\"><img src=\"…/s/k7Qm2xPa9Lz3.gif\" width=\"570\" height=\"134\" …></a>",
"deadline": "2026-11-27T22:59:00.000Z",
"opens": 0,
…
}Put html in your email (or point an image block at imageUrl). Change the timer later with PATCH /api/timers/{id} — every email already sent shows the change on its next open.
Authentication
Send your key as Authorization: Bearer tb_live_…. A key acts as its account: its timers, its brand kits, its plan’s limits and features. A read-only key can only use GET. No key can sign in, touch billing, delete the account or manage keys — those stay in the dashboard. Revoke a key there and it stops working at once.
Without a key, POST /api/timers still works: a one-off timer (baked into its link, not editable), or with "store": true a saved timer whose manageKey — returned once — authorises its own GET/PATCH/DELETE.
Time zones
Give deadline as local time and name the zone: "deadline": "2026-11-27T23:59", "timezone": "Europe/Budapest" — daylight saving included. Or send an absolute time: ISO 8601 with an offset (2026-11-27T22:59:00Z) or unix seconds. Responses always give deadline in UTC.
Templates & brand kits
GET /api/templates lists the starting designs. Pass one as template, and a brand kit as brandKit to put it in your colours and font — in the same call:
{ "deadline": "2026-11-27T23:59", "timezone": "Europe/Budapest",
"template": "cards", "brandKit": "<id from GET /api/brand-kits>", "store": true } Or send a full design (see TimerDesign in the OpenAPI document). Templates marked proFont use a font from the Pro plan and up; brand kits are Business.
Evergreen timers
A countdown per recipient instead of one deadline (Pro and up, saved timers only):
{ "label": "Welcome offer", "store": true,
"evergreen": { "durationSeconds": 172800, "start": "first-open" } } The returned html carries ?r=RECIPIENT_ID (or ?start=START_DATE for "url-date") — swap the placeholder for your email platform’s merge tag. The email platform guides list the tags for Klaviyo, Mailchimp, HubSpot and Brevo.
Idempotency
Send an Idempotency-Key header (a UUID) with POST /api/timers. If the request is retried — a timeout, a crash, an agent trying again — the same key and body return the first answer (with Idempotent-Replayed: true) instead of creating a second timer. Keys are kept 24 hours; the same key with a different body is a 422. A replay never repeats a manage key.
Errors & limits
Every error is JSON: { "error": "<code>", "message": "<what to fix>" } — branch on error, show message.
| Status | Codes | Meaning |
|---|---|---|
| 400 | bad_deadline, bad_units, bad_template, bad_timezone, timezone_conflict, bad_evergreen, … | The input — `message` says what to fix. |
| 401 | invalid_api_key, unauthorised | A revoked or mistyped key, or none where one is needed. |
| 402 | plan_required, quota_exceeded | The plan doesn’t include it (a Pro font, evergreen, brand kits) or its timer limit is reached. |
| 403 | insufficient_scope, session_required | A read-only key tried to write, or a key tried a browser-only endpoint. |
| 404 | unknown_timer, not_found | No such timer or kit on this account. |
| 409 | limit_reached | Too many brand kits or keys. |
| 422 | idempotency_key_reused | The same Idempotency-Key with a different request. |
| 429 | rate_limited | Wait `Retry-After` seconds. Each key has its own budget. |
Each key has its own rate budget; X-RateLimit-Remaining on every keyed response says what’s left. Plan limits (saved timers, fonts, evergreen, brand kits) apply exactly as in the editor — see pricing.
Analytics
On Business, GET /api/stats gives the account’s opens by day and by timer, and GET /api/timers/{id}/stats one timer’s — any range within the last year, every day included (zeros too), in UTC. Without from/to, the last 30 days.
curl "https://api.timerbeam.com/api/stats?from=2026-11-01&to=2026-11-30" \
-H "Authorization: Bearer tb_live_…"{
"from": "2026-11-01", "to": "2026-11-30", "total": 48210,
"days": [{ "day": "2026-11-01", "count": 0 }, …, { "day": "2026-11-27", "count": 19344 }, …],
"timers": [{ "id": "k7Qm2xPa9Lz3", "label": "Black Friday", "opens": 41022 }, …]
}Webhooks
On Business, Timerbeam can tell your own systems what happens — add endpoints on the Webhooks page or with POST /api/webhooks. Each event is a JSON POST:
| Event | When |
|---|---|
timer.created | A saved timer was created — in the editor, over the API or by an agent. |
timer.updated | A saved timer was changed. Rapid edits (the editor saves as you go) arrive as one event with the final state. |
timer.deleted | A saved timer was deleted. |
timer.expired | A timer’s deadline passed. Not sent for evergreen timers, which never end for everyone at once. |
usage.approaching_limit | Opens this month passed 80% of the plan’s monthly limit — once a month. |
usage.limit_reached | Opens this month reached the plan’s monthly limit — once a month. Timers keep working. |
POST https://example.com/timerbeam-webhook
webhook-id: evt_4f0c2a…
webhook-timestamp: 1795820340
webhook-signature: v1,Zr0cBC5v…
{
"id": "evt_4f0c2a…",
"type": "timer.expired",
"createdAt": "2026-11-27T22:59:04.000Z",
"data": {
"timer": {
"id": "k7Qm2xPa9Lz3", "label": "Black Friday",
"deadline": "2026-11-27T22:59:00.000Z", "expired": true,
"imageUrl": "https://api.timerbeam.com/s/k7Qm2xPa9Lz3.gif", …
}
}
}Deliveries follow Standard Webhooks, so its libraries verify them as they are. Or by hand — check the signature against the raw body, and the timestamp against replays:
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as received, before any JSON parsing.
export function isFromTimerbeam(headers, rawBody, secret) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // replayed or stale
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
return headers['webhook-signature'].split(' ').some((part) => {
const signature = part.split(',')[1] ?? '';
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
});
} Answer with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, then marked failed — the Webhooks page shows every delivery, its answer, and a Retry button. The same webhook-id comes with every retry, so a repeat is easy to ignore. Answer 410 Gone to turn the endpoint off. Endpoints must be public https:// URLs; redirects aren’t followed.
For AI agents
Point an agent at the OpenAPI document — every operation has an operationId, a summary and example requests — and give it a key (a read-only one, to start). The errors say what to change, local deadlines need no offset arithmetic, and Idempotency-Key makes its retries safe. A summary of the whole product for language models is at /llms.txt. Or connect it to the MCP server and skip the HTTP entirely.
MCP server
Timerbeam is also an MCP server, so Claude, Cursor and other agents can use it as a set of tools — “make a 48-hour countdown for the welcome email in our brand colours” just works. It’s the same API underneath: the same key, the same plan limits, the same errors.
- URL
https://api.timerbeam.com/mcp- Transport
- Streamable HTTP
- Auth
- OAuth sign-in, or an API key
claude.ai and ChatGPT
Add a custom connector (in claude.ai: Settings → Connectors) with this URL, then sign in to Timerbeam and allow it when asked — no key to copy:
https://api.timerbeam.com/mcpClaude Code
In a terminal — then run /mcp in Claude Code to sign in. (Or skip the sign-in: add --header "Authorization: Bearer tb_live_…".)
claude mcp add --transport http timerbeam https://api.timerbeam.com/mcpCursor
In ~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"timerbeam": {
"url": "https://api.timerbeam.com/mcp",
"headers": { "Authorization": "Bearer tb_live_…" }
}
}
}VS Code
In .vscode/mcp.json:
{
"servers": {
"timerbeam": {
"type": "http",
"url": "https://api.timerbeam.com/mcp",
"headers": { "Authorization": "Bearer tb_live_…" }
}
}
}Claude Desktop
In claude_desktop_config.json, through the mcp-remote bridge (needs Node.js):
{
"mcpServers": {
"timerbeam": {
"command": "npx",
"args": ["mcp-remote", "https://api.timerbeam.com/mcp", "--header", "Authorization:${AUTH_HEADER}"],
"env": { "AUTH_HEADER": "Bearer tb_live_…" }
}
}
}Clients that support it sign in with OAuth: you approve the app once on a Timerbeam page, choosing nothing more than “Allow”, and it appears under Connected apps on your API keys page, where you can disconnect it. Anything else sends an API key. A read-only key gives an agent the read tools only in effect — anything that would change a timer is refused.
| Tool | What it does |
|---|---|
get_account | The account’s plan, what it includes (fonts, evergreen timers, brand kits, the “Made with Timerbeam” strip) and how much of its limits is used.Read-only |
list_templates | Ready-made designs. Pass a template’s id as template to create_timer or update_timer. A proFont template needs the Pro plan and up.Read-only |
list_brand_kits | The account’s brand kits (colours and a font). Pass a kit’s id as brandKit to create_timer or update_timer.Read-only |
create_brand_kit | Save a brand’s colours and font as a kit (Business plan; up to 20 per account). |
list_timers | The account’s saved timers, newest first, with their opens.Read-only |
get_timer | One timer in full: its settings, design, image URL, embed HTML and opens.Read-only |
create_timer | Create a saved countdown timer on the account. Give a deadline (or evergreen), and optionally a template and brand kit. Returns the image URL and embed HTML; get_embed_code tailors the HTML to an email platform. |
update_timer | Change a saved timer — send only what changes. Every email it was already sent in shows the change on its next open. |
delete_timer | Delete a saved timer for good. It stops showing in every email it was sent in (an invisible pixel from then on). |
get_stats | Opens by day (UTC) for one timer, or for the whole account with a per-timer breakdown. Defaults to the last 30 days; from/to reach back up to a year (Business plan).Read-only |
get_embed_code | The HTML to paste into an email for a timer. For an evergreen timer, name the email platform and the right merge tag is filled in.Read-only |
Reference
GET/api/timers
List the account’s saved timers
200Newest first.401No key or session.
POST/api/timers
Create a timerNo key needed
With store: true, a saved timer: editable after sending, counted toward the plan. Otherwise a one-off timer baked into a signed link. Send Idempotency-Key to make a retry safe.
201Created. A saved timer made without an account includes itsmanageKey— shown once.400Invalid input — seeerrorandmessage.401An API key that is not valid.402The plan does not include this (a timer limit, a font, evergreen).403A read-only key.422Idempotency-Keyreused with a different request.429Rate limited — waitRetry-Afterseconds.
GET/api/timers/{id}
Get a saved timer, with its opens
200The timer.404No such timer on this account (or a wrong manage key).
PATCH/api/timers/{id}
Change a saved timer
Send only what changes. Every email it was already sent in shows the change on its next open.
200The updated timer.400Invalid input.402The plan does not include this.404No such timer on this account.
DELETE/api/timers/{id}
Delete a saved timer
It stops rendering in every email it was sent in (an invisible pixel from then on).
200Deleted.404No such timer.
GET/api/templates
List design templatesNo key needed
Pass a template’s id as template when creating a timer. proFont templates use a font from the Pro plan and up.
200Every template.
GET/api/brand-kits
List the account’s brand kits
200The kits.
POST/api/brand-kits
Create a brand kit (Business)
201Created.400Invalid input.402Brand kits are Business.409At most 20 per account.
PATCH/api/brand-kits/{id}
Replace a brand kit (Business)
200Updated.402Brand kits are Business.404No such kit.
DELETE/api/brand-kits/{id}
Delete a brand kit
Allowed on any plan. Timers styled with it keep their colours.
200Deleted.404No such kit.
GET/api/stats
Opens across the account, by day and by timer (Business)
Defaults to the last 30 days. from and to reach back as far as the plan keeps opens — a year on Business.
200The opens.400A bad or out-of-reach range (bad_range).402The analytics API is Business.
GET/api/timers/{id}/stats
One timer’s opens by day (Business)
For the account that owns the timer — not by manage key. Evergreen timers also report how many recipients have started their countdown.
200The opens.400A bad or out-of-reach range.402The analytics API is Business.404No such timer on this account.
GET/api/webhooks
List webhook endpoints
200The endpoints.
POST/api/webhooks
Add a webhook endpoint (Business)
Returns the endpoint’s signing secret — keep it to verify deliveries. Up to 5 per account.
201Added.400A bad URL or event list.402Webhooks are Business.409At most 5 per account.
PATCH/api/webhooks/{id}
Change an endpoint, or turn it on or off
Send only what changes. After a downgrade an endpoint can still be turned off ({"enabled": false}) or deleted.
200Updated.400Invalid input.402Webhooks are Business.404No such endpoint.
DELETE/api/webhooks/{id}
Delete an endpoint and its delivery log
200Deleted.404No such endpoint.
POST/api/webhooks/{id}/test
Send a `ping` event now
Waits for the attempt and returns the delivery, with the endpoint’s answer.
201Attempted.402Webhooks are Business.404No such endpoint.409The endpoint is off.
POST/api/webhooks/{id}/rotate-secret
Replace the signing secret
The old secret stops working at once.
200The new secret.402Webhooks are Business.404No such endpoint.
GET/api/webhooks/{id}/deliveries
The endpoint’s recent deliveries
The last 25, newest first, kept 30 days.
200The deliveries.404No such endpoint.
POST/api/webhooks/{id}/deliveries/{deliveryId}/retry
Send a delivery again, now
200Attempted.402Webhooks are Business.404No such delivery.
GET/api/capabilities
What this account’s plan includes
200The plan’s features.
Timer fields
The body of POST /api/timers and PATCH /api/timers/{id} (a PATCH sends only what changes):
| Field | Type | What it does |
|---|---|---|
deadline | string | integer | When it ends: ISO 8601 with an offset ("2026-11-27T22:59:00Z"), unix seconds — or, with timezone, a local "YYYY-MM-DDTHH:mm". |
timezone | string | An IANA zone ("Europe/Budapest"): read deadline as local time there, daylight saving included. |
store | boolean | Save it (editable, counted toward the plan). Create only. |
label | string | Your name for it, on the dashboard. |
units | "days" | "hours" | "minutes" | "seconds"[] | Which units to show, largest first. |
locale | "en" | "hu" | "de" | "fr" | "es" | "nl" | … | Label language. |
href | string (uri) | Where a click goes. Without one, a countdown page. |
expiredText | string | Page title and alt text once it is over. |
template | string | A template id: its design becomes the timer’s. |
brandKit | string | A brand kit id: the design (the template’s, or design) is put in its colours and font. |
design | TimerDesign | |
evergreen | Evergreen | null | null in a PATCH turns it back into a fixed deadline. |