Ping API
Send heartbeats and progress updates from your cron jobs
Use these endpoints from your cron jobs to monitor execution. Available on all plans. For programmatic management of jobs and workspaces, see the Management API (Business plan).
Prefer an SDK? We have official libraries for PHP, Node.js, Python, Ruby, and Go. View SDK documentation →
Endpoints
| POST | /ping/{job_key} | Simple heartbeat — signal job completed | ↓ |
| POST | /ping/{job_key}/start | Mark job as started, begin execution timer | ↓ |
| POST | /ping/{job_key}/end/success | Mark job as finished successfully | ↓ |
| POST | /ping/{job_key}/end/fail | Mark job as failed | ↓ |
| POST | /ping/{job_key}/progress/{seq} | Progress update with percentage (0–100) | ↓ |
| POST | /ping/{job_key}/progress | Progress update with message only | ↓ |
On this page
Simple ping
Send a single heartbeat when your job completes. No start/end tracking needed.
https://cronbeats.io/ping/{job_key}
curl -X POST https://cronbeats.io/ping/abc123defile_get_contents('https://cronbeats.io/ping/abc123de', false, stream_context_create(['http' => ['method' => 'POST']]));import requests
requests.post('https://cronbeats.io/ping/abc123de')await fetch('https://cronbeats.io/ping/abc123de', { method: 'POST' });require 'net/http'
Net::HTTP.post(URI('https://cronbeats.io/ping/abc123de'), nil)import "net/http"
http.Post("https://cronbeats.io/ping/abc123de", "", nil)Start & end signals
Signal when a job starts and when it finishes. Enables execution time tracking and stuck-job detection.
https://cronbeats.io/ping/{job_key}/start
Marks job as started and begins the execution timer.
curl -X POST https://cronbeats.io/ping/abc123de/startfile_get_contents('https://cronbeats.io/ping/abc123de/start', false, stream_context_create(['http' => ['method' => 'POST']]));import requests
requests.post('https://cronbeats.io/ping/abc123de/start')await fetch('https://cronbeats.io/ping/abc123de/start', { method: 'POST' });require 'net/http'
Net::HTTP.post(URI('https://cronbeats.io/ping/abc123de/start'), nil)import "net/http"
http.Post("https://cronbeats.io/ping/abc123de/start", "", nil)https://cronbeats.io/ping/{job_key}/end/{status}
Marks job as finished. Use end/success when the job succeeded, or end/fail when it failed.
Example with both variants:
# On success
curl -X POST https://cronbeats.io/ping/abc123de/end/success
# On failure
curl -X POST https://cronbeats.io/ping/abc123de/end/fail$u = 'https://cronbeats.io/ping/abc123de';
$ctx = stream_context_create(['http' => ['method' => 'POST']]);
// On success
file_get_contents($u.'/end/success', false, $ctx);
// On failure
file_get_contents($u.'/end/fail', false, $ctx);import requests
u = 'https://cronbeats.io/ping/abc123de'
# On success
requests.post(f'{u}/end/success')
# On failure
requests.post(f'{u}/end/fail')const u = 'https://cronbeats.io/ping/abc123de';
// On success
await fetch(u + '/end/success', { method: 'POST' });
// On failure
await fetch(u + '/end/fail', { method: 'POST' });require 'net/http'
u = 'https://cronbeats.io/ping/abc123de'
# On success
Net::HTTP.post(URI(u + '/end/success'), nil)
# On failure
Net::HTTP.post(URI(u + '/end/fail'), nil)import "net/http"
u := "https://cronbeats.io/ping/abc123de"
// On success
http.Post(u+"/end/success", "", nil)
// On failure
http.Post(u+"/end/fail", "", nil)Advanced: start → run job → end (one command)
Wrap your script so execution time is tracked end-to-end. Replace YOUR_KEY with your job key and ./backup.sh with your command.
curl -sS -X POST https://cronbeats.io/ping/YOUR_KEY/start && ./backup.sh && curl -sS -X POST https://cronbeats.io/ping/YOUR_KEY/end/successphp -r "\$u='https://cronbeats.io/ping/YOUR_KEY'; file_get_contents(\$u.'/start',0,stream_context_create(['http'=>['method'=>'POST']])); passthru('./backup.sh'); file_get_contents(\$u.'/end/success',0,stream_context_create(['http'=>['method'=>'POST']]));"import requests, subprocess
u = "https://cronbeats.io/ping/YOUR_KEY"
requests.post(f"{u}/start"); subprocess.run("./backup.sh", shell=True); requests.post(f"{u}/end/success")const u = 'https://cronbeats.io/ping/YOUR_KEY';
require('https').request(u+'/start', {method:'POST'}, ()=>{}).end();
require('child_process').execSync('./backup.sh');
require('https').request(u+'/end/success', {method:'POST'}, ()=>{}).end();ruby -r net/http -e 'u="https://cronbeats.io/ping/YOUR_KEY"; Net::HTTP.post(URI(u+"/start"), nil); system("./backup.sh"); Net::HTTP.post(URI(u+"/end/success"), nil)'import ("net/http"; "os/exec")
u := "https://cronbeats.io/ping/YOUR_KEY"
http.Post(u+"/start", "", nil)
exec.Command("sh", "-c", "./backup.sh").Run()
http.Post(u+"/end/success", "", nil)Progress tracking
Send percentage and/or status messages while your job runs. Two modes depending on your use case.
Mode 1 — With percentage
/progress/{seq}
Body: {"message": "..."}
Dashboard shows a progress bar (0–100) alongside your message.
Use when you can calculate meaningful progress — e.g. processed 50 of 100 records.
Mode 2 — Message only
/progress
Body: {"message": "..."}
Dashboard shows only your status message — no percentage bar.
Use when progress isn't measurable — e.g. "Connecting to database..."
Note: Both modes require Content-Type: application/json. The seq value (0–100) goes in the URL path, not the request body.
Progress endpoint reference
https://cronbeats.io/ping/{job_key}/progress/{seq}
(with percentage)
https://cronbeats.io/ping/{job_key}/progress
(message only)
| Parameter | Type | Description |
|---|---|---|
| seq | integer (0–100), URL path | Progress percentage. Include in URL path like /progress/50 to show a percentage bar. Omit for message-only mode. |
| message / msg | string, JSON body | Status text in POST body as JSON: {"message": "Step 2/4"}. Max 255 characters. |
curl -X POST "https://cronbeats.io/ping/abc123de/progress/50" \
-H "Content-Type: application/json" \
-d '{"message": "Processing batch 50/100"}'<?php
$u = 'https://cronbeats.io/ping/abc123de/progress/50';
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode(['message' => 'Processing batch 50/100'])
]
]);
file_get_contents($u, false, $ctx);import requests
requests.post('https://cronbeats.io/ping/abc123de/progress/50', json={'message': 'Processing batch 50/100'})await fetch('https://cronbeats.io/ping/abc123de/progress/50', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Processing batch 50/100' })
});require 'net/http'
require 'json'
uri = URI('https://cronbeats.io/ping/abc123de/progress/50')
Net::HTTP.post(uri, JSON.generate({message: 'Processing batch 50/100'}), {'Content-Type' => 'application/json'})import ("net/http"; "bytes"; "encoding/json")
u := "https://cronbeats.io/ping/abc123de/progress/50"
body, _ := json.Marshal(map[string]string{"message": "Processing batch 50/100"})
http.Post(u, "application/json", bytes.NewBuffer(body))Examples
Copy and adapt these examples. Replace YOUR_KEY with your job's ping key from the dashboard.
1. Backup with percentage
Run backup steps and report progress after each stage.
#!/bin/bash
PING_URL="https://cronbeats.io/ping/YOUR_KEY"
curl -sS -X POST $PING_URL/start
curl -sS -X POST "$PING_URL/progress/10" -H "Content-Type: application/json" -d '{"message":"Starting backup"}'
pg_dump -U user mydb > backup.sql
curl -sS -X POST "$PING_URL/progress/40" -H "Content-Type: application/json" -d '{"message":"Database dump complete"}'
gzip -f backup.sql
curl -sS -X POST "$PING_URL/progress/70" -H "Content-Type: application/json" -d '{"message":"Compression complete"}'
aws s3 cp backup.sql.gz s3://my-bucket/
curl -sS -X POST "$PING_URL/progress/90" -H "Content-Type: application/json" -d '{"message":"Upload complete"}'
curl -sS -X POST "$PING_URL/progress/100" -H "Content-Type: application/json" -d '{"message":"Done"}'
curl -sS -X POST $PING_URL/end/success<?php
$u = 'https://cronbeats.io/ping/YOUR_KEY';
$ctx = stream_context_create(['http' => ['method' => 'POST']]);
$json = fn($msg) => stream_context_create(['http' => ['method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode(['message' => $msg])]]);
file_get_contents($u.'/start', false, $ctx);
file_get_contents($u.'/progress/10', false, $json('Starting backup'));
exec('pg_dump -U user mydb > backup.sql');
file_get_contents($u.'/progress/40', false, $json('Database dump complete'));
exec('gzip -f backup.sql');
file_get_contents($u.'/progress/70', false, $json('Compression complete'));
exec('aws s3 cp backup.sql.gz s3://my-bucket/');
file_get_contents($u.'/progress/90', false, $json('Upload complete'));
file_get_contents($u.'/progress/100', false, $json('Done'));
file_get_contents($u.'/end/success', false, $ctx);import requests, subprocess
u = "https://cronbeats.io/ping/YOUR_KEY"
requests.post(f"{u}/start")
requests.post(f"{u}/progress/10", json={"message": "Starting backup"})
subprocess.run(["pg_dump", "-U", "user", "mydb"], stdout=open("backup.sql", "w"), check=True)
requests.post(f"{u}/progress/40", json={"message": "Database dump complete"})
subprocess.run(["gzip", "-f", "backup.sql"], check=True)
requests.post(f"{u}/progress/70", json={"message": "Compression complete"})
subprocess.run(["aws", "s3", "cp", "backup.sql.gz", "s3://my-bucket/"], check=True)
requests.post(f"{u}/progress/90", json={"message": "Upload complete"})
requests.post(f"{u}/progress/100", json={"message": "Done"})
requests.post(f"{u}/end/success")const u = 'https://cronbeats.io/ping/YOUR_KEY';
const { execSync } = require('child_process');
const p = (path, msg) => fetch(u+path, { method:'POST', headers:{'Content-Type':'application/json'}, body: msg ? JSON.stringify({message: msg}) : undefined });
await p('/start');
await p('/progress/10', 'Starting backup');
execSync('pg_dump -U user mydb > backup.sql');
await p('/progress/40', 'Database dump complete');
execSync('gzip -f backup.sql');
await p('/progress/70', 'Compression complete');
execSync('aws s3 cp backup.sql.gz s3://my-bucket/');
await p('/progress/90', 'Upload complete');
await p('/progress/100', 'Done');
await p('/end/success');require 'net/http'
require 'json'
u = "https://cronbeats.io/ping/YOUR_KEY"
Net::HTTP.post(URI(u+'/start'), nil)
Net::HTTP.post(URI(u+'/progress/10'), JSON.generate({message: 'Starting backup'}), {'Content-Type' => 'application/json'})
system('pg_dump -U user mydb > backup.sql')
Net::HTTP.post(URI(u+'/progress/40'), JSON.generate({message: 'Database dump complete'}), {'Content-Type' => 'application/json'})
system('gzip -f backup.sql')
Net::HTTP.post(URI(u+'/progress/70'), JSON.generate({message: 'Compression complete'}), {'Content-Type' => 'application/json'})
system('aws s3 cp backup.sql.gz s3://my-bucket/')
Net::HTTP.post(URI(u+'/progress/90'), JSON.generate({message: 'Upload complete'}), {'Content-Type' => 'application/json'})
Net::HTTP.post(URI(u+'/progress/100'), JSON.generate({message: 'Done'}), {'Content-Type' => 'application/json'})
Net::HTTP.post(URI(u+'/end/success'), nil)import ("net/http"; "bytes"; "encoding/json"; "os/exec")
u := "https://cronbeats.io/ping/YOUR_KEY"
post := func(path, msg string) {
if msg == "" { http.Post(u+path, "", nil); return }
body, _ := json.Marshal(map[string]string{"message": msg})
http.Post(u+path, "application/json", bytes.NewBuffer(body))
}
post("/start", "")
post("/progress/10", "Starting backup")
exec.Command("sh", "-c", "pg_dump -U user mydb > backup.sql").Run()
post("/progress/40", "Database dump complete")
exec.Command("gzip", "-f", "backup.sql").Run()
post("/progress/70", "Compression complete")
exec.Command("aws", "s3", "cp", "backup.sql.gz", "s3://my-bucket/").Run()
post("/progress/90", "Upload complete")
post("/progress/100", "Done")
post("/end/success", "")2. Processing records
Update progress every N records in a loop.
PING_URL="https://cronbeats.io/ping/YOUR_KEY"
TOTAL=10000
curl -sS -X POST $PING_URL/start
for ((i=1;i<=TOTAL;i++)); do
# process_record $i
if (( i % 500 == 0 )); then
pct=$(( i * 100 / TOTAL ))
curl -sS -X POST "$PING_URL/progress/$pct" -H "Content-Type: application/json" -d "{\"message\":\"Processed $i / $TOTAL\"}"
fi
done
curl -sS -X POST $PING_URL/end/success<?php
$u = 'https://cronbeats.io/ping/YOUR_KEY';
$total = 10000;
file_get_contents($u.'/start', false, stream_context_create(['http'=>['method'=>'POST']]));
for ($i = 1; $i <= $total; $i++) {
if ($i % 500 === 0) {
$pct = (int)($i * 100 / $total);
file_get_contents($u."/progress/$pct", false, stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json','content'=>json_encode(['message'=>"Processed $i / $total"])]]));
}
}
file_get_contents($u.'/end/success', false, stream_context_create(['http'=>['method'=>'POST']]));import requests
u = "https://cronbeats.io/ping/YOUR_KEY"
TOTAL = 10000
requests.post(f"{u}/start")
for i in range(1, TOTAL + 1):
if i % 500 == 0:
pct = int(i * 100 / TOTAL)
requests.post(f"{u}/progress/{pct}", json={"message": f"Processed {i:,} / {TOTAL:,}"})
requests.post(f"{u}/end/success")const u = 'https://cronbeats.io/ping/YOUR_KEY';
const TOTAL = 10000;
await fetch(u+'/start', { method: 'POST' });
for (let i = 1; i <= TOTAL; i++) {
if (i % 500 === 0) {
const pct = Math.floor(i * 100 / TOTAL);
await fetch(u+`/progress/${pct}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message: `Processed ${i} / ${TOTAL}` }) });
}
}
await fetch(u+'/end/success', { method: 'POST' });require 'net/http'
require 'json'
u = "https://cronbeats.io/ping/YOUR_KEY"
total = 10000
Net::HTTP.post(URI(u+'/start'), nil)
(1..total).each do |i|
if i % 500 == 0
pct = (i * 100 / total).to_i
Net::HTTP.post(URI(u+"/progress/#{pct}"), JSON.generate({message: "Processed #{i} / #{total}"}), {'Content-Type' => 'application/json'})
end
end
Net::HTTP.post(URI(u+'/end/success'), nil)import ("net/http"; "bytes"; "encoding/json"; "strconv"; "fmt")
u, total := "https://cronbeats.io/ping/YOUR_KEY", 10000
http.Post(u+"/start", "", nil)
for i := 1; i <= total; i++ {
if i%500 == 0 {
pct := strconv.Itoa(i * 100 / total)
body, _ := json.Marshal(map[string]string{"message": fmt.Sprintf("Processed %d / %d", i, total)})
http.Post(u+"/progress/"+pct, "application/json", bytes.NewBuffer(body))
}
}
http.Post(u+"/end/success", "", nil)3. Multi-step pipeline
Report progress for each step in an ETL or pipeline.
PING_URL="https://cronbeats.io/ping/YOUR_KEY"
curl -sS -X POST $PING_URL/start
curl -sS -X POST "$PING_URL/progress/0" -H "Content-Type: application/json" -d '{"message":"Step 1/4: Extract"}'
# extract...
curl -sS -X POST "$PING_URL/progress/25" -H "Content-Type: application/json" -d '{"message":"Step 2/4: Transform"}'
# transform...
curl -sS -X POST "$PING_URL/progress/50" -H "Content-Type: application/json" -d '{"message":"Step 3/4: Load"}'
# load...
curl -sS -X POST "$PING_URL/progress/100" -H "Content-Type: application/json" -d '{"message":"Step 4/4: Done"}'
curl -sS -X POST $PING_URL/end/success<?php
$u = 'https://cronbeats.io/ping/YOUR_KEY';
$json = fn($msg) => stream_context_create(['http' => ['method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode(['message' => $msg])]]);
$ctx = stream_context_create(['http'=>['method'=>'POST']]);
file_get_contents($u.'/start', false, $ctx);
file_get_contents($u.'/progress/0', false, $json('Step 1/4: Extract'));
// extract()
file_get_contents($u.'/progress/25', false, $json('Step 2/4: Transform'));
// transform()
file_get_contents($u.'/progress/50', false, $json('Step 3/4: Load'));
// load()
file_get_contents($u.'/progress/100', false, $json('Step 4/4: Done'));
file_get_contents($u.'/end/success', false, $ctx);import requests
u = "https://cronbeats.io/ping/YOUR_KEY"
requests.post(f"{u}/start")
requests.post(f"{u}/progress/0", json={"message": "Step 1/4: Extract"})
# extract()
requests.post(f"{u}/progress/25", json={"message": "Step 2/4: Transform"})
# transform()
requests.post(f"{u}/progress/50", json={"message": "Step 3/4: Load"})
# load()
requests.post(f"{u}/progress/100", json={"message": "Step 4/4: Done"})
requests.post(f"{u}/end/success")const u = 'https://cronbeats.io/ping/YOUR_KEY';
const p = (path, msg) => fetch(u+path, { method:'POST', headers: msg ? {'Content-Type':'application/json'} : {}, body: msg ? JSON.stringify({message: msg}) : undefined });
await p('/start');
await p('/progress/0', 'Step 1/4: Extract');
// await extract()
await p('/progress/25', 'Step 2/4: Transform');
// await transform()
await p('/progress/50', 'Step 3/4: Load');
// await load()
await p('/progress/100', 'Step 4/4: Done');
await p('/end/success');require 'net/http'
require 'json'
u = "https://cronbeats.io/ping/YOUR_KEY"
Net::HTTP.post(URI(u+'/start'), nil)
Net::HTTP.post(URI(u+'/progress/0'), JSON.generate({message: 'Step 1/4: Extract'}), {'Content-Type' => 'application/json'})
# extract
Net::HTTP.post(URI(u+'/progress/25'), JSON.generate({message: 'Step 2/4: Transform'}), {'Content-Type' => 'application/json'})
# transform
Net::HTTP.post(URI(u+'/progress/50'), JSON.generate({message: 'Step 3/4: Load'}), {'Content-Type' => 'application/json'})
# load
Net::HTTP.post(URI(u+'/progress/100'), JSON.generate({message: 'Step 4/4: Done'}), {'Content-Type' => 'application/json'})
Net::HTTP.post(URI(u+'/end/success'), nil)import ("net/http"; "bytes"; "encoding/json")
u := "https://cronbeats.io/ping/YOUR_KEY"
post := func(path, msg string) {
if msg == "" { http.Post(u+path, "", nil); return }
b, _ := json.Marshal(map[string]string{"message": msg})
http.Post(u+path, "application/json", bytes.NewBuffer(b))
}
post("/start", "")
post("/progress/0", "Step 1/4: Extract")
// extract()
post("/progress/25", "Step 2/4: Transform")
// transform()
post("/progress/50", "Step 3/4: Load")
// load()
post("/progress/100", "Step 4/4: Done")
post("/end/success", "")What you see on the dashboard
- Live progress bar — percentage you send, updated in real time
- Current status message — your latest message string
- Elapsed time — time since
/start - Progress timeline — full history of updates for the current run
Response codes
| Code | Meaning |
|---|---|
| 200 | Success — signal recorded |
| 400 | Invalid route, key format, or payload |
| 404 | Job key not found or disabled |
| 429 | Rate limit exceeded |
| 500 | Service unavailable or processing failed |