Documentation
PingStep receives small lifecycle events from your script and shows the last reported stage of each run. Keep raw output, logs, and job data in your own system.
Quick start
Set two environment variables in your script's runtime:
export PINGSTEP_URL=https://pingstep.dev export PINGSTEP_TOKEN=ps_job_your-token-here
Then send lifecycle events over HTTPS. A minimal run looks like this:
# 1. Start the run
curl -X POST "$PINGSTEP_URL/v1/events" \
-H "Authorization: Bearer $PINGSTEP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt-001",
"job_key": "nightly-backup",
"run_id": "run-2026-08-04",
"sequence": 1,
"type": "started",
"occurred_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'
# 2. Report a stage
curl -X POST "$PINGSTEP_URL/v1/events" \
-H "Authorization: Bearer $PINGSTEP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt-002",
"job_key": "nightly-backup",
"run_id": "run-2026-08-04",
"sequence": 2,
"type": "step",
"occurred_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"data": { "name": "uploading" }
}'
# 3. Finish
curl -X POST "$PINGSTEP_URL/v1/events" \
-H "Authorization: Bearer $PINGSTEP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt-003",
"job_key": "nightly-backup",
"run_id": "run-2026-08-04",
"sequence": 3,
"type": "succeeded",
"occurred_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'
Python example
No dependencies required — uses only the standard library.
import json, os, urllib.request
URL = os.environ["PINGSTEP_URL"] + "/v1/events"
TOKEN = os.environ["PINGSTEP_TOKEN"]
def send(event_id, job_key, run_id, seq, event_type, data=None):
from datetime import datetime, timezone
payload = {
"event_id": event_id,
"job_key": job_key,
"run_id": run_id,
"sequence": seq,
"type": event_type,
"occurred_at": datetime.now(timezone.utc).isoformat(),
}
if data:
payload["data"] = data
req = urllib.request.Request(
URL,
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
# Usage
send("evt-1", "nightly-backup", "run-42", 1, "started")
send("evt-2", "nightly-backup", "run-42", 2, "step", {"name": "processing"})
send("evt-3", "nightly-backup", "run-42", 3, "succeeded")
Event API reference
POST https://pingstep.dev/v1/events
Send a JSON body with Authorization: Bearer YOUR_JOB_TOKEN and Content-Type: application/json. Maximum body size is 64 KB.
Event fields
| Field | Type | Required | Description |
|---|---|---|---|
event_id | string | yes | Unique identifier for this event. Used for deduplication — sending the same event_id with identical content returns 200 instead of 202. Reusing an event_id with different content returns 409. |
job_key | string | yes | Must match the job key you created in the dashboard. The token must belong to this job. |
run_id | string | yes | An identifier you choose for this run. Use something unique per execution, like a timestamp or UUID. All events in one run share the same run_id. |
sequence | integer | yes | Must be a positive integer (≥ 1) and must increase with each event in a run. The started event must have sequence: 1. |
type | string | yes | One of: started, step, heartbeat, succeeded, failed, cancelled. |
occurred_at | string | yes | RFC 3339 timestamp of when the event happened in your system, e.g. 2026-08-04T14:30:00Z. |
data | object | no | Optional metadata. Must be a JSON object if provided. Required for some event types (see below). |
Event types
| Type | When to send | Required data | Effect |
|---|---|---|---|
started | Beginning of the run | None | Creates the run. Must have sequence: 1. Sets status to Running. |
step | When entering a new stage | data.name (string) — the stage name | Updates the current stage shown on the dashboard. Resets the stale timer. |
heartbeat | When still alive but no new stage | None | Resets the stale timer without changing the displayed stage. |
succeeded | Job completed successfully | None | Terminal — sets the run's final outcome to Succeeded. |
failed | Job failed | data.message (string) — error description | Terminal — sets the run's final outcome to Failed. |
cancelled | Job was intentionally stopped | None | Terminal — accepted as its own event type, but the run's final outcome currently displays as Failed (see below). |
Terminal, but not ingestion-blocking. A terminal event (succeeded, failed, or cancelled) sets a run's final outcome — rebuildRun bounds the projection to the first terminal event, and later events never reopen the run or change that outcome. This is not the same as rejecting later events: if your script still sends one, PingStep stores it rather than discarding it. cancelled is accepted as its own terminal event type, but PingStep does not yet have a distinct Cancelled status in the dashboard — a cancelled run's outcome currently displays as Failed.
HTTP status codes
| Code | Meaning |
|---|---|
202 | Event accepted. |
200 | Duplicate — this event_id was already processed with identical content. |
400 | Validation error — missing or invalid fields. The response body contains {"error": "..."} with details. |
401 | Invalid or missing job token. |
402 | Plan limit exceeded — you have used all your runs for this 30-day window. |
409 | Event ID conflict — this event_id was already used with different content. |
413 | Request body exceeds 64 KB. |
Authentication
PingStep uses three types of tokens:
Job token (ps_job_...) — used to send events. Created when you create a job. Pass it as Authorization: Bearer ps_job_.... Each token is scoped to one job.
Viewer token (ps_view_...) — read-only access to view runs for one job. Share this with someone who needs to monitor a job but should not send events. Pass it as Authorization: Bearer ps_view_....
Session cookie — set automatically when you sign in via GitHub or Google on the dashboard. Gives full access to your jobs and runs.
PingStep stores only the SHA-256 hash of each token. If you lose a token, rotate it from the dashboard — the old token stops working immediately.
Read API
Use these endpoints to read run status programmatically. Authenticate with a viewer token, job token, or session cookie.
GET https://pingstep.dev/v1/runs
Returns all runs visible to the authenticated user or token.
{
"runs": [
{
"job_key": "nightly-backup",
"run_id": "run-2026-08-04",
"status": "running",
"current_step": "uploading",
"is_late": false,
"received_at": "2026-08-04T14:35:00Z",
"stale_transitions": 0,
"late_transitions": 0
}
],
"role": "viewer"
}
GET https://pingstep.dev/v1/runs/{job_key}/{run_id}
Returns a single run. Returns 404 if the run does not exist.
GET https://pingstep.dev/v1/runs/{job_key}/{run_id}/events
Returns all events for a run, with parsed data objects.
{
"events": [
{
"event_id": "evt-001",
"job_key": "nightly-backup",
"run_id": "run-2026-08-04",
"type": "started",
"sequence": 1,
"occurred_at": "2026-08-04T14:30:00Z",
"data": {}
}
]
}
Run status and conditions
Running, Stale, Succeeded, and Failed are the current primary displayed run statuses — a run is always exactly one of them at a time. Late is a secondary condition that can apply alongside Running or Stale, not a status of its own. A cancelled event is accepted as a terminal event type, but currently projects/displays as Failed rather than as a distinct Cancelled status (see below).
Running — PingStep has received a recent update. The dashboard shows the last reported stage.
Stale — Updates stopped arriving after the interval and grace time you configured. This does not mean the job failed — it means PingStep cannot confirm the job is still active. A new step or heartbeat event recovers the run back to Running.
Late — The run has been active longer than expected. This is separate from stale: a run can be both on time (receiving updates) and late (running longer than the expected duration). Late is shown as an additional badge alongside the Running status.
Succeeded — Your script explicitly sent a succeeded event. PingStep never marks a run as succeeded on its own.
Failed — Your script explicitly sent a failed event. PingStep never marks a run as failed on its own.
Cancelled — Your script explicitly sent a cancelled event. PingStep does not yet show a distinct Cancelled status on the dashboard; a cancelled run's outcome currently displays as Failed.
Stale detection
When you create a job, you set two values:
Expected update interval — how often your script normally sends an event (minimum 30 seconds on the Free plan, 15 seconds on Pro/Scale).
Liveness grace period — extra time before PingStep marks the run stale. Defaults to 120 seconds.
The stale deadline is: last liveness event time + interval + grace. Events that reset the stale timer are started, step, and heartbeat. If no liveness event arrives before the deadline, the run becomes Stale. An alert is created, and if the PingStep operator has configured an alert webhook, PingStep notifies it. There is no self-service, per-customer webhook configuration yet.
Example: with a 60-second interval and 120-second grace, the run goes stale after 3 minutes of silence.
Late detection
Late detection is optional. Set it when you know roughly how long a run should take:
Expected duration — how long the run normally takes from start to finish, in seconds.
Late grace — extra time before marking the run late. If not set, defaults to the larger of 300 seconds or 20% of the expected duration.
The late deadline is: start time + expected duration + late grace. A run marked late is still Running — it just has an additional "late" badge. Terminal events (succeeded, failed, cancelled) clear the late flag.
Job configuration
Create jobs from the dashboard or programmatically via POST /v1/jobs (requires session cookie).
| Field | Type | Default | Description |
|---|---|---|---|
job_key | string | — | 2–101 characters. Letters, numbers, dots, dashes, underscores. Must start with a letter or number. |
expected_update_interval_seconds | integer | 60 | How often your script sends an event. |
liveness_grace_seconds | integer | 120 | Extra time before a run is marked stale. |
expected_duration_seconds | integer | — | Optional. Enables late detection. |
late_grace_seconds | integer | — | Optional. Extra time before marking late. Defaults to max(300, ceil(expected_duration × 0.2)). |
Plan limits
| Free | Pro ($19/mo) | Scale ($49/mo) | |
|---|---|---|---|
| Jobs | 1 | 10 | 50 |
| Runs per 30 days | 100 | 10,000 | 50,000 |
| Min update interval | 30s | 15s | 15s |
Plan limits are enforced when creating jobs and when sending events. If you exceed your run limit, POST /v1/events returns 402. View full pricing.