Tallyworks Heartbeat / docs

Heartbeat API

Your job calls a URL when it finishes, optionally with a count of how much work it did. Heartbeat alerts a webhook when a job is late, reports a failure, or its count breaks a rule. Everything below works with curl; the dashboard does the same through the same API.

Quickstart

1. Get an API key (no email; shown once, store it in a password manager):

curl -X POST https://heartbeat.tallyworks.dev/api/signup

2. Create a check for a job that runs every hour and should import at least 50 orders:

curl -X POST https://heartbeat.tallyworks.dev/api/checks \
  -H "Authorization: Bearer tw_..." -H "content-type: application/json" \
  -d '{"name": "Order import", "period_s": 3600, "min_count": 50,
       "alert_webhook": "https://hooks.slack.com/services/..."}'

The response contains ping_url.

3. Ping at the end of the job, with the count:

import-orders.sh && curl -fsS "https://heartbeat.tallyworks.dev/ping/<token>?n=$ORDERS" || curl -fsS "https://heartbeat.tallyworks.dev/ping/<token>/fail"

Pings

GET, POST or HEAD to the check's ping URL. No API key needed; the token in the URL identifies the check. Answers OK (200), or 404 for an unknown token.

URLMeaning
/ping/<token>The run finished. Status becomes up unless a count rule is broken.
/ping/<token>?n=123Same, with the count (whole number, up to 15 digits). Other values are ignored as "no count".
/ping/<token>/failThe run failed. Status becomes down and an alert goes out.
/ping/<token>/startThe run started. Recorded as a sign of life; does not change the status.

A check is late when no run finished within period_s plus grace_s after the last one. A sweep runs every minute, so the alert goes out within about a minute after that. Paused checks ignore pings and never alert; after resume a check waits for its next ping.

Count rules

FieldAlerts when
min_counta run reports fewer than this many items.
max_drop_pcta run reports this many percent fewer items than the previous run.
detect_anomalies (Pro, not available yet)a run's count is far from the median of the last 30 runs; it starts judging after 8 runs with a count.

A run that breaks a rule still counts as a run: the check is not also reported late.

Alerts

When a check goes down, Heartbeat sends one POST with a JSON body to the check's alert_webhook (an https URL). When it recovers, it sends one up message. While a check stays down, no further alerts are sent. The body works as-is with Slack (text) and Discord (content) incoming webhooks; other tools can read event and check.

{
  "text": "🔴 Order import is DOWN: run reported n=12, expected at least 50 (last ping 0s ago, last count 12)",
  "content": "🔴 Order import is DOWN: run reported n=12, expected at least 50 (last ping 0s ago, last count 12)",
  "event": "down",
  "check": {
    "id": "3f9a1c2b4d5e6f70",
    "name": "Order import",
    "status": "down",
    "last_ping_at": 1791810000,
    "last_count": 12
  }
}

Recovery:

{
  "text": "🟢 Order import is UP: ping received (last ping 0s ago, last count 118)",
  "content": "🟢 Order import is UP: ping received (last ping 0s ago, last count 118)",
  "event": "up",
  "check": {
    "id": "3f9a1c2b4d5e6f70",
    "name": "Order import",
    "status": "up",
    "last_ping_at": 1791813500,
    "last_count": 118
  }
}

Times are Unix seconds. Each alert is sent once, without retries; if your endpoint is down at that moment, the alert is lost, so use a reliable endpoint (Slack, Discord or a service you monitor). Requests come with the user agent Tallyworks-Heartbeat/1. A check without a webhook can't alert anyone.

API reference

Base URL https://heartbeat.tallyworks.dev. Send the key as Authorization: Bearer tw_.... Request and response bodies are JSON; errors look like {"error": "..."}.

RequestDoes
POST /api/signupCreates an account and returns api_key once (no key needed).
GET /api/checksYour plan, check limit and all checks.
POST /api/checksCreates a check (fields below). 201 with the check.
GET /api/checks/<id>One check.
PATCH /api/checks/<id>Changes any field; the ping URL stays the same.
DELETE /api/checks/<id>Deletes the check and its history (204).
GET /api/checks/<id>/pingsThe last 100 pings: at, kind (ok, fail, start), count.
POST /api/checks/<id>/pause, /resumeStops or restarts alerting.

Check fields

FieldTypeDefault and range
namestringrequired, up to 100 characters
period_sinteger86400 (a day); 60 to 2678400 seconds; Free: at least 300
grace_sintegera tenth of the period, between 60 and 3600; up to 2678400
alert_webhookhttps URL or nullnone
min_countinteger or nullnone; at least 1
max_drop_pctinteger or nullnone; 1 to 100
detect_anomaliesbooleanfalse; Pro only

Responses also contain id, status (new, up, down, paused), last_ping_at, next_due_at, last_count and ping_url.

Errors

StatusWhen
400The body is not a JSON object, or a field is out of range (the message names it).
401Missing or invalid API key.
402The plan's check limit is reached.
404Unknown check, path or ping token.
429Too many signups from one address in a minute.

Limits

Questions: ops@tallyworks.dev · Terms · Privacy