Update, duplicate, archive, and delete
Everything about a timer except its type can be changed after creation, and none of it counts against your monthly allowance — only creating does. Updates use optimistic concurrency: you send the revision you read, and the API refuses the write if anything changed underneath you. That single rule is most of this page.
Updating a timer
Send only the fields that change. At least one is required, and omitted fields keep their current values. The If-Match header carries the timer's current revision, which you get from any read.
Move a deadline
curl -X PATCH https://countdownshare.com/api/v1/timers/$TIMER_ID \
-H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: 3" \
-d '{ "deadline_at": "2030-01-02T09:00:00-05:00" }'409 if it changed first
HTTP/1.1 409 Conflict
{
"error": {
"code": "revision_conflict",
"message": "The timer has changed since revision 3 was read"
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}| Field | Type | Notes |
|---|---|---|
name | string | Rename the timer. 1–200 characters. |
deadline_at | string | Fixed timers only. Must carry Z or a numeric offset. |
duration_seconds | integer | 1 to 31,536,000. Resets what a reset action returns to. |
timezone | string | Changes the display zone, and the schedule zone for recurring timers. |
recurrence | object | null | Replace the schedule. Send null to remove it entirely. |
external_user_id | string | Reassign a personalized countdown to a different identity. |
publish | boolean | Make the public page available or unavailable. Production only. |
expiry | object | Change what happens after zero. |
metadata | object | Replaces your stored pairs wholesale — it is not merged. |
metadata is replaced, not merged. Sending { "segment": "vip" } removes every other key you had stored. Read the timer, spread the existing object, then send the result.Why the revision is required
A timer's revision starts at 1 and increments on every change. Requiring it in If-Match turns a class of silent data loss into a visible error.
The lost update problem
// Without revisions, this loses data silently.
//
// 10:00:00 Support reads the timer (deadline 18:00, note "VIP")
// 10:00:05 Marketing reads the timer (deadline 18:00, note "VIP")
// 10:00:10 Support writes deadline 20:00
// 10:00:20 Marketing writes note "Q1" <- built on a stale 18:00
//
// Result: the 20:00 deadline is gone, and nobody was told.Both writes succeed. Both look fine in the logs. The 20:00 deadline is simply gone, because the second writer sent a whole object it had read before the first writer changed it. Nobody finds out until a customer sees the wrong countdown.
With revisions, the second write arrives carrying revision 3 while the timer is at revision 4, and it is rejected. Nothing is lost, and the caller gets a chance to reapply its change to the current state.
Handling revision_conflict
A 409 means someone wrote first. The fix is always the same: re-read, reapply your change on top of the new state, and try again. Retrying the same revision fails identically.
Read, write, retry on conflict
async function extendDeadline(timerId, newDeadline) {
for (let attempt = 0; attempt < 3; attempt++) {
// 1. Read the current state, including its revision.
const { data: timer } = await call(`/timers/${timerId}`);
try {
// 2. Write against exactly that revision.
return await call(`/timers/${timerId}`, {
method: "PATCH",
headers: { "If-Match": String(timer.revision) },
body: JSON.stringify({ deadline_at: newDeadline }),
});
} catch (error) {
// 3. Someone else wrote first. Re-read and reapply — do not
// retry the same revision, it will fail identically.
if (error.code !== "revision_conflict") throw error;
}
}
throw new Error("Timer is being modified faster than we can update it");
}Bound the retries. A loop that retries forever against a timer being updated continuously is a loop that never exits, and three attempts is enough for genuine contention — beyond that, something is wrong that a retry will not fix.
Duplicate, archive, restore, delete
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /timers/{timer_id}/duplicate | Copy the definition into a new timer. Requires Idempotency-Key. Counts against your allowance. |
| POST | /timers/{timer_id}/archive | Move out of the active set. Reversible. Costs nothing. |
| POST | /timers/{timer_id}/restore | Bring an archived timer back to its previous state. |
| DELETE | /timers/{timer_id} | Delete permanently. Cannot be undone. |
Archive or delete?
Archive by default. It is free, reversible, and keeps the record — which matters when someone asks in March what the February campaign's deadline actually was.
Archiving
- Removes the timer from default list results
- Keeps activity history and metrics intact
- Fully reversible with restore
- Does not free a slot in your monthly allowance — that was spent at creation
Deleting
- Permanent, with no recovery path on our side
- The public page and embeds stop working immediately
- Attached webhook rules go with it
- Appropriate for a data-deletion request, not for tidying up
Duplicating
A duplicate is a new timer: new ID, revision back to 1, and one more against your allowance. It copies the definition, not the history — no metrics, no activity, and no webhook rules come with it. Requires an Idempotency-Key for the same reason creation does. See idempotency.
Reading what happened to a timer
Two endpoints answer different questions, and neither costs anything.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /timers/{timer_id}/activity | Paginated history of every change and control action |
| GET | /timers/{timer_id}/metrics | Views, unique visitors, interactions, and conversions |
Activity is the audit trail — who changed the deadline, when it was paused, when it was published. Metrics is the engagement data on the public page and embeds. Reach for activity when a countdown is behaving unexpectedly; it usually shows a change nobody remembered making.
What cannot be changed
Two things are permanent.
| Field | Why | What to do instead |
|---|---|---|
type | Each type has a different timing model. Converting one to another would silently change what the timer means. | Create a new timer of the right type. Archive the old one. |
id | It is the identifier everything else references — your database, webhook rules, published URLs. | Nothing. Store it once and keep it. |