Publishing heartbeats
A beat is one POST to your monitor’s publish URL. Send one each time your job runs.
New here? Start with Publish your first heartbeat.
The request
Section titled “The request”curl --request POST \ --url https://ingest.signalsitter.com/nightly-backup \ --header "Authorization: Bearer ${SIGNALSITTER_PUBLISH_CREDENTIAL}"The URL is an ingest host plus the monitor’s ID. Copy it from the monitor in the dashboard, or from publishUrl when you create a monitor through the API. Use it as given.
| Region | Ingest host |
|---|---|
| Unrestricted | https://ingest.signalsitter.com |
| European Union | https://eu.ingest.signalsitter.com |
A monitor’s region is fixed when it’s created. Keep publishing an existing monitor to its own URL.
Authenticate with Authorization: Bearer <credential>. The credential’s level decides what a beat can do:
manage: records the beat and applies any configuration headers. Sending to a new ID creates the monitor.writeorreadwrite: records the beat and ignores configuration headers. The response lists them inconfiguration.ignoredFields.read: refused with403.
The body
Section titled “The body”The body is optional. Send one of:
- nothing;
- a single JSON number, such as
42; - a flat JSON object with 1 to 16 numeric measurements, plus a few scalar metadata fields.
curl --request POST \ --url https://ingest.signalsitter.com/nightly-backup \ --header "Authorization: Bearer ${SIGNALSITTER_PUBLISH_CREDENTIAL}" \ --header "Content-Type: application/json" \ --data '{"files":42,"source":"nightly","complete":true}'Rules for the body:
- Any body needs
Content-Type: application/json. - Measurement names use lowercase letters, numbers and underscores, and start with a letter.
- Numbers must be finite JSON numbers.
"42"as a string is refused, not converted. - The body can be at most 16 KiB.
Compressed bodies
Section titled “Compressed bodies”Send Content-Encoding: gzip to compress the body. The 16 KiB limit applies to the decompressed body. Any other encoding is refused with 415.
Configure a monitor with headers
Section titled “Configure a monitor with headers”With a manage credential, headers on a beat change the monitor’s configuration. The change is saved with the beat.
| Header | Sets | Values |
|---|---|---|
Signalsitter-Name |
Display name | Text |
Signalsitter-Interval |
Expected time between beats | Seconds, 60 to one year |
Signalsitter-Grace |
Extra time before a beat counts as missing | Seconds, 0 to 7 days. Default 60. Values under 60 become 60 |
Signalsitter-Heartbeat |
Turns missing-beat alerts off | disabled. Send Signalsitter-Interval to turn them back on |
Signalsitter-Callback |
Where incidents are sent | none, default, alias:<name>, or a public https:// URL |
Signalsitter-Metric-Type |
How a bare-number body is read | gauge, count, rate, duration, counter, size |
Signalsitter-Min / Signalsitter-Max |
Limits on a bare-number body. A value outside them opens an incident | Finite numbers |
curl --request POST \ --url https://ingest.signalsitter.com/nightly-backup \ --header "Authorization: Bearer ${SIGNALSITTER_PUBLISH_CREDENTIAL}" \ --header "Signalsitter-Name: Nightly backup" \ --header "Signalsitter-Interval: 86400" \ --header "Signalsitter-Grace: 1800"Your plan may set a longer minimum interval. See Plans and limits.
For anything the common headers don’t cover, send Signalsitter-Config: a versioned JSON patch, encoded as unpadded base64url. It takes the same patch as the update configuration operation. If a common header and Signalsitter-Config set the same field, the values must agree.
Older learned-detection settings, such as Signalsitter-Sensitivity and Signalsitter-Mode, are still accepted for compatibility. This guide doesn’t cover them.
Cadence
Section titled “Cadence”Set it. Send Signalsitter-Interval, or set an interval when you create the monitor. Each deadline counts from the latest beat.
Let it learn. Create the monitor without an interval. It watches your beats and sets the interval itself once five beats in a row arrive at a steady pace, each gap within 20% of the typical gap. Grace is then half the interval, at least 60 seconds, unless you set one.
While it’s learning:
- Silence raises nothing. There’s no deadline yet.
- Only beats sent with a
managecredential count. - Each response shows progress:
{ "heartbeatExpectation": { "state": "automatic", "version": 1 }, "heartbeatLearning": { "status": "collecting", "samples": 3, "requiredSamples": 5 }}status is collecting, inconsistent (beats too irregular, still trying) or unsupported_cadence (the measured pace is outside 60 seconds to one year). Accept values you don’t recognise, as more may be added.
If your job runs irregularly, set the interval. A source that never settles into a steady pace is never watched.
To learn again, set the heartbeat expectation back to { "state": "automatic", "version": 1 } in the dashboard, the API or Signalsitter-Config.
Measurement types
Section titled “Measurement types”An object measurement without a type is a gauge.
| Type | Accepts |
|---|---|
gauge |
Any finite number, including negatives |
count, counter |
Non-negative whole numbers |
rate, duration, size |
Non-negative numbers |
A counter is a running total. Its first beat sets the starting point, and each later beat is measured as the increase. A drop counts as a reset. unit is a display label only. Nothing is converted.
Numeric rules
Section titled “Numeric rules”Rules are fixed limits on a measurement. A breach opens an incident and sends your callback.
For a bare-number body, set the limits with headers:
curl --request POST \ --url https://ingest.signalsitter.com/queue-worker \ --header "Authorization: Bearer ${SIGNALSITTER_PUBLISH_CREDENTIAL}" \ --header "Content-Type: application/json" \ --header "Signalsitter-Min: 0" \ --header "Signalsitter-Max: 100" \ --data '42'| Rule | Fires when |
|---|---|
minimum / maximum |
The value is below or above the limit. Limits are inclusive |
zeroExpectation |
The value isn’t zero ("zero") or is zero ("nonzero") |
maximumAbsoluteDelta |
The value moved more than this since the last beat |
maximumPercentageDelta |
The value moved more than this percentage since the last beat |
ratio |
This measurement divided by another one is outside minimum / maximum |
To set rules on named measurements in an object body, or any rule beyond a minimum and maximum, use update configuration, the rule workbench in the dashboard, or Signalsitter-Config on a beat. Set them with a rule’s "ruleAction": { "mode": "active", "version": 1 } to make it open incidents; without it, the rule is checked and shown but never alerts.
This beat alerts when fewer than one file is backed up:
CONFIG='{"version":1,"patch":{"measurements":{"files":{"minimum":1,"ruleAction":{"mode":"active","version":1}}}}}'CONFIG_HEADER=$(printf '%s' "$CONFIG" | base64 | tr -d '\n=' | tr '+/' '-_')
curl --request POST \ --url https://ingest.signalsitter.com/nightly-backup \ --header "Authorization: Bearer ${SIGNALSITTER_PUBLISH_CREDENTIAL}" \ --header "Content-Type: application/json" \ --header "Signalsitter-Config: ${CONFIG_HEADER}" \ --data '{"files":0}'Good to know:
- A monitor can have up to 32 rules.
- By default, one breaching beat opens a critical incident. Set
rulePolicyto wait for several beats or a length of time first. - To pause one rule, list its kind in the measurement’s
disabledRules. To remove one, set it tonull, for example{ "measurements": { "files": { "maximum": null } } }. - Try a beat against your rules without recording it with test a beat.
The response
Section titled “The response”200 means the beat and any configuration change are saved. A trimmed example:
{ "accepted": true, "replayed": false, "monitorKey": "nightly-backup", "observation": { "id": "obs_01234567-89ab-4cde-8fab-0123456789ab", "status": "on_time" }, "configuration": { "status": "applied", "appliedFields": ["grace_seconds", "interval_seconds"], "ignoredFields": [], "warnings": [] }, "nextDeadline": "2026-07-28T12:05:30.000Z"}observation.status is on_time, late (after interval plus grace), learning, or not_expected when missing-beat alerts are off.
Errors use application/problem+json with a request ID to quote to support.
| Status | Meaning | What to do |
|---|---|---|
400 |
Bad monitor ID, header, configuration or body | Fix the request. Don’t retry it unchanged |
401 |
Credential missing or invalid | Check the Authorization header and the credential |
403 |
Credential can’t publish, or publishing is refused for your account | Use a write, readwrite or manage credential, or read the problem detail |
409 |
Idempotency key reused with different content | Use a new key for a new beat |
410 |
Monitor deleted | Deleted monitors can’t come back. Create a new monitor with a new ID |
413 |
Body or headers too large | Keep the body to 16 KiB or less, and shorten long headers |
415 |
Body isn’t JSON, or uses an encoding other than gzip | Send JSON, uncompressed or gzip |
429 |
Plan limit reached, or beats faster than the cadence allows | Read the problem’s limit context. See below for cadence |
503 |
Temporary problem on our side | Retry the same request |
Retries
Section titled “Retries”Retry 503 with the same request. A 200 with "replayed": true means the first attempt had already been saved.
Publishing faster than the cadence
Section titled “Publishing faster than the cadence”You can publish up to twice per interval, and no more than once every 48 seconds. Ten beats can arrive back to back before that spacing applies. A 5-minute monitor accepts one beat every 2 minutes 30 seconds.
Faster beats get 429 with a Retry-After and the beat_cadence limit. Ignore them or retry after Retry-After. Either way nothing is lost, and the monitor stays on time.