Heartbeat
A Heartbeat monitor has a secret ping URL. Your job calls it with GET when it has finished successfully:
https://dev.tickingora.com/ping/<token>
How signals are counted
- Each scheduled window is a separate run:
[start, end), start included, end excluded. - One signal inside the window is enough for
success. All signals are recorded and counted (signal_count). - No signal until the end of the window →
missed, an incident opens and you get an alert. - There is no "late" status: a signal after the window never changes it. A signal outside any window is recorded as outside of schedule and changes nothing.
- The receive time decides — the moment Tickingora accepted the request.
The response is 200 OK with a plain-text body (OK, OK (outside of schedule), OK (monitor paused)) and Cache-Control: no-store. An unknown or rotated token gets 404.
Send it after success — never before
The ping means "the job completed". Put it after the work, and only on success.
Two rules for every example below:
- The job decides, not the ping. If the job fails, no ping is sent.
- Monitoring must not change the job's result. If Tickingora is unreachable, the failed ping is reported, but the job still ends as successful — otherwise a monitoring outage would turn your successful backups into "failed" ones (and possibly trigger retries).
Also never print the ping URL in error messages: it is a secret.
#!/usr/bin/env bash
set -euo pipefail # stop on the first error — the ping is never reached
pg_dump mydb | gzip > /backups/mydb-$(date +%F).sql.gz
curl -fsS -m 10 --retry 3 "$TICKINGORA_PING_URL" > /dev/null || echo "Tickingora ping failed" >&2
Cron: make the URL available to the job
Cron starts jobs with a minimal environment: variables exported in your ~/.bashrc or interactive shell are not there, and the default shell is /bin/sh (use ., not source). Keep the URL in an environment file that only the job's user can read:
sudo install -d -o root -g backup -m 750 /etc/tickingora
sudo install -o backup -g backup -m 600 /dev/null /etc/tickingora/backup.env
sudoedit /etc/tickingora/backup.env
# /etc/tickingora/backup.env (mode 600, owner backup)
TICKINGORA_PING_URL=https://…/ping/…
Then either load it in the crontab line and chain with &&:
0 2 * * * . /etc/tickingora/backup.env && /opt/jobs/backup.sh && { curl -fsS -m 10 --retry 3 "$TICKINGORA_PING_URL" > /dev/null || echo "Tickingora ping failed" >&2; }
The { … || … ; } group keeps a failed ping from failing the line — but do not write backup.sh && curl … || true: that would also hide a failed backup.
…or, cleaner, use a small wrapper and keep the crontab simple:
#!/bin/sh
# /opt/jobs/backup-and-ping.sh
set -eu
. /etc/tickingora/backup.env
/opt/jobs/backup.sh # set -e: a failure stops here, no ping
curl -fsS -m 10 --retry 3 "$TICKINGORA_PING_URL" > /dev/null || echo "Tickingora ping failed" >&2
0 2 * * * /opt/jobs/backup-and-ping.sh >> /var/log/backup.log 2>&1
You can also put TICKINGORA_PING_URL=… as a line at the top of the crontab — simpler, but then the secret lives in the crontab, readable by the user and visible in crontab -l. Never commit the env file. Check it in the job's own environment: sudo -u backup sh -c '. /etc/tickingora/backup.env && curl -fsS "$TICKINGORA_PING_URL?test=1"'.
-f makes curl fail on HTTP errors, -m 10 limits the time, --retry 3 survives short network glitches. curl's own error messages contain only the host name, not the token.
PHP
function runImport(): void { /* … */ }
runImport(); // throws on failure, so the ping below is skipped
// A failed ping must not fail the job; @ also keeps the URL out of PHP warnings.
if (@file_get_contents(getenv('TICKINGORA_PING_URL'), false, stream_context_create(['http' => ['timeout' => 10]])) === false) {
error_log('Tickingora ping failed');
}
Laravel scheduler — rescue(..., report: false) keeps a failed ping from failing the task and from writing the exception (whose message contains the URL) to the log:
// config/services.php: 'tickingora' => ['ping_url' => env('TICKINGORA_PING_URL')],
Schedule::command('import:bank')->dailyAt('06:05')
->onSuccess(fn () => rescue(
fn () => Http::timeout(10)->retry(3, 2000)->get(config('services.tickingora.ping_url')),
fn () => Log::warning('Tickingora ping failed'),
report: false,
));
Python
import os, sys, requests
def main():
run_import() # raises on failure — no ping
try:
requests.get(os.environ["TICKINGORA_PING_URL"], timeout=10).raise_for_status()
except requests.RequestException:
print("Tickingora ping failed", file=sys.stderr) # not the exception: it contains the URL
if __name__ == "__main__":
main()
Node.js / TypeScript
await runSync(); // throws on failure — no ping
await fetch(process.env.TICKINGORA_PING_URL!, { signal: AbortSignal.timeout(10_000) })
.then((res) => { if (!res.ok) throw new Error(); })
.catch(() => console.error("Tickingora ping failed")); // never rethrow, never log the URL
systemd
[Service]
Type=oneshot
ExecStart=/opt/jobs/backup.sh
ExecStartPost=-/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null ${TICKINGORA_PING_URL}
EnvironmentFile=/etc/tickingora/backup.env
ExecStartPost runs only if ExecStart succeeded. The - prefix tells systemd to ignore the curl exit code: without it, an unreachable Tickingora would mark the unit as failed even though the backup succeeded. curl's error still appears in the journal (with the host name only). Keep EnvironmentFile readable only by root (systemd reads it before dropping privileges): chmod 600.
Testing the connection
Add ?test=1 to check the token and connectivity from the job's machine. The test signal is recorded (the dashboard shows "test signal received") but never counts for a window, opens or closes incidents:
curl -fsS "$TICKINGORA_PING_URL?test=1"
# OK (test signal, not counted)
Limits
- 60 signals per minute per ping URL and 600 per minute per IP address; above that the response is
429. - Only the first 100 signals per window are stored individually; the rest are counted.
Rotating the token
If the URL leaks (logs, a public repository) or you lost it, rotate it — the old URL stops working at once and the new one is shown in the response (or in the dashboard: Generate new URL):
curl -sS -X POST https://dev.tickingora.com/api/v1/monitors/42/rotate-token -H "Authorization: Bearer $TICKINGORA_API_KEY"
The ping URL is a secret: keep it in an environment file or a secret manager, not in code. Tickingora stores only a hash of the token (so it cannot show an old URL again — rotate instead) and never writes the URL to access logs.