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.

Before you start: create a job in the dashboard, copy its job token, and save it as a secret in the environment where the script runs.

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

FieldTypeRequiredDescription
event_idstringyesUnique 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_keystringyesMust match the job key you created in the dashboard. The token must belong to this job.
run_idstringyesAn 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.
sequenceintegeryesMust be a positive integer (≥ 1) and must increase with each event in a run. The started event must have sequence: 1.
typestringyesOne of: started, step, heartbeat, succeeded, failed, cancelled.
occurred_atstringyesRFC 3339 timestamp of when the event happened in your system, e.g. 2026-08-04T14:30:00Z.
dataobjectnoOptional metadata. Must be a JSON object if provided. Required for some event types (see below).

Event types

TypeWhen to sendRequired dataEffect
startedBeginning of the runNoneCreates the run. Must have sequence: 1. Sets status to Running.
stepWhen entering a new stagedata.name (string) — the stage nameUpdates the current stage shown on the dashboard. Resets the stale timer.
heartbeatWhen still alive but no new stageNoneResets the stale timer without changing the displayed stage.
succeededJob completed successfullyNoneTerminal — sets the run's final outcome to Succeeded.
failedJob faileddata.message (string) — error descriptionTerminal — sets the run's final outcome to Failed.
cancelledJob was intentionally stoppedNoneTerminal — 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

CodeMeaning
202Event accepted.
200Duplicate — this event_id was already processed with identical content.
400Validation error — missing or invalid fields. The response body contains {"error": "..."} with details.
401Invalid or missing job token.
402Plan limit exceeded — you have used all your runs for this 30-day window.
409Event ID conflict — this event_id was already used with different content.
413Request 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).

FieldTypeDefaultDescription
job_keystring2–101 characters. Letters, numbers, dots, dashes, underscores. Must start with a letter or number.
expected_update_interval_secondsinteger60How often your script sends an event.
liveness_grace_secondsinteger120Extra time before a run is marked stale.
expected_duration_secondsintegerOptional. Enables late detection.
late_grace_secondsintegerOptional. Extra time before marking late. Defaults to max(300, ceil(expected_duration × 0.2)).

Plan limits

FreePro ($19/mo)Scale ($49/mo)
Jobs11050
Runs per 30 days10010,00050,000
Min update interval30s15s15s

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.