Ping Happens API

Authentication

Every endpoint below (other than logging in) needs a Bearer token belonging to a client account. There is no separate "API key" and no way to create an account through the API — sign up with Create account on the site first. Once you have an account:

  1. POST the email and password for that account to /api/client-auth to retrieve a token.
  2. Send that token on every request that follows, as an Authorization header: Authorization: Bearer <token>.
Two things that trip people up:
  • The token expires 24 hours after it's issued — log in again to get a new one.
  • The token is bound to the exact User-Agent header your client sent when it was issued, and every later request must send that same value. This isn't something you need to configure — a plain curl call already sends a consistent default User-Agent on every request, so it just works. It only matters if you later copy the token into a different tool (a browser, a different HTTP library): that tool's User-Agent won't match, and the token will be rejected. Log in again from whichever tool you intend to keep using.

Log in

POST/api/client-auth

Retrieve a Bearer token for an existing account

Auth: None

FieldType
userstring
passwordstring
cURL request
curl -X POST https://pinghappens.com/api/client-auth \
  -H "Content-Type: application/json" \
  -d '{
    "user": "you@example.com",
    "password": "Str0ngPassw0rd!"
  }'
Response — 200 OK
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2NzAxZjM5YWM0YTFiMmMzZDRlNWY1ZmYiLCJ1c2VyIjoieW91QGV4YW1wbGUuY29tIn0.xH3...",
  "client": {
    "id": "6701f39ac4a1b2c3d4e5f5ff",
    "user": "you@example.com"
  }
}
Response — 401 Unauthorized
{ "error": "Invalid credentials" }
Response — 429 Too Many Requests
{ "error": "Too many failed attempts. Try again in 58 minutes." }

3 failed attempts in a row locks the account for 1 hour; a Retry-After header (in seconds) comes back alongside the 429.

From here on, save the token from the response and send it as Authorization: Bearer <token> — for example, as an environment variable:
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Ownership & visibility

  • You can always read and write your own services and rules.
  • You can never read or write another client's data through the API, even data belonging to a service marked public — public services are for the public status page on the website, not for anonymous or third-party API access. Fetch them from the site itself, or authenticate as the owner.
  • Administrators (staff accounts with the admin role) can read and write any client's data.

Client profile & services

GET/api/client-services/{clientId}

Get a client's profile and services

Auth: Bearer token, must belong to this client

Returns the client's profile and the list of their services, with a check-status summary for each. Only the account owner or an admin receives every service and the account email; anyone else gets neither.

cURL request
curl https://pinghappens.com/api/client-services/6701f39ac4a1b2c3d4e5f5ff \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{
  "client": {
    "_id": "6701f39ac4a1b2c3d4e5f5ff",
    "user": "you@example.com",
    "name": "Ada Lovelace"
  },
  "services": [
    {
      "_id": "6701f3a1c4a1b2c3d4e5f600",
      "resource": "Payments API",
      "user": "you@example.com",
      "ruleCount": 1,
      "public": false,
      "status": "healthy",
      "lastCheckedAt": "2026-10-03T12:05:00.000Z"
    }
  ],
  "authenticated": true
}

Services

POST/api/services

Create a service

Auth: Bearer token, must match the given userId

FieldTypeNotes
userIdstringYour client ID
userstringYour account email
resourcestringService display name
publicbooleanDefaults to false. See Ownership & visibility
rulearrayOne or more rule objects (can be empty)
cURL request
curl -X POST https://pinghappens.com/api/services \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "6701f39ac4a1b2c3d4e5f5ff",
    "user": "you@example.com",
    "resource": "Payments API",
    "public": false,
    "rule": [
      {
        "url": "https://payments.example.com/healthz",
        "method": "GET",
        "cron": "*/5 * * * *"
      }
    ]
  }'
Response — 200 OK
{
  "service": {
    "_id": "6701f3a1c4a1b2c3d4e5f600",
    "userId": "6701f39ac4a1b2c3d4e5f5ff",
    "user": "you@example.com",
    "resource": "Payments API",
    "public": false,
    "active": true,
    "rule": [
      {
        "id": "3f1b1e2a-1c2d-4e3f-9a0b-123456789abc",
        "url": "https://payments.example.com/healthz",
        "method": "GET",
        "cron": "*/5 * * * *",
        "active": true,
        "attemptsByAccess": 1,
        "attemptsDelay": 1,
        "headers": []
      }
    ]
  }
}

Keep that rule's id — you'll need it for the rule endpoints below.

GET/api/services/{id}

Get a service

Auth: Owner, admin, or public — see note

Returns the service, its rules, and up to 300 recent check results per rule. A rule's configuration (headers, payload, action) is only included for the owner or an admin. Each check result omits its response body — fetch that on demand with List a service's history.

cURL request
curl https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600 \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{
  "service": {
    "_id": "6701f3a1c4a1b2c3d4e5f600",
    "userId": "6701f39ac4a1b2c3d4e5f5ff",
    "resource": "Payments API",
    "user": "you@example.com",
    "public": false,
    "active": true,
    "rules": [
      {
        "id": "3f1b1e2a-1c2d-4e3f-9a0b-123456789abc",
        "url": "https://payments.example.com/healthz",
        "method": "GET",
        "attemptsByAccess": 1,
        "attemptsDelay": 1,
        "cron": "*/5 * * * *",
        "active": true,
        "hasAuth": false,
        "headers": [],
        "payload": null,
        "action": null,
        "history": [
          {
            "_id": "6701f402c4a1b2c3d4e5f610",
            "statusCode": 200,
            "responseTime": 184,
            "createdAt": "2026-10-03T12:05:00.000Z",
            "actionStatusCode": null
          }
        ]
      }
    ]
  }
}
PATCH/api/services/{id}

Rename a service

Auth: Owner or admin

FieldType
resourcestring
cURL request
curl -X PATCH https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "resource": "Payments API (EU)" }'
Response — 200 OK
{
  "service": {
    "_id": "6701f3a1c4a1b2c3d4e5f600",
    "resource": "Payments API (EU)"
  }
}
DELETE/api/services/{id}

Delete a service

Auth: Owner or admin

Also deletes the service's check history. This cannot be undone.

cURL request
curl -X DELETE https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600 \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{ "success": true }

Rules

POST/api/services/{id}/rules

Add a rule to a service

Auth: Owner or admin

Body: a rule object.

cURL request
curl -X POST https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600/rules \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://payments.example.com/status",
    "method": "GET",
    "cron": "*/15 * * * *",
    "attemptsByAccess": 2,
    "attemptsDelay": 500
  }'
Response — 200 OK
{
  "rule": {
    "id": "9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d",
    "url": "https://payments.example.com/status",
    "method": "GET",
    "attemptsByAccess": 2,
    "attemptsDelay": 500,
    "cron": "*/15 * * * *",
    "active": true,
    "hasAuth": false,
    "headers": [],
    "payload": null,
    "action": null
  }
}
PATCH/api/services/{id}/rules/{ruleId}

Update a rule

Auth: Owner or admin

Body: any subset of a rule object's fields.

cURL request
curl -X PATCH https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600/rules/9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
Response — 200 OK
{
  "rule": {
    "id": "9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d",
    "url": "https://payments.example.com/status",
    "method": "GET",
    "attemptsByAccess": 2,
    "attemptsDelay": 500,
    "cron": "*/15 * * * *",
    "active": false,
    "hasAuth": false,
    "headers": [],
    "payload": null,
    "action": null
  }
}
DELETE/api/services/{id}/rules/{ruleId}

Delete a rule

Auth: Owner or admin

Removes the rule. Its past check history is kept with the service.

cURL request
curl -X DELETE https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600/rules/9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{ "success": true }

History

GET/api/services/{id}/history

List a service's check history

Auth: Owner or admin

Returns the service's check results newest first, including each check's response body (truncated to 2000 characters), which Get a service omits, and the ruleUrl of the rule that produced it (null if that rule has since been deleted). Results are paginated at up to 100 entries per page, and only checks from the last 6 months are ever returned — older entries are excluded regardless of the filters used. Only the service's owner (or an admin) can call this endpoint, even when the service is public; a service or rule belonging to another account responds exactly like one that doesn't exist.

Query parameterTypeNotes
pagenumberPage number, starting at 1. Defaults to 1
limitnumberEntries per page, 1–100. Defaults to 100
ruleIdstringOptional — only entries for this rule's id
historyIdstringOptional — only the entry with this _id, e.g. from a history entry in Get a service
cURL request
curl "https://pinghappens.com/api/services/6701f3a1c4a1b2c3d4e5f600/history?page=1&ruleId=9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{
  "history": [
    {
      "_id": "6701f402c4a1b2c3d4e5f610",
      "ruleId": "9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d",
      "ruleUrl": "https://payments.example.com/status",
      "statusCode": 200,
      "responseTime": 184,
      "response": "{\"status\":\"ok\"}",
      "actionStatusCode": null,
      "createdAt": "2026-10-03T12:05:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "total": 1,
    "totalPages": 1,
    "hasMore": false
  },
  "since": "2026-04-05T12:05:00.000Z"
}

since is the oldest timestamp the response can include (6 months before the request). Request the next page while pagination.hasMore is true.

Response — 400 Bad Request
{ "error": "limit must be an integer between 1 and 100" }
Response — 401 Unauthorized
{ "error": "Unauthorized" }
Response — 404 Not Found
{ "error": "Rule not found" }

401 means no valid Bearer token was sent. 404 means the service — or the rule given in ruleId — doesn't exist on your account ("Service not found" / "Rule not found"). A historyId that matches nothing returns 200 with an empty history array.

Failure calendar

GET/api/client-calendar

List failed checks by day

Auth: Client Bearer token only (not admin)

Returns the failed checks (status code 400 or higher) of your own services, with one entry per service, rule and day. Each entry is the day's last failure and carries failCount, the number of failures that rule had that day. Days are grouped in the time zone you pass in tz. The account is always taken from your token: there is no client id parameter, and admin tokens are not accepted. Failures of rules that have since been deleted are not included. Response bodies are never returned here; use List a service's history for those.

Query parameterTypeNotes
startstringISO 8601 date/time, inclusive. Required
endstringISO 8601 date/time, exclusive. Required. At most 32 days after start
tzstringIANA time zone used to group days, e.g. America/Sao_Paulo. Required
cURL request
curl "https://pinghappens.com/api/client-calendar?start=2026-10-01T03:00:00.000Z&end=2026-11-01T03:00:00.000Z&tz=America/Sao_Paulo" \
  -H "Authorization: Bearer $TOKEN"
Response — 200 OK
{
  "events": [
    {
      "id": "6701f3a1c4a1b2c3d4e5f600:9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d:2026-10-03",
      "serviceId": "6701f3a1c4a1b2c3d4e5f600",
      "serviceName": "payments-api",
      "ruleId": "9c2b6a4e-5d3f-4a1b-8c7d-0e1f2a3b4c5d",
      "method": "GET",
      "url": "https://payments.example.com/status",
      "statusCode": 503,
      "failCount": 3,
      "day": "2026-10-03",
      "lastFailAt": "2026-10-03T21:40:00.000Z"
    }
  ]
}
Response — 400 Bad Request
{ "error": "end must be after start, within 32 days" }
Response — 401 Unauthorized
{ "error": "Unauthorized" }
GET/api/client-calendar/export

Export failures as CSV

Auth: Client Bearer token only (not admin)

Downloads every failed check (status code 400 or higher) of your own services in the range as a CSV file, one line per failure, oldest first. It takes the same start, end and tz parameters as /api/client-calendar above. The columns are Service, Rule URL, Error Status, Date (YYYY-MM-DD) and Time (HH:mm:ss, 24h), with Date and Time in tz. The file is UTF-8 with a BOM, and failures of deleted rules are not included.

Limited to one export per hour per account, whatever the month. Requests rejected with 400 don't count towards the limit. Within the hour, the response is 429 with a Retry-After header (in seconds).

cURL request
curl "https://pinghappens.com/api/client-calendar/export?start=2026-10-01T03:00:00.000Z&end=2026-11-01T03:00:00.000Z&tz=America/Sao_Paulo" \
  -H "Authorization: Bearer $TOKEN" \
  -o failures.csv
Response — 200 OK (text/csv)
"Service","Rule URL","Error Status","Date","Time"
"payments-api","https://payments.example.com/status","503","2026-10-03","18:35:00"
"payments-api","https://payments.example.com/status","503","2026-10-03","18:40:00"
Response — 429 Too Many Requests
{ "error": "Only one export per hour. Try again in 42 minutes." }

Rule object

FieldTypeNotes
urlstringTarget URL to check, required
methodstringGET | POST | PUT | DELETE
cronstringCron expression, defaults to every 5 minutes
activebooleanDefaults to true
attemptsByAccessnumberRetries per check, 1–3
attemptsDelaynumberDelay between retries in ms, 1–5000
authstringOptional bearer token sent to the target. Write-only — never returned; omit to leave an existing one unchanged
headersarray of { name, value }Extra request headers
payloadobjectJSON request body, for non-GET methods
actionobject or nullOptional webhook, fired when a check's status code matches. Shape: { url, statusCode: number[], headers?, auth? }

In responses, a rule's auth and action.auth are never echoed back — look for the boolean hasAuth (and action.hasAuth) instead to tell whether one is set.

Errors

StatusMeaning
400The request body failed validation. The response body includes an error message.
401Missing, invalid, or insufficient credentials for this resource.
404Not found — also returned instead of 401 for a private resource you don't own, so its existence isn't revealed.
429Too many failed login attempts — the account is temporarily locked. See Log in. Also returned by the CSV export within an hour of the previous export. A Retry-After header is included.
500Unexpected server error.