sigabrt

Pinging an endpoint

There is one thing to integrate with: a URL you send a request to when a job finishes. Optionally, you can also indicate that a job has started or failed.

The URL

Every endpoint has its own, shown when you create it and on its edit page. The token in the middle is what identifies the endpoint.

https://sigabrt.dev/pulse/8f3c…/beat

The URL is the credential. Anyone who has it can report your job as healthy, so keep it out of anywhere that expands links — a chat message, a public repository, an issue tracker.

Methods

MethodResponseRecords a pulse
GET200Yes
POST200Yes
HEAD200No — a preview or scanner cannot fake one
Anything else405No

GET and POST behave identically. Use whichever your tooling reaches for — a bare curl sends a GET and that is fine.

Responses

200 Recorded. The endpoint is healthy until the next one is due.
{"message":"Ping received","status":"ok","timestamp":"2026-09-05T12:58:53Z"}
404 No endpoint has that token — usually deleted, or a typo.
{"message":"Endpoint not found"}
429 Pulses arrived faster than this endpoint's ceiling. Nothing was recorded.
{"message":"Pulses are arriving faster than this endpoint allows."}

Heartbeats are unlimited and no pulse is ever charged against a quota. What a plan limits is how many endpoints you may have and how short an interval you may set, both settled on the endpoint's own page rather than here.

Each endpoint does have a ceiling on how fast its URL may be used, since anyone holding the URL can use it. It sits far above any schedule — a job pulsing every minute, reporting its start and retrying on failure stays comfortably inside it — so in practice a 429 means something is pulsing in a loop.

When an alert fires

Each endpoint has an interval — how often a pulse should arrive — and a grace period — how late it may be before anyone is woken up. Both are counted from the last pulse received, unless a run has reported its start, in which case that run's own limit takes over.

Healthy a pulse arrived within the interval
Running a run has said it began and has not reported back yet — see below
Grace overdue, but still inside the grace period — no alert yet
Down interval and grace are both spent — this is when the alert goes out

Endpoints are checked every 30 seconds, so an alert lands within about half a minute of the deadline passing. You get a second one when a pulse finally arrives and the endpoint recovers.

Alerts go out by email, by ntfy push, or both — per endpoint. Either way it is recorded in the endpoint's event log, which is not something you can switch off: the log is what the service saw.

Examples

crontab
0 3 * * * ./backup.sh && curl -fsS https://sigabrt.dev/pulse/8f3c…/beat
The && is the important part: the pulse only goes out if the job succeeded. A failed run stays silent, which is what triggers the alert.
shell script
#!/bin/sh
set -e

run_the_work

curl -fsS -m 10 --retry 3 https://sigabrt.dev/pulse/8f3c…/beat
A timeout and a couple of retries stop a momentary network problem from being reported as a dead job.
without curl
wget -q -O /dev/null https://sigabrt.dev/pulse/8f3c…/beat

When a job knows it failed

A job that fails and says nothing is reported down when its deadline passes. But a failure signal can optionally be sent sooner than that.

https://sigabrt.dev/pulse/8f3c…/fail
./backup.sh && curl -fsS https://sigabrt.dev/pulse/8f3c…/beat \
           || curl -fsS https://sigabrt.dev/pulse/8f3c…/fail

The endpoint goes down the moment that arrives, and the alert says the job reported a failure rather than that nothing was heard. It is the same down as any other: the next heartbeat brings it back, and the recovery alert follows.

A failure reported by an endpoint that is already down is recorded and nothing more, so a job failing every hour wakes you once rather than hourly. It closes a run that pulsed its start, and it deliberately does not count as a pulse — the endpoint stays down until something actually succeeds.

Alerts are not held back or batched: a failure and the beat that ends it each reach you as they happen. The one exception is an endpoint that has stopped resembling a schedule — breaking and recovering many times an hour — where alerts pause until it settles down. A job on an hourly interval would have to misbehave several times over before it noticed. Either way the dashboard and the event log record every change.

Jobs that run a long time

A job that sometimes runs longer than the gap between its runs can optionally signal when it begins, and fail after a timeout.

https://sigabrt.dev/pulse/8f3c…/start
curl -fsS https://sigabrt.dev/pulse/8f3c…/start
./backup.sh && curl -fsS https://sigabrt.dev/pulse/8f3c…/beat

Between the two, the endpoint is running: the interval is not counting, and the deadline is the run's own limit — Let a run take up to on the endpoint's page. By default the timeout equals the interval and grace period from the time when the job was started. After this time, the job fails.

It is a backstop as much as a reprieve. A run that never reports back is marked down once its limit is spent, so a job that hangs is still caught — at a point you picked for the job rather than for its schedule. The heartbeat at the end closes the run, and the event log records how long it took.

Start pulses are optional. The URL takes GET and POST like the other one, and ignores HEAD for the same reason.

A wrapper script

If you would rather not write the three pulses out for every job, a small shell script sends them around a command: the start before it runs, then the beat or the failure depending on how it exits.

sudo curl -fsS https://sigabrt.dev/run.sh -o /usr/local/bin/sigabrt-run
sudo chmod +x /usr/local/bin/sigabrt-run

It is about forty lines of POSIX sh — read it first.

Then a job is its token and the command to run:

PATH=/usr/local/bin:/usr/bin:/bin

0 3 * * * sigabrt-run 8f3c… ./backup.sh

The PATH line is what lets the job name the script rather than spell out where it lives. Cron brings its own — commonly /sbin:/bin:/usr/sbin:/usr/bin, which has no /usr/local/bin in it — so without it the script is simply not found, and that is the first thing to check if a wrapped job stops reporting. Leaving the line out and writing /usr/local/bin/sigabrt-run on the job does just as well.

The token is enough: the copy you downloaded knows which instance it came from. A whole pulse URL works in its place, with or without the /beat on the end, and SIGABRT_TOKEN in the environment saves naming the endpoint on every line.

The job comes first, always. Its output and the status it exits with pass through untouched, and a sigabrt that cannot be reached is ignored rather than turned into a failed run — the script can only ever report what happened, never change it.

Push notifications

Set a topic under Settings and install the ntfy app, or open ntfy.sh/<your topic> in a browser. Every endpoint set to push uses that topic; one can be given a topic of its own if you would rather keep it separate.

A topic on a public server has no password — the name is the password. Anyone who guesses it can read your alerts, and whoever runs the server can read them regardless, so keep the generated name rather than picking a memorable one, and expect nothing in a push beyond an endpoint name and a time.

The terminal dashboard

Your endpoints without a browser: the list, each one's event log, and the keys to add, change, pause or delete one. Paste a public key under Settings first — the key is how the account is found, so nothing else is asked for.

ssh sigabrt.dev
KeyDoes
↑ ↓Move, and scroll an event log
enterThe selected endpoint's events
n · eNew endpoint, or edit the selected one — ←→ changes a value, enter saves
dDelete it, once you have typed its name back
q · escBack, then out

A new endpoint's pulse URLs are shown once, on the screen that confirms it. They are not in the list afterwards: this is the part of sigabrt anyone on the internet can reach, so it is trusted with an account's endpoints and nothing else — not the address on it, not the SSH keys, not billing, and not the URLs that would let someone fake a heartbeat.

Which key you arrive with is worth pinning down. ssh offers everything your agent holds, in its own order, and this end takes the first one it is given — so with more than one key loaded you can be told your key isn't linked while looking at a fingerprint you never chose. Name the key for this host in ~/.ssh/config and the question goes away:

Host sigabrt.dev
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

IdentitiesOnly yes is the half that does the work: without it the agent's other keys are still offered ahead of the one you named. The screen always shows the key it let you in with, so if that is the wrong one, this is what to fix.

Managing endpoints from a script

Not yet. The dashboard talks to a JSON API, but the only credential it accepts is a token that expires after a day and can only be obtained with your account password — which is not something to put in a script. Long-lived API tokens are needed first, and this page will grow a section when they exist.