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"
}
FieldTypeNotes
namestringRename the timer. 1–200 characters.
deadline_atstringFixed timers only. Must carry Z or a numeric offset.
duration_secondsinteger1 to 31,536,000. Resets what a reset action returns to.
timezonestringChanges the display zone, and the schedule zone for recurring timers.
recurrenceobject | nullReplace the schedule. Send null to remove it entirely.
external_user_idstringReassign a personalized countdown to a different identity.
publishbooleanMake the public page available or unavailable. Production only.
expiryobjectChange what happens after zero.
metadataobjectReplaces 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.

This matters more than it first appears in any system where a timer can be touched by more than one thing — a dashboard, a support tool, a scheduled job, and a webhook handler are four writers, and they will overlap eventually.

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

MethodEndpointPurpose
POST/timers/{timer_id}/duplicateCopy the definition into a new timer. Requires Idempotency-Key. Counts against your allowance.
POST/timers/{timer_id}/archiveMove out of the active set. Reversible. Costs nothing.
POST/timers/{timer_id}/restoreBring 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
Deleting does not restore allowance. Both creating and then deleting a timer still consumes one from the monthly count, which is worth knowing before writing a job that recreates timers on a schedule.

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.

MethodEndpointPurpose
GET/timers/{timer_id}/activityPaginated history of every change and control action
GET/timers/{timer_id}/metricsViews, 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.

FieldWhyWhat to do instead
typeEach 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.
idIt is the identifier everything else references — your database, webhook rules, published URLs.Nothing. Store it once and keep it.
If you find yourself wanting to change a type, it is usually a sign the wrong one was chosen at creation. The timer types page has the one question that resolves the choice.