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 · Pings · Count rules · Alerts · API reference · Limits
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.
| URL | Meaning |
|---|---|
/ping/<token> | The run finished. Status becomes up unless a count rule is broken. |
/ping/<token>?n=123 | Same, with the count (whole number, up to 15 digits). Other values are ignored as "no count". |
/ping/<token>/fail | The run failed. Status becomes down and an alert goes out. |
/ping/<token>/start | The 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
| Field | Alerts when |
|---|---|
min_count | a run reports fewer than this many items. |
max_drop_pct | a 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": "..."}.
| Request | Does |
|---|---|
POST /api/signup | Creates an account and returns api_key once (no key needed). |
GET /api/checks | Your plan, check limit and all checks. |
POST /api/checks | Creates 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>/pings | The last 100 pings: at, kind (ok, fail, start), count. |
POST /api/checks/<id>/pause, /resume | Stops or restarts alerting. |
Check fields
| Field | Type | Default and range |
|---|---|---|
name | string | required, up to 100 characters |
period_s | integer | 86400 (a day); 60 to 2678400 seconds; Free: at least 300 |
grace_s | integer | a tenth of the period, between 60 and 3600; up to 2678400 |
alert_webhook | https URL or null | none |
min_count | integer or null | none; at least 1 |
max_drop_pct | integer or null | none; 1 to 100 |
detect_anomalies | boolean | false; Pro only |
Responses also contain id, status (new, up, down, paused), last_ping_at,
next_due_at, last_count and ping_url.
Errors
| Status | When |
|---|---|
| 400 | The body is not a JSON object, or a field is out of range (the message names it). |
| 401 | Missing or invalid API key. |
| 402 | The plan's check limit is reached. |
| 404 | Unknown check, path or ping token. |
| 429 | Too many signups from one address in a minute. |
Limits
- Free: 10 checks with a period of 5 minutes or longer, count rules, webhook alerts, the last 100 pings per check.
- Pro (coming soon, price not set yet): 100 checks, periods from 1 minute, anomaly detection. Free stays free.
- Signups: 3 per minute from one address.
Questions: ops@tallyworks.dev · Terms · Privacy