Pingtower Docs
Docs Reference API reference

API reference

Every endpoint on Pingtower's ingest and API-key data planes, generated from the daemon's own route table.

Every endpoint below answers at https://api.pingtower.com, over TLS — almost all of them with a bearer token. Which token you send decides which plane you are on — the prefixes are mutually exclusive, so one kind can never be mistaken for another.

This page is generated from the daemon’s route table rather than written by hand, and a build gate fails when the two disagree. Nothing here can outlive the code it describes. The same document is served machine-readable at /api/openapi.json, and this page as Markdown at index.md.

It covers the planes an integration talks to: the ingest plane, where your services write events; the API-key plane, where everything else is read and configured; and the slice of the account plane that pairing and push need — linking a hosted account to a self-hosted box, device registration, relay keys. The rest of the account plane — signing up, creating tenants, minting the API keys used here — is the shipped clients' surface and is not documented as an integration point.

Authentication

Requests carry a bearer token; the prefix decides which plane it opens, so one kind of token can never be mistaken for another. The few routes with no plane badge authenticate by the one-shot code or key they redeem.

TokenHeaderGrants
Authorization: Bearer ptp_<tenant>_<secret>A source's agent token for polling assignments and submitting probe results.
API keyAuthorization: Bearer ptk_<tenant>_<secret>A tenant API key. Each operation's x-pingtower-role is the least membership role (owner, admin, member, responder, viewer) the key's live role must meet; GET /v1/roles lists them all.
Authorization: Bearer ptf_<secret>A fleet server's credential for polling assignments and submitting results.
ingest tokenAuthorization: Bearer pti_<tenant>_<secret>A source's ingest token. The tenant segment routes the write; the secret is verified against that tenant's own sources table.
Authorization: Bearer pto_<secret>The daemon operator's credential.
Authorization: BearerHex-encoded HMAC-SHA256 over <timestamp>.<body>, under the relay secret named by the path's key_id.
Authorization: BearerUnix timestamp in seconds, bound into the relay signature and checked for freshness.
session tokenAuthorization: Bearer pts_<secret>An account session token. The account plane only — a session manages tenants and never touches alert data directly.

Versioning

Every response carries X-Pingtower-Version: the server’s release as x.y.z, or dev for a build not cut from a release. A response without the header comes from a server that predates version reporting; treat it as such, never as an error.

Pingtower’s apps and agent identify themselves with a User-Agent product token, matched case-insensitively: Pingtower-iOS/, Pingtower-macOS/, Pingtower-Android/ or Pingtower-Addons/, followed by x.y.z (x.y reads as x.y.0). Only such a clean version is judged. A version with any suffix (1.2.0-debug, 0.0.0-dev+abc) is a development build, and a request with no recognised token (your own scripts, curl, a browser) is never judged at all.

On /v1 routes, a judged client older than the oldest release the server still supports gets its normal answer plus X-Pingtower-Client-Unsupported: <x.y.z>, the oldest supported release. It keeps working; the app asks its user to update.

A judged client older than the oldest release the server can still serve is refused with 426 Upgrade Required before any authentication runs, so the answer is the same with a valid, an expired or no credential. The body is {"error": "client version too old; update the app", "code": "client_outdated", "client": "ios", "min_version": "1.2.0"} (client is one of ios, macos, android, addons; schema ClientOutdated). Branch on code or the status, never on the error text. That minimum is raised only by a release that ships a change older clients cannot work with.

/v1/fleet/*, Pingtower’s own probe fleet, is never refused.

Endpoints

actionsagentalertschecksdevicesfleetingestintegrationslogsmaintenancemcpoauthoncallpairingprojectsrelayreportsrolesrulessourceswebhooks

actions

Operator-defined HTTP runbook actions and their execution log.

GET /v1/projects/{id}/actions API keyviewer role or above

List runbook actions

Every action on the project. Stored headers and bodies marshal as "-" rather than in cleartext.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/actions \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/actions API keymember role or above

Create a runbook action

An action is an HTTP call the daemon makes on your behalf when an alert fires. events defaults to alert.opened; alert.escalated is opt-in per action. The timeout is capped so a slow target cannot pin the worker, and header names and values are length- and charset-limited like every other field here.

headers and body are sealed at rest and never echoed back by a read, so a read-modify-write update that omits them keeps what is stored rather than clearing it.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/actions \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"restart-api","method":"POST",
       "url":"https://ops.internal/restart","timeout_seconds":10}'
GET /v1/projects/{id}/actions/{aid} API keyviewer role or above

Read one runbook action

The action as stored, with its sealed headers and body redacted.

Path parameters

aidREQUIRED
string (hex)

The action id.

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/actions/<aid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/actions/{aid} API keymember role or above

Update a runbook action

An omitted enabled, headers, or body keeps the stored value rather than resetting it — necessary because a read never hands the sealed fields back, so a client that reads then writes cannot resend them. Send body as an explicit empty string to clear it.

Path parameters

aidREQUIRED
string (hex)

The action id.

idREQUIRED
string

The project name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/actions/<aid> \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"restart-api","method":"POST",
       "url":"https://ops.internal/restart","enabled":false}'
DELETE /v1/projects/{id}/actions/{aid} API keymember role or above

Delete a runbook action

Removes the action. Its execution-log rows stay.

Path parameters

aidREQUIRED
string (hex)

The action id.

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/actions/<aid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/actions/{aid}/executions API keyviewer role or above

List an action's executions

The execution log the background drain worker fills in as alert-triggered runs settle, plus any terminal rows from test fires.

Path parameters

aidREQUIRED
string (hex)

The action id.

idREQUIRED
string

The project name.

Query parameters

limit
integer

Maximum execution rows to return.

1 to 500, defaults to 100

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/projects/demo/actions/<aid>/executions?limit=50" \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/actions/{aid}/test API keymember role or above

Test-fire a runbook action

Runs the action once, synchronously, with its stored headers and body, through the same guarded executor and per-action timeout every alert-driven run uses. The result is recorded as one terminal execution-log row rather than a retryable queued one.

A daemon started without a configured executor answers 503.

Path parameters

aidREQUIRED
string (hex)

The action id.

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/actions/<aid>/test \
  -H "Authorization: Bearer ptk_<tenant>_..."

agent

The control channel an agent holding a source's ptp_ credential polls and reports on.

GET /v1/agent/config

Poll a source's assignments

One of the three routes an agent token may call, with results and heartbeat. Answers the checks the credential's source is expected to run, each as an opaque assignment — an id, the addon, its parameters, and a cadence with every value already resolved — and never a tenant, project or source name. version is what the agent holds: a current one is answered with an empty change, a stale one with the whole set, never a delta. wait parks the request until a check in the project changes, the wait elapses, or the daemon drains, exactly as GET /v1/pull does.

The channel carries configuration and nothing else. No assignment can name a command, a path, or a shell string.

Query parameters

version
integer

The config version the agent holds; 0 asks for everything.

0 to 1.073741824e+09, defaults to 0

wait
integer

Seconds to park waiting for a change. 0 returns immediately.

0 to 60, defaults to 0

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/agent/config?version=41&wait=30" \
  -H "Authorization: Bearer ptp_<tenant>_..."
POST /v1/agent/heartbeat

Report that the agent is alive

The agent's own beat, on the channel its credential is confined to. A machine holding an agent token and no ingest token has nowhere else to say it is running, and silence from it is indistinguishable from every target being healthy. The body is the ingest event the agent builds — message, level, keys — carrying its self-metrics and, once a set has been loaded, the assignment gauges: assignments, assignments_rejected, assignments_age_s, assignment_version and supervisor_restarts. assignments_age_s is the number to alert on for a machine cut off from the tower. The source is the credential's own and cannot be claimed; the addon:agent and status:heartbeat tags are the server's, and any addon:, check: or status: tag the agent supplied is discarded, so a beat can never dress itself up as a check result. From there it is an ordinary ingest, evaluated by the project's rules like any other event, and it advances the source's last-seen clock. The body carries ingest's per-key and per-tag bounds.

Request body

Responses

202

Status 202.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/agent/heartbeat \
  -H "Authorization: Bearer ptp_<tenant>_..." \
  -d '{"message":"addons agent eu-1a heartbeat","level":200,
       "keys":{"assignments":12,"assignments_age_s":41,"assignment_version":"7"}}'
POST /v1/agent/results

Post one check result

Ingests the event the agent built for one assignment. The server maps assignment_id to a check the credential holds — an id it does not hold and an id that does not exist are refused identically — then attributes the event itself: the source is the credential's, the check key and the addon:/check: tags are the assignment's, and whatever the agent claimed for them is discarded. From there it is an ordinary ingest, evaluated by the project's rules like any other event.

Keys and tags carry ingest's bounds. The assignment id in message is replaced by the check's name, and a result the substitution pushes past 65536 bytes is cut to that length rather than refused — the agent sent a valid message and could not know the name it would be rewritten with.

Request body

Responses

202

Status 202.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/agent/results \
  -H "Authorization: Bearer ptp_<tenant>_..." \
  -d '{"assignment_id":"asg_...","message":"httpcheck failed: check api-health","level":400,
       "keys":{"status_code":503},"tags":["status:down"]}'

alerts

The alert stream, and acknowledging or resolving what it carries.

POST /v1/alerts/{id}/ack API keyresponder role or above

Acknowledge an alert

Marks the alert as being worked on, which stops its renotify timer without closing it. Acknowledging an alert that is already acked or resolved changes nothing and returns 200 with the alert as it stands; only an unknown id is an error.

Path parameters

idREQUIRED
string

The alert id, as returned by GET /v1/pull.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/alerts/42/ack \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/alerts/{id}/resolve API keyresponder role or above

Resolve an alert

Closes the alert. A resolved alert stops counting against the project's open_alerts and stops renotifying; the matching rule opens a new alert if the condition recurs.

The body is optional. Sending cause and/or fix keeps the history as an incident. When the alert is on no incident, a resolved incident is opened for it with that cause and fix. When it is on one, they are added to that incident as an internal entry. Without a body, the resolve behaves as it always has.

Path parameters

idREQUIRED
string

The alert id, as returned by GET /v1/pull.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/alerts/42/resolve \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/pull API keyviewer role or above

Long-poll the alert stream

Returns alerts newer than cursor, in id order, and the cursor to pass next. With wait set, the request parks until an alert arrives or the wait elapses, so a poller costs one connection rather than a busy loop. The tenant's store is held for the whole park, including the wait.

kind says where an alert came from. "event" is one an arriving event opened, and carries no source. "source_health" is one a rule with a source_silent trigger opened because a source stopped reporting; it carries that source's name in source, so a client never has to read the human message to learn which one went quiet. Both arrive here the same way and through the same integrations, and a source_health alert resolves on its source's next accepted ingest.

A 503 with Retry-After: 5 means the daemon could not open this tenant right now because every tenant it may hold open is in use; poll again after the wait, from the same cursor.

Query parameters

cursor
integer (int64)

The last alert id already seen; 0 starts from the beginning.

at least 0, defaults to 0

wait
integer

Seconds to park waiting for a new alert. 0 returns immediately.

0 to 60, defaults to 0

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/pull?cursor=0&wait=25" \
  -H "Authorization: Bearer ptk_<tenant>_..."

checks

What a project asks its sources to monitor — an addon, its parameters, a cadence.

GET /v1/projects/{id}/checks API keyviewer role or above

List a project's checks

Every check in the project: the addon it names, its parameters, its cadence, the sources expected to run it, and the rule its alert_if wrote, if any. A check is a source of events, not a second alerting path — its results are ordinary ingest events, and every alerting decision about them belongs to a rule.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/checks \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/checks API keymember role or above

Create a check

Names an addon from the closed registry and the parameters to run it with — never a command. An unknown addon, an unknown parameter, or a parameter of the wrong type is refused naming the field; an interval below fifteen seconds, or a timeout at or above the interval, is refused naming both numbers. Every source named must exist in the project, and the target the addon will dial must not resolve into a private, loopback, link-local or metadata range — that refusal says the target was refused and never what it resolved to. The tenant's plan bounds how many checks it may hold, how fast they may run and how many fleet regions one may name; past any of them the answer is 429 naming the limit. interval_seconds is at most 86400 and timeout_seconds at most 3600. An alert_if writes a rule, so a project already holding 500 rules refuses the check with 429 max_rules_per_project.

timeout_seconds defaults to 10 and reads back resolved. A write bumps the project's config_version and releases a parked pull.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/checks \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"api-health","addon":"httpcheck","with":{"url":"https://api.yourdomain.com/healthz"},"interval_seconds":60,"sources":["on-prem-fra"]}'
GET /v1/projects/{id}/checks/{name} API keyviewer role or above

Read one check

The check as stored, with its parameters and the rule alert_if wrote for it.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The check name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/checks/api-health \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/checks/{name} API keymember role or above

Update a check

A check's name is immutable and comes from the path — it is what the event's check key carries and what a rule groups on, so a rename would orphan a running alert's ledger. Sending a different name in the body is rejected; delete and recreate instead. An unknown name is 404, never an upsert. The same registry, bound, source and target refusals apply as on create.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The check name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/checks/api-health \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"addon":"httpcheck","with":{"url":"https://api.yourdomain.com/healthz"},"interval_seconds":120,"sources":["on-prem-fra"]}'
DELETE /v1/projects/{id}/checks/{name} API keymember role or above

Delete a check

Removes the check. A rule its alert_if wrote stays listed and editable, detached rather than deleted — the desugar creates a rule the tenant owns.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The check name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

The 200 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/checks/api-health \
  -H "Authorization: Bearer ptk_<tenant>_..."

devices

Push-notification device registration, scoped to the calling account.

GET /v1/account/devices session token

List this account's device push tokens (account plane)

Every device token registered to the calling session's account, with the platform and last-seen time of each. The session-authenticated counterpart to GET /v1/devices.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

500

The daemon could not complete the request.

Example

curl https://api.pingtower.com/v1/account/devices \
  -H "Authorization: Bearer pts_..."
POST /v1/account/devices session token

Register a device push token (account plane)

The same registry as POST /v1/devices, reached with a session token instead of a tenant API key. Prefer this one.

Registering a handset was never a tenant-scoped act — a device token belongs to an account, and the tenant key was only ever expressing which bearer the client happened to hold. Authenticating it with a tenant key becomes wrong once a tenant's data plane can live on a box the tenant runs: that tenant's only data key is minted by its box, and no data key issued anywhere would reach this registry. A session token has neither problem, and it exists before any tenant is selected.

Body, both token shapes (ios and macos: 64 hex; android: 100 to 1024 characters of [A-Za-z0-9_:.-]), the platform field, critical_alerts, and the 20-per-account limit are identical to POST /v1/devices. The tenant-key routes remain for shipped clients.

Request body

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

422

The request is well-formed but references something that cannot be used.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/account/devices \
  -H "Authorization: Bearer pts_..." \
  -d '{"token":"<64 hex>","platform":"ios","critical_alerts":true}'
DELETE /v1/account/devices/{token} session token

Unregister a device push token (account plane)

The sign-out half of the contract above, session-authenticated. Call it before a different account signs into the same physical device, or the outgoing account's row lingers and keeps pushing to that handset.

Path parameters

tokenREQUIRED
string (hex)

The 64-character hex device token, as registered.

Responses

204

Success, with no response body.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X DELETE https://api.pingtower.com/v1/account/devices/<64 hex> \
  -H "Authorization: Bearer pts_..."
GET /v1/devices API keyviewer role or above

List the account's registered devices

Every device token registered to the calling key's account — not to its tenant. An account in three tenants sees one list, whichever tenant's key it asks with.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/devices \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/devices API keyviewer role or above

Register a device push token

The token is account-scoped, not tenant-scoped: the same handset registered from two tenants resolves to one row per account. Another account registering the same physical device does not retire this account's row.

That makes DELETE /v1/devices/{token} the client's sign-out obligation. Call it before a different account signs in on this device, or the outgoing account keeps receiving its tenant's pushes here. An uninstall is reclaimed without client cooperation — APNs reports the token permanently gone and the daemon prunes it.

The token's shape depends on platform, which must be "ios", "macos" or "android". An ios or macos token is exactly 64 hex characters, the format Apple's token-based provider API expects, and is lowercased before storage so two spellings cannot take two rows for one device. An android token is 100 to 1024 characters of [A-Za-z0-9_:.-] and is stored verbatim: an FCM registration token is case-significant, and normalising it would address a device that does not exist. The two shapes cannot overlap, so a token always identifies its own platform.

critical_alerts is optional and defaults to false. It is the handset's own answer to whether a critical push would actually break through Do Not Disturb, a Focus, or a silenced ringer — an omitted key (every shipped App Store build today) means the daemon must not treat this device as able to receive one. Every registration is a complete, fresh report: re-registering with a changed value overwrites what is stored, in either direction. Where the same token has been registered more than once — by any accounts, in any tenants — and the registrations disagree, every tenant pages the handset critically only if every one of those registrations says true: a registration does not prove who holds the phone, so a false is never overridden by somebody else's true. Which handsets a tenant pages is still decided by its own members' registrations; only whether the page breaks through looks past the tenant. The flip side is that registering a known token with false quiets that handset's critical pages everywhere, though its pages still arrive.

Request body

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/devices \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"token":"<64 hex>","platform":"ios","critical_alerts":true}'
DELETE /v1/devices/{token} API keyviewer role or above

Unregister a device push token

The sign-out half of the contract above. Call this before a different account signs into the same physical device, or the outgoing account's row lingers and keeps pushing to that handset.

Path parameters

tokenREQUIRED
string (hex)

The 64-character hex device token, as registered.

Responses

204

Success, with no response body.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/devices/<64 hex> \
  -H "Authorization: Bearer ptk_<tenant>_..."

fleet

The probe fleet pingtower.com operates — its operator routes, the regions a tenant may name, and the channel a fleet server polls.

POST /v1/fleet/artifacts/{id}

Post a browser run's artifact a member asked for

Answers one entry of the config's uploads list: the raw bytes of the named run's screenshot, video or trace, with Content-Length, which the tower streams straight to the member waiting on it and never stores. Only the server the upload was queued for may post it, once; any other id is 404. `?missing=1` with no body says the file is gone from the box. The caps are 5 MiB for a screenshot and 50 MiB for a video or a trace.

Path parameters

idREQUIRED
string

Responses

204

Success, with no response body.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/fleet/artifacts/<upload id> \
  -H "Authorization: Bearer ptf_..." -H "Content-Type: image/png" \
  --data-binary @screenshot.png
GET /v1/fleet/config

Poll a server's assignments

The fleet-scoped channel: every assignment placed on the calling server, across every tenant, in the same opaque shape the source-scoped channel serves. version is the hash the server holds; an unchanged one is answered with an empty change. It polls rather than parks — a check edit reaches the fleet within one sweep interval. It carries configuration and never a command.

Query parameters

wait
integer

0 to 3600, defaults to 0

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

500

The daemon could not complete the request.

Example

curl "https://api.pingtower.com/v1/fleet/config?version=<hash>" \
  -H "Authorization: Bearer ptf_..."
POST /v1/fleet/heartbeat

Report a server's measured load

observed_slots is what the server measured and version the assignment set it holds; both are operator state on /metrics and reach no tenant.

Request body

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/fleet/heartbeat \
  -H "Authorization: Bearer ptf_..." -d '{"observed_slots":412,"version":"<hash>"}'
POST /v1/fleet/results

Post one assignment's result

The result is attributed from the assignment id — the tenant, project, region source and check the id resolves to on this server — and put through the ordinary ingest under the region's name with the check's tags. An id the server does not hold and an id that does not exist are refused identically. A 503 carries Retry-After: 5 and means the tenant's store could not be opened right now because every tenant the daemon may hold open is in use; nothing was recorded, and the result should be posted again after the wait.

Request body

Responses

202

Status 202.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/fleet/results \
  -H "Authorization: Bearer ptf_..." \
  -d '{"assignment_id":"asg_...","message":"httpcheck failed","level":400}'
GET /v1/regions API keyviewer role or above

List the regions a check may name

The distinct regions with an active server in the caller's plan pool, or with `?pool=browser` in the browser runners' pool, which every plan's browser checks run on; any other pool is 400. Derived, not stored: nothing about the servers behind a region is reported. Naming one on a check materialises the region's source in the project, with a <region>-quiet rule beside it.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/regions -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/servers

List the fleet

Operator plane. Every server with its status, capacity, the load its heartbeat last measured, and how long since it answered the channel.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl https://api.pingtower.com/v1/servers -H "Authorization: Bearer pto_..."
POST /v1/servers

Register a fleet server

Operator plane. Adds one machine to the fleet pingtower.com operates and returns its ptf_ credential exactly once. region is a continent prefix and an IATA city code (eu-fra, us-iad): it becomes a source name in tenant projects permanently, so a compass name is refused. Every server in a region shares one provider; a region with no server yet cannot be provisioned over a name a tenant already holds.

Request body

Responses

201

Created.

401

The request's credential is missing, malformed, or not recognized.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/servers \
  -H "Authorization: Bearer pto_..." \
  -d '{"id":"eu-1a","region":"eu-fra","provider":"hetzner","capacity_slots":600}'
PATCH /v1/servers/{id}

Set a server's status

Operator plane. active serves and admits; draining leaves the placement ring on its next answer so its work re-lands; retired stops the credential resolving.

Path parameters

idREQUIRED
string

The server id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl -X PATCH https://api.pingtower.com/v1/servers/eu-1a \
  -H "Authorization: Bearer pto_..." -d '{"status":"draining"}'

ingest

Writing events into a project from an instrumented service.

GET /v1/heartbeat/{token}

Record that a job ran

A cron job's ping. The pth_ token in the path is the credential, so the URL needs no header and fits a crontab line; GET, HEAD and POST all record a run. The ping is an event on the token's source — message "heartbeat", level 200, tag heartbeat:ok — so it keeps the source alive and resolves its silence alert; a source_silent rule on the source is what pages when the job stops running.

Every bad token answers the same 401. Pings go through the tenant's ingest rate. A pasted URL a chat client previews can record a run, so keep the URL in the crontab.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://api.pingtower.com/v1/heartbeat/pth_<tenant>_...
POST /v1/heartbeat/{token}

Record that a job ran, with its output

GET /v1/heartbeat/{token} with a body: up to 4 KiB of the job's output, kept on the event as its output key. The message stays "heartbeat", so every run of the job shares one template.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

/usr/local/bin/backup.sh 2>&1 | tail -c 4000 | curl -fsS -m 10 --data-binary @- \
  https://api.pingtower.com/v1/heartbeat/pth_<tenant>_...
GET /v1/heartbeat/{token}/fail

Record that a job failed

The job ran and failed: an event on the token's source with message "heartbeat failed", level 500 and tag heartbeat:fail, for a rule on that tag to page on. It also counts as the source being heard from.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

/usr/local/bin/backup.sh || curl -fsS -m 10 https://api.pingtower.com/v1/heartbeat/pth_<tenant>_.../fail
POST /v1/heartbeat/{token}/fail

Record that a job failed, with its output

GET /v1/heartbeat/{token}/fail with up to 4 KiB of the job's output as the body, kept as the event's output key.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

out=$(/usr/local/bin/backup.sh 2>&1) || printf '%s' "$out" | tail -c 4000 | \
  curl -fsS -m 10 --data-binary @- https://api.pingtower.com/v1/heartbeat/pth_<tenant>_.../fail
GET /v1/heartbeat/{token}/start

Record that a job started

The job is starting a run. It records only the start: no event, no stamp on the source's activity clock, and an open missed-run alert stays open, since a job that starts and never finishes has not run. The next ping to /v1/heartbeat/{token} or its /fail finishes the run and carries its duration as the event's duration_ms key. A second start before a finish replaces the first. On a cron source_silent rule, a start with no finish within grace_minutes pages as a missed run. GET, HEAD and POST all record a start.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

5 2 * * * curl -fsS -m 10 https://api.pingtower.com/v1/heartbeat/pth_<tenant>_.../start && /usr/local/bin/backup.sh && curl -fsS -m 10 https://api.pingtower.com/v1/heartbeat/pth_<tenant>_...
POST /v1/heartbeat/{token}/start

Record that a job started (POST)

GET /v1/heartbeat/{token}/start for pingers that only POST. A body is read and dropped.

Path parameters

tokenREQUIRED
string

The source's heartbeat token.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -fsS -m 10 -X POST https://api.pingtower.com/v1/heartbeat/pth_<tenant>_.../start
POST /v1/ingest ingest token

Ingest one event, or a batch

The only route an ingest token may call. The token's tenant segment picks the store; the secret is verified against that tenant's own sources table, and the source it resolves to decides which project the event lands in — nothing in the body names a project.

Levels are HTTP-ish: 100 debug, 200 info, 300 warn, 400 error, 500 critical. Key values arrive as arbitrary JSON scalars and are stringified. The per-field caps are defence in depth below the overall body limit: one event is a log line plus a handful of structured keys, not a bulk payload. Each key name is at most 128 bytes and each value at most 4096 — a string's length, anything else's JSON encoding — and each tag at most 256 characters, the same bound a rule's match.tags carries. An empty tag or key name is still accepted.

A body holds exactly one event, or a batch: up to 1000 events as a JSON array, or as application/x-ndjson with one event per line, all within the same body limit. A batch answers 200 with {accepted, results: [...]}, one result per event in order — status 202 with template_id and alerted, 400 with the event's error, 429 past the ingest rate or the storage quota, or 500 when storing that event failed. Each event takes a token from the tenant's ingest rate, so the events past it answer 429 in their slots while the ones before are kept. A batch of more than 1000 is refused whole with 400. Any other trailing data after one event is refused with 400.

A 503 here, as on every route a pti_, ptk_ or ptp_ credential calls, means the daemon is holding as many tenants open as it is allowed to and every one of them is in use. Nothing was written. It carries Retry-After: 5, and the event should be sent again after it — a 503 is the one refusal of an event that waiting cures.

Request body

Responses

200

Success.

202

Status 202.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/ingest \
  -H "Authorization: Bearer pti_<tenant>_..." \
  -d '{"message":"user bob logged in","level":200,
       "keys":{"name":"bob"},"tags":["auth"]}'

integrations

Typed delivery channels — webhook, Telegram, Slack, and APNs.

GET /v1/projects/{id}/integrations API keyviewer role or above

List typed integrations

Every channel on the project. Credentials are never echoed back. An apns row additionally carries device_count, joined at request time from the account's registered devices.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/integrations \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/integrations API keymember role or above

Create a typed integration

Which credential fields are required depends on type: webhook needs url (secret is optional and minted when absent), telegram needs token and chat_id, slack needs webhook_url, and discord needs webhook_url set to a Discord channel webhook (https://discord.com/api/webhooks/...; any other host is refused). A discord message parses no mentions, so an alert quoting "@everyone" pings nobody.

An apns integration takes no credentials at all. The push provider key is one operator-wide credential rather than a per-project one, and its recipients come from tenant membership — so every credential field is rejected on that type rather than quietly ignored.

A project holds at most 50 webhooks and integrations; past that the create answers 429 naming the limit — "max_integrations_per_project: a project may hold at most 50 webhooks and integrations and 50 exist" — with no Retry-After, because waiting frees nothing. The cap is shared with POST /v1/projects/{id}/webhooks, since both write one list.

The name "pingtower.com relay" is reserved for the row PUT /v1/relay writes, compared ignoring case and surrounding space; a create taking it answers 422 "name is reserved for the relay".

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

The 201 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/integrations \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"type":"slack","name":"eng-alerts",
       "webhook_url":"https://hooks.slack.com/services/..."}'
GET /v1/projects/{id}/integrations/{iid} API keyviewer role or above

Read one integration

The channel as stored, minus its credentials.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/integrations/<iid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/integrations/{iid} API keymember role or above

Update an integration

type is pinned server-side to the stored value and cannot be changed. A blank credential field keeps what is already stored, and an omitted name or events key keeps what is already stored — only a key that is present, including an explicitly empty one, changes anything. Read a channel, edit one field, send it back: nothing you did not mention moves.

The relay row PUT /v1/relay writes is not editable here, whatever the key's role, and answers 409 "managed by /v1/relay" before the body is read. Renaming any other channel to the relay's reserved name answers 422 "name is reserved for the relay".

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Request body

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/integrations/<iid> \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"eng-alerts-oncall"}'
DELETE /v1/projects/{id}/integrations/{iid} API keymember role or above

Delete an integration

Removes the channel and its stored credentials. The relay row PUT /v1/relay writes answers 409 "managed by /v1/relay" instead, whatever the key's role.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/integrations/<iid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/integrations/{iid}/deliveries API keyviewer role or above

List an integration's deliveries

The delivery log for one channel — what was attempted, when, and how it ended. This is where a silently failing channel becomes visible.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Query parameters

limit
integer

Maximum delivery rows to return.

1 to 200, defaults to 50

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/projects/demo/integrations/<iid>/deliveries?limit=50" \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/integrations/{iid}/disable API keymember role or above

Disable an integration

Stops delivery through the channel while keeping its configuration and credentials in place. The relay row answers 409 "managed by /v1/relay": a box that silently paged nobody is the failure its one writer exists to prevent.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/disable \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/integrations/{iid}/enable API keymember role or above

Enable an integration

Resumes delivery through the channel and clears its failure streak — the way back from a circuit-broken endpoint. Not for the relay row, which answers 409 "managed by /v1/relay": PUT /v1/relay rewrites it enabled with a clean streak.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/enable \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/integrations/{iid}/test API keymember role or above

Test-fire an integration

Sends a synthetic alert.test event through the channel synchronously, so you learn whether the credentials work without waiting on the drain tick or opening a real alert. The response reports the channel's own answer, so a 200 here can still carry ok=false with the remote error.

A daemon started without a configured sender answers 503: delivery is optional, and the rest of the API runs without it.

Test-firing the relay row rings every paired phone and spends the same per-tenant relay budget real pages draw on, so it needs an owner or admin key (403 otherwise) and is throttled per tenant — five in a row, then one a minute — with 429 and Retry-After past that.

Path parameters

idREQUIRED
string

The project name.

iidREQUIRED
string

The integration id.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/test \
  -H "Authorization: Bearer ptk_<tenant>_..."

logs

Retained log lines, for the sources configured to keep them.

GET /v1/projects/{id}/logtail API keyviewer role or above

Read, and optionally follow, retained log lines

Two response shapes share one route. `messages` is the original shape: the newest `limit` retained events for the project, newest first — unchanged, and always present. `lines` and `cursor` are additive, and are what a following client reads: `lines` is ascending by id, oldest first, so a tail reads downward. Only sources created or updated with retain_logs set retain anything at all; the rest keep no line-level history, and `retention` on every response says how many of the project's sources are retaining and whether the tenant is currently over its log quota (retention is silently skipped, not queued, once a tenant is over quota, so a quota breach can make a busy incident's tail go quiet with nothing else on the wire to say why).

Omit `after` to open the screen: the newest `limit` lines, ascending. Pass the `cursor` a prior response returned as `after` to continue from there, optionally with `wait` (up to 60 seconds) to long-poll for the next line rather than re-polling on a timer — a request with `after` set and no new lines returns as soon as one is retained, or once `wait` elapses, whichever comes first. `source` and `min_level` narrow `lines` (and the `retention` counts when `source` is set); they do not narrow `messages`, which is always the plain `template`-filtered read.

Retention has no fixed retention window on its own — `Prune` normally clears logs after 7 days or once a source holds more than 10,000 rows, whichever comes first — so a client that stops polling for a while can find its last `cursor` has aged out from under it with no error to say so; this is a tail, not a durable stream, and a gap across a long-enough pause is possible and undetectable from the response alone.

The tail also takes the log explorer's filters: `q` in the explorer's query language (words and "phrases" on the message; source:, env:, host:, run:, trace:, project: and level: fields; a leading - negates), and repeated `env`, `host`, `run`, `level` and `source` parameters, where a source may be project/name. A parse error is 422 with the byte offset. Key terms (name:value on an event key) are refused on the tail with 422; search them through the explorer instead.

Path parameters

idREQUIRED
string

The project name.

Query parameters

after
integer (int64)

Cursor: return lines with id greater than this. Omit to read the newest page instead.

at least 0, defaults to 0

limit
integer

Maximum lines to return.

1 to 200, defaults to 50

min_level
integer

Minimum log level to include (narrows lines only, not retention counts).

0 to 2.147483647e+09, defaults to 0

source
string

Filter to one source name.

at most 128 characters

template
string

Filter to one extracted message template.

at most 128 characters

wait
integer

Seconds (0-60) to long-poll for a new line when none are immediately available. Default 0 (return immediately).

0 to 60, defaults to 0

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/projects/demo/logtail?after=41200&wait=30" \
  -H "Authorization: Bearer ptk_<tenant>_..."

maintenance

Scheduled maintenance windows — one-off or recurring times when alerts are recorded but nobody is paged.

GET /v1/projects/{id}/maintenance-windows API keyviewer role or above

List a project's maintenance windows

Every window in the project: those in progress first, then by next start, then those that will not recur. Each carries active, the occurrence in progress as current (null when none), and up to five next_occurrences as Unix-second spans, so no client evaluates an RRULE itself.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/maintenance-windows \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/maintenance-windows API keymember role or above

Schedule a maintenance window

While a window is in progress, what it covers records without paging — the same semantics as muting a rule: alerts still open, count and show in every feed, but no integration, push or runbook action runs for them and escalation does not climb. At the window's end an alert still firing and unacknowledged pages as it would have; one acknowledged or resolved meanwhile pages no one. Maintenance time is also left out of status-page uptime and of a check's success rate.

A window is one-off (starts_at and ends_at) or recurring (starts_at as DTSTART, duration_minutes and an RFC 5545 rrule such as FREQ=WEEKLY;BYDAY=SU, with optional exdates). Times are local wall clocks, "YYYY-MM-DDTHH:MM", in the IANA timezone given, so 02:00 stays 02:00 across daylight-saving changes; a start the zone skips moves forward by the gap, and one it repeats takes its first instance. FREQ=SECONDLY and MINUTELY are refused, as are DTSTART or TZID inside rrule; an occurrence lasts at most 90 days.

scope is the whole project ({"project": true}) or any set of check, source and rule names. A rule is covered when a window names the rule, the check whose alert_if owns it, or a source its match.sources or its source_silent trigger names. A rule that matches every source is not covered by a window naming one source: name the rule or the project. A window must have an occurrence ending after now. A project holds at most 100 windows still to come or in progress; one that has ended and will not recur is removed 95 days after its last occurrence.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/maintenance-windows \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"Sunday patching","timezone":"Europe/Oslo","starts_at":"2026-10-04T02:00",
       "duration_minutes":120,"rrule":"FREQ=WEEKLY;BYDAY=SU",
       "scope":{"project":false,"checks":["api-health"],"sources":[],"rules":[]}}'
GET /v1/projects/{id}/maintenance-windows/{wid} API keyviewer role or above

Read a maintenance window

One window, as written, with active, current and next_occurrences.

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The window id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/maintenance-windows/mw_0123456789abcdef \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/maintenance-windows/{wid} API keymember role or above

Replace a maintenance window

A full replace, validated as a create is. The change applies from now on: time the window already covered stays covered, with the scope it had then, so uptime already measured does not move.

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The window id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/maintenance-windows/mw_0123456789abcdef \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"Sunday patching","timezone":"Europe/Oslo","starts_at":"2026-10-04T03:00",
       "duration_minutes":90,"rrule":"FREQ=WEEKLY;BYDAY=SU","scope":{"project":true}}'
DELETE /v1/projects/{id}/maintenance-windows/{wid} API keymember role or above

Delete a maintenance window

An occurrence in progress ends now, and the alerts it held that are still firing and unacknowledged page at the next tick. The time it already covered stays excluded from uptime.

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The window id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/maintenance-windows/mw_0123456789abcdef \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/maintenance-windows/{wid}/occurrences API keyviewer role or above

List a maintenance window's next occurrences

The window's next count occurrences as Unix-second spans, the one in progress first.

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The window id.

Query parameters

count
integer

How many occurrences, 1–100 (default 10).

1 to 100, defaults to 10

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/projects/demo/maintenance-windows/mw_0123456789abcdef/occurrences?count=20" \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/maintenance-windows/preview API keyviewer role or above

Preview a maintenance window's occurrences

Validates a window body exactly as a create would, saves nothing, and answers its next count occurrences (the one in progress first), or the same field errors a create would. This is what an editor calls while a schedule is being written.

Path parameters

idREQUIRED
string

The project name.

Query parameters

count
integer

How many occurrences, 1–100 (default 10).

1 to 100, defaults to 10

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST "https://api.pingtower.com/v1/projects/demo/maintenance-windows/preview?count=5" \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"x","timezone":"America/New_York","starts_at":"2026-11-01T01:30",
       "duration_minutes":60,"rrule":"FREQ=DAILY","scope":{"project":true}}'

mcp

The Model Context Protocol endpoint — read-only tools an AI client calls over one tenant's alerts, configuration, on-call and logs.

POST /mcp API keyviewer role or above

Call the MCP server

The Model Context Protocol server, over the streamable-HTTP transport. The body is one JSON-RPC 2.0 message — initialize, notifications/initialized, tools/list or tools/call — sent with Content-Type: application/json and an Accept header naming both application/json and text/event-stream; the answer is one JSON-RPC response as application/json. The server is stateless: it issues no Mcp-Session-Id, every tools/call is one request against the tenant's data rate, and GET and DELETE answer 405. A JSON-RPC batch (a JSON array) is refused with 400, and a body over 256 KiB with 413, before either is parsed.

Six read-only tools, each needing one scope: pingtower_alerts (alerts:read) lists open alerts and those resolved in the last hours; pingtower_projects, pingtower_rules and pingtower_checks (config:read) list projects, one project's alert rules, and one project's checks with only the host or address each probes, or a URL's scheme and host (never its path, query or credentials); pingtower_oncall (oncall:read) names who each of a project's schedules pages now, by email; pingtower_logtail (logs:read) returns the newest retained log lines. A tenant API key is granted every scope. Nothing any tool returns can change a rule, a source, a schedule or an alert.

Text an ingested event wrote — an alert's message, template and identifier values, and every log line — arrives only in fields named untrusted_log_text inside the result's structuredContent, never in its text content, with recognisable secrets replaced by [redacted]. The scrubbing is a best-effort floor, not a guarantee. Every result is capped at 256 KiB and says when it was cut.

A refused credential is a 401 whose WWW-Authenticate header names the protected-resource metadata document and the scopes, which is where an MCP client starts its sign-in.

Responses

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/mcp \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

oauth

The OAuth 2.1 authorization server an MCP client signs in through, and the consent and connected-app routes the portal builds on.

GET /oauth/authorize

Start an authorization

The OAuth 2.1 authorization endpoint (RFC 6749 §4.1), served on the portal host (app.pingtower.com) only; on any other host it is a 404. A browser arrives here from an MCP client and leaves with a 302 to the portal's consent page, /connect/{request}, carrying a cookie that binds the consent to this browser.

Clients are identified by Client ID Metadata Documents: client_id is the https URL of the client's metadata document, and only the exact URLs pingtower.com admits are accepted — Claude's hosted apps, Claude Code and ChatGPT. Dynamic client registration is not offered. redirect_uri must be one the document lists; a loopback redirect URI (http://localhost or http://127.0.0.1) matches with its port ignored. An unknown client_id or unregistered redirect_uri is answered with an error page, never a redirect.

Every other parameter is checked next, and a failure redirects back to redirect_uri with error= (unsupported_response_type, invalid_request, invalid_target or invalid_scope), the client's state, and iss (RFC 9207): response_type must be code; PKCE is required, with code_challenge_method S256; resource must be https://api.pingtower.com/mcp; scope names scopes from alerts:read, logs:read, config:read, oncall:read and offline_access, and every one is requested when it is absent. At most 20 authorizations a client address in a burst, one every five seconds after; 503 when too many are pending across the service.

Responses

302

A redirect; the Location header says where the browser goes next.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

Example

curl -i "https://app.pingtower.com/oauth/authorize?client_id=https%3A%2F%2Fclaude.ai%2Foauth%2Fmcp-oauth-client-metadata&redirect_uri=https%3A%2F%2Fclaude.ai%2Fapi%2Fmcp%2Fauth_callback&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.pingtower.com%2Fmcp&scope=alerts%3Aread+offline_access&state=xyz"
POST /oauth/revoke

Revoke a token

RFC 7009 revocation, for a client that disconnects cleanly. The body is application/x-www-form-urlencoded with token and client_id. A refresh token ends its whole grant; an access token ends only itself. The answer is 200 whatever happened, so it confirms nothing about any token.

Responses

200

Success.

Example

curl -X POST https://api.pingtower.com/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d token=ptn_... \
  -d client_id=https://claude.ai/oauth/mcp-oauth-client-metadata
POST /oauth/token

Redeem a code or rotate a refresh token

The OAuth 2.1 token endpoint. The body is application/x-www-form-urlencoded, as RFC 6749 specifies, with each parameter sent once; clients are public (token_endpoint_auth_method none) and identify themselves with client_id.

grant_type=authorization_code takes code, code_verifier (checked against the S256 challenge), redirect_uri and client_id, which must match the authorization, and resource, which when sent must be https://api.pingtower.com/mcp. A code lives 60 seconds and is single use: a second redemption is refused and revokes the grant the first one issued.

grant_type=refresh_token takes refresh_token and client_id, and rotates: the answer is a new access token and a new refresh token, and the presented refresh token is spent. Presented again within 30 seconds of its rotation it is answered with the same new pair, so two racing refreshes end on one chain; after that, presenting it revokes the whole grant.

The answer carries access_token (pta_, one hour), token_type Bearer, expires_in, scope, and refresh_token (ptn_, 30 days) only when offline_access was granted. Every refusal is RFC 6749 JSON — invalid_request, invalid_client, invalid_grant, unsupported_grant_type or invalid_target — and responses are never cached. Failures count against a per-network ceiling, past which a network is answered 429.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<64 hex> \
  -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk \
  -d redirect_uri=https://claude.ai/api/mcp/auth_callback \
  -d client_id=https://claude.ai/oauth/mcp-oauth-client-metadata \
  -d resource=https://api.pingtower.com/mcp
GET /v1/oauth/requests/{id} session token

Read a pending authorization

The consent page's read. It answers the client (its name, which is text from the client's own document, its client-ID host), the host the browser returns to and whether that is this computer, the requested scopes, the signed-in account's email, and the caller's teams — each marked unavailable, with the reason self_hosted or ai_connectors_off, when it cannot be chosen.

The first signed-in account to read a request claims it. The answer is one 404 unless the request exists, is unexpired and unanswered, the browser presents the cookie /oauth/authorize set for it, and the request is unclaimed or claimed by the caller.

Path parameters

idREQUIRED
string

The request id from /connect/{request}, 32 hex characters.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl https://app.pingtower.com/v1/oauth/requests/<32 hex> \
  -H "Cookie: __Host-pt_session=...; __Host-pt_oauth_<32 hex>=..."
POST /v1/oauth/requests/{id}/approve session token

Approve a pending authorization

Grants the client read access to one team with the scopes the user left ticked, which must be a non-empty subset of what the client asked for. Everything is re-checked here rather than trusted from the read: the caller is a member of the team, the team is hosted by pingtower.com with AI connectors on, and the member holds fewer than 20 connected apps in it. The request is consumed and a single-use code minted in one step, so a double submit mints one code. The answer is the URL to send the browser to, carrying code, state and iss. The same 404 as the read applies.

Path parameters

idREQUIRED
string

The request id, 32 hex characters.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl -X POST https://app.pingtower.com/v1/oauth/requests/<32 hex>/approve \
  -H "Cookie: __Host-pt_session=...; __Host-pt_xsrf=...; __Host-pt_oauth_<32 hex>=..." \
  -H "X-XSRF-Token: ..." -H "Origin: https://app.pingtower.com" \
  -d '{"tenant":"<tenant id>","scopes":["alerts:read","config:read","offline_access"]}'
POST /v1/oauth/requests/{id}/deny session token

Deny a pending authorization

Consumes the request and answers the URL to send the browser to, carrying error=access_denied, state and iss. The same 404 as the read applies.

Path parameters

idREQUIRED
string

The request id, 32 hex characters.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X POST https://app.pingtower.com/v1/oauth/requests/<32 hex>/deny \
  -H "Cookie: __Host-pt_session=...; __Host-pt_xsrf=...; __Host-pt_oauth_<32 hex>=..." \
  -H "X-XSRF-Token: ..." -H "Origin: https://app.pingtower.com"
PUT /v1/tenants/{tid}/ai-actions session tokenadmin role or above

Let AI agents act on the team's alerts, rules, maintenance and checks

Owner or admin only. On lets a connected app use the action scopes it was granted: alerts:write acknowledges and resolves alerts, rules:mute mutes a rule for at most 24 hours, maintenance:write schedules a one-off maintenance window of at most 24 hours, and checks:write creates checks and edits them without disabling them. No agent can delete anything. Off by default; off refuses every action tool at once.

Path parameters

tidREQUIRED
string

The tenant id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X PUT https://api.pingtower.com/v1/tenants/<tid>/ai-actions \
  -H "Authorization: Bearer pts_..." \
  -d '{"enabled":true}'
PUT /v1/tenants/{tid}/ai-connectors session tokenadmin role or above

Turn the team's AI connectors on or off

Owner or admin only. Off stops every connected app for the team on its next call, and refuses new consents for it, without deleting any grant; on restores them. On by default.

Path parameters

tidREQUIRED
string

The tenant id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X PUT https://api.pingtower.com/v1/tenants/<tid>/ai-connectors \
  -H "Authorization: Bearer pts_..." \
  -d '{"enabled":false}'
PUT /v1/tenants/{tid}/ai-incident-writes session tokenadmin role or above

Let AI agents write the team's incidents

Owner or admin only. On lets a connected app holding the incidents:write scope open incidents (as suggestions a person accepts), attach alerts that are on no incident, add internal notes and draft a cause and fix. It never lets one resolve, publish or make anything public. Off by default; off refuses every write tool at once.

Path parameters

tidREQUIRED
string

The tenant id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X PUT https://api.pingtower.com/v1/tenants/<tid>/ai-incident-writes \
  -H "Authorization: Bearer pts_..." \
  -d '{"enabled":true}'
GET /v1/tenants/{tid}/grants session tokenviewer role or above

List the team's connected apps

The team's live OAuth grants. A member sees their own; a holder of members:write also sees those of members the caller outranks (whose team role and every project grant's role the caller could give), so an admin never sees an owner's. A project-limited caller sees only their own, and can_manage is false. The answer also carries the team's AI connectors switch and whether the caller may change it and disconnect other members' apps.

Path parameters

tidREQUIRED
string

The tenant id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl https://api.pingtower.com/v1/tenants/<tid>/grants \
  -H "Authorization: Bearer pts_..."
DELETE /v1/tenants/{tid}/grants/{gid} session tokenviewer role or above

Disconnect an app

Revokes one grant: the caller's own, or — for a holder of members:write — that of a member the caller outranks. Its tokens stop working on their next use. Any other member's grant is a 404, exactly as an unknown one is.

Path parameters

gidREQUIRED
string

The grant id, 24 hex characters.

tidREQUIRED
string

The tenant id.

Responses

204

Success, with no response body.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X DELETE https://api.pingtower.com/v1/tenants/<tid>/grants/<gid> \
  -H "Authorization: Bearer pts_..."

oncall

On-call schedules, their overrides, and who is paging right now.

GET /v1/projects/{id}/oncall/schedules API keyviewer role or above

List on-call schedules

Every schedule on the project, with its layers and participants.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/oncall/schedules \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/oncall/schedules API keymember role or above

Create an on-call schedule

Every participant is checked against the tenant's memberships at write time. A schedule routing through a non-member would silently page nobody, so it is rejected with a 422 listing each offending account rather than accepted and left to fail quietly at 3am.

A project holds at most 50 on-call schedules; past that the create answers 429 naming the limit — "max_schedules_per_project: a project may hold at most 50 on-call schedules and 50 exist" — with no Retry-After, because waiting frees nothing.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/oncall/schedules \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"primary","timezone":"UTC",
       "layers":[{"name":"weekly","participants":["<account_id>","<account_id>"],
                  "rotation_start":1756677600,"shift_minutes":10080}]}'
GET /v1/projects/{id}/oncall/schedules/{sid} API keyviewer role or above

Read one on-call schedule

The schedule as stored, including its layers.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/oncall/schedules/{sid} API keymember role or above

Update an on-call schedule

Replaces the schedule's layers and participants, re-checking every participant against the tenant's memberships.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Request body

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"primary","timezone":"UTC",
       "layers":[{"name":"daily","participants":["<account_id>","<account_id>"],
                  "rotation_start":1756677600,"shift_minutes":1440}]}'
DELETE /v1/projects/{id}/oncall/schedules/{sid} API keymember role or above

Delete an on-call schedule

Refused while a rule's escalation ladder still names the schedule — deleting it would leave that ladder paging nobody. Detach the ladder step first.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/oncall/schedules/{sid}/current API keyviewer role or above

Who is on call right now

Resolves the schedule's layers and any active override against the current time, and answers with the account and its email — or on_call=false when the rotation leaves nobody covering this moment.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

The 200 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/current \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/oncall/schedules/{sid}/overrides API keyviewer role or above

List a schedule's overrides

Every override on the schedule, past and future.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/oncall/schedules/{sid}/overrides API keyresponder role or above

Add an on-call override

A time-bounded substitution covering [starts_at, ends_at). The duration is capped: an override is not a way to permanently reassign a schedule — that belongs in the schedule's own layers — so an effectively-permanent window is rejected at write time rather than silently accepted.

A schedule holds at most 500 current or upcoming overrides; past that the create answers 429 naming max_overrides_per_schedule, with no Retry-After. Overrides that have already ended do not count, so a long-lived rotation is never locked out by its own history.

Path parameters

idREQUIRED
string

The project name.

sidREQUIRED
string

The schedule name.

Request body

Responses

201

Created.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"account_id":"<account_id>","starts_at":1755000000,
       "ends_at":1755028800}'
DELETE /v1/projects/{id}/oncall/schedules/{sid}/overrides/{oid} API keyresponder role or above

Delete an on-call override

Removes the substitution, returning the window to whatever the schedule's layers resolve to.

Path parameters

idREQUIRED
string

The project name.

oidREQUIRED
string

The override id.

sidREQUIRED
string

The schedule name.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides/<oid> \
  -H "Authorization: Bearer ptk_<tenant>_..."

pairing

Linking a hosted account to a box account, and deciding whose phone rings.

POST /v1/pair

Redeem a pairing code for a data credential

The box half of pairing, and the only route in the product that mints a tenant API key without a session. The code is the credential: a one-shot secret the box's operator issued with `pingtower pair` and read out to a person, valid for an hour and unrecoverable from the database afterwards.

What comes back is a `ptk_` this box minted for an account this box holds, so the data plane it opens is the box's own — no assertion signed elsewhere, and nothing that stops working when pingtower.com does. The receipt alongside it is for pingtower.com, which redeems it once at GET /v1/pair/verify to satisfy itself that the person claiming this pairing really did pair.

Every failure of the code itself answers 401 with one message. "Already redeemed" and "no such code" are exactly the distinction a holder of a candidate code would like back, and neither helps the legitimate holder, who has the operator standing next to them.

A seat — a code that names no account — adds three answers, told apart because none of them means "you do not hold a valid code": 400 when the email or password it needs is missing, 403 when the code is bound to a different address, and 401 when the email already has an account here and the password did not match (the code stays live, so a typo does not cost the seat). A seat's email spends the same rate-limit budget as a login to that account. On pingtower.com a seat code answers 409: seats are box-only.

Each member holds at most 100 active API keys in the tenant, the ones pairing mints for them included. At that cap the redemption answers 429 naming max_api_keys — after the code is spent, so the operator issues another once one of that member's keys is revoked.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

curl -X POST https://box.yourdomain.com/v1/pair \
  -d '{"code":"<48 hex>","display_name":"nightowl",
       "email":"[email protected]","password":"<8-72 bytes>"}'
POST /v1/pair/codes API keyadmin role or above

Issue a pairing code for someone joining this box

Mints a one-shot code that admits one person to this tenant, and returns a join code carrying it once this box knows its counterpart on pingtower.com. Manager-gated: issuing a code is the act of admitting somebody, which is the same rank that decides who this tenant's pages reach.

Box only. pingtower.com answers 409: there a manager's ptk_ is a data-plane credential, and a seat would turn it into an account-plane admin with a session. Invite teammates on hosted through POST /v1/tenants/{tid}/invites.

It exists so that adding a teammate does not require a shell on the box. `pingtower pair` is still the bootstrap path and still the one that works when nothing else does — on a new box there is no account, no membership, and no key to authenticate this call with.

The code names a **seat**, not an account: whoever redeems it brings or creates their own account on this box, with a password they choose. Nothing is created here, so a code that is never redeemed leaves nothing behind — unlike the CLI's account-bound form, which creates an account, a membership, and a generated password whether or not anybody turns up.

`email` optionally binds the seat to one address, and is worth setting whenever the issuer knows who they are inviting: an unbound seat is one that whoever holds the code may take, so a leaked code admits a stranger rather than the intended person. A code may not be issued at a role above the issuer's own.

`ttl_seconds` defaults to an hour and is capped at seven days. An hour suits a code read out while somebody is standing there; a seat sent by chat is acted on later.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

The 201 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl -X POST https://box.yourdomain.com/v1/pair/codes \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"role":"member","email":"[email protected]","ttl_seconds":86400}'
GET /v1/pair/verify

Redeem a pairing receipt (called by pingtower.com)

The other end of the callback. pingtower.com fetches this once, while the user is waiting, to learn which of the box's accounts a receipt vouches for — without it, any signed-in account that learned a tenant id could attach itself to that tenant's roster.

It answers the box account id and nothing else, and the receipt is one-shot and short-lived, so a caller who somehow guessed one learns an identifier they had already claimed to know.

Clients do not call this. It is documented because a self-hoster's box must answer it, and because an operator reading their access log is entitled to know what pingtower.com asked for.

Query parameters

receipt
string

The one-shot receipt POST /v1/pair returned.

at most 48 characters

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

503

Status 503.

The 200 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl "https://box.yourdomain.com/v1/pair/verify?receipt=<48 hex>"
POST /v1/tenants/{tid}/pair session token

Link this account to a box account

Records that the caller is a particular account on the tenant's box, and therefore that this account's phones may be paged for that tenant. It grants no data access: on a self-hosted tenant that is the box's to grant, which is also why invite and role-change refuse for these tenants.

The tenant's first pairing is self-authorizing — the account that created the tenant is the one that just set its data plane URL — and is the only one that receives the relay credential, since that credential authorizes paging the whole tenant. Every pairing after it must present a receipt, which pingtower.com redeems against the box before writing anything.

That redemption runs pingtower.com→box, over public HTTPS, through the SSRF-guarded client. So the requirement it implies is worth stating plainly: a box on a private address or a tailnet can pair the person who created the tenant and nobody after. The phone may be on the tailnet; pingtower.com never is. A team's box needs a publicly resolvable HTTPS endpoint, and the 401 this route answers with says so.

Path parameters

tidREQUIRED
string (hex)

The tenant id.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

500

The daemon could not complete the request.

The 201 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl -X POST https://api.pingtower.com/v1/tenants/<tid>/pair \
  -H "Authorization: Bearer pts_..." \
  -d '{"box_account_id":"<box account>","receipt":"<48 hex>"}'
GET /v1/tenants/{tid}/pairings session tokenviewer role or above

List who on this tenant is pageable, and who was

The roster behind the pages. Each row names a hosted account, the box account it is linked to, when the link was made, and `revoked_at` — null while the pairing is live. Revoked rows are returned deliberately: "was paired and is not any more" is what an owner auditing the roster is looking for, and omitting them would make a revocation look identical to never having paired.

A box cannot answer this itself. The pairings live on pingtower.com, so a box's own escalation ladder renders names it has no way to resolve a pairing for — a rung that pages nobody looks exactly like one that works. A client holding both hosts joins this list to the box's ladder on `box_account_id` and can say which is which.

Visible to any member rather than to managers only: knowing whether you yourself are pageable is not privileged, and it is the fact a person most needs before the night they are not woken. A hosted tenant has no box, so it answers 409 rather than an empty list, which would read as a tenant that pages nobody.

Path parameters

tidREQUIRED
string (hex)

The tenant id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

500

The daemon could not complete the request.

Example

curl https://api.pingtower.com/v1/tenants/<tid>/pairings \
  -H "Authorization: Bearer pts_..."
DELETE /v1/tenants/{tid}/pairings/{aid} session tokenadmin role or above

Stop paging one person for this tenant

The act that stops the pages, and the counterpart to removing someone's box membership, which stops their data access. Neither implies the other: doing only the first leaves a leaver reading and writing the data in silence, and doing only the second leaves them receiving pushes that carry log lines.

It writes one column on one row and consults nothing else, so nothing about the tenant's schedules, ladders, or store can make it fail. Revoking the last pairing is allowed and means "this box pages nobody". Re-pairing restores it.

Path parameters

aidREQUIRED
string (hex)

The hosted account id whose pairing is revoked.

tidREQUIRED
string (hex)

The tenant id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

500

The daemon could not complete the request.

Example

curl -X DELETE https://api.pingtower.com/v1/tenants/<tid>/pairings/<aid> \
  -H "Authorization: Bearer pts_..."
POST /v1/tenants/{tid}/relay-key session tokenowner role

Rotate the tenant's relay credential

Mints a fresh relay credential and revokes every earlier one — the tenant-wide kill switch for a box that has been compromised or is misbehaving, as against revoking one person's pairing, which is per person and reversible.

The secret is returned exactly once, and the app is its courier to the box exactly as at pairing. Owner-gated: until the box is reconfigured with the new credential, its pages stop.

Path parameters

tidREQUIRED
string (hex)

The tenant id.

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

500

The daemon could not complete the request.

Example

curl -X POST https://api.pingtower.com/v1/tenants/<tid>/relay-key \
  -H "Authorization: Bearer pts_..."

projects

Projects — the namespace every source, rule, and report lives in.

GET /v1/projects API keyviewer role or above

List the tenant's projects

The project index: every project the API key's tenant owns, each entry carrying its name, config version, source and rule counts, open alert count, and creation time, name-ordered. Paginate with after, which takes the name the previous page ended on.

Query parameters

after
string

Resume after this project name.

at most 200 characters

limit
integer

Maximum projects to return.

1 to 500, defaults to 200

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

The 200 response body here is assembled across branches, so its exact key set depends on the path taken. The example shows the shape; a guessed schema would be worse than the omission.

Example

curl "https://api.pingtower.com/v1/projects?limit=50" \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects API keymember role or above

Create a project

Projects are explicit rows rather than namespaces conjured on first write, so a typo in a later request fails with a 404 instead of silently creating a second project. The name is caller-chosen and is the id every nested route addresses.

Request body

Responses

201

Created.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"demo"}'
DELETE /v1/projects/{id} API keyadmin role or above

Delete a project

Removes the project with its sources, rules, alerts, and reports. Any public status pages the project's reports published stop resolving. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/rename API keyadmin role or above

Rename a project

Changes the name every nested route addresses the project by. Existing ingest tokens keep working — they resolve through the source row, not through the project name. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project's current name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/rename \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"demo-eu"}'

relay

Receiving a self-hosted box's alerts and paging its tenant's phones.

GET /v1/relay API keyviewer role or above

Read whether this box can page a phone

Answers whether a relay credential has been configured on this tenant, and the URL its alerts are sent to. The secret is never returned.

It exists so "does this page anybody?" is a question with an answer. A ladder rung that pages nobody looks identical to one that works, and the version of that discovery nobody wants is the one made during an incident.

So it reports the rows as well as the credential. `relay_url` is what PUT /v1/relay last wrote; `projects` lists every project with what its relay row actually holds — its URL, whether it is enabled, its consecutive failures and last error — and a project with no row is listed with an empty URL, disabled. `enabled` is true only when the relay is configured and every project has a row, enabled, pointing at that URL, so a row the circuit breaker tripped, or a project that never got one, turns it false and `projects` shows which. PUT /v1/relay is the one way to repair either.

A tenant hosted by pingtower.com answers `configured: false` and always will: it has no box, and its pushes go out through its own apns integration instead.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://box.yourdomain.com/v1/relay \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/relay API keyadmin role or above

Configure where this box sends alerts to be paged

Records the relay credential pingtower.com minted at pairing, and wires every project on this tenant to it — the ones that exist now, and the ones created afterwards, which inherit it at creation.

That second half is the point of the route. Delivery endpoints are per project, so configuring a relay by hand would mean pasting a credential once per project, and a project created next week would page nobody with nothing to say so. The endpoint it writes is an ordinary webhook integration named "pingtower.com relay": same outbox, same HMAC signature, same retry and circuit breaker as any endpoint configured by hand. It has this one writer, though: the generic webhook and integration routes refuse to edit, enable, disable or delete it for every role with 409 "managed by /v1/relay", and refuse its name to any other channel. Writing the relay again rewrites every project's row enabled with a clean failure streak, which is how a tripped one is recovered.

The app calls this, not a person. It holds both ends of the pairing moment — the code it just redeemed here, and the credential it just received from pingtower.com — so no operator copies a secret between two web UIs.

Box only. pingtower.com answers 409 and writes nothing: a tenant it hosts has no box to relay from, and the row this route writes is a webhook endpoint its delivery drain would post to.

Manager-gated: it decides where this tenant's alerts go to become pages, which is the same rank that decides who those pages reach. Writing it again replaces the credential rather than adding a second relay, so rotating a compromised key is this call with the new secret.

The secret is write-only. No read route selects it, so a box member cannot recover it from a project they can otherwise see.

`hosted_tenant_id` and `box_public_url` are the other half of the same link, and both are optional. They tell the box which tenant it is on pingtower.com and the URL pingtower.com reaches it at — two facts a box cannot derive, since the ids are separate namespaces and a box behind a proxy does not reliably know the name it is reached by. `pingtower pair` reads them back to print a single join code instead of leaving an operator to read out three strings. Neither is a secret: knowing a tenant id grants nothing, because every pairing but a tenant's first must survive the verify callback.

They are recorded only when both are present, so a half-written counterpart cannot produce a join code that resolves to the wrong place. Omitting them leaves whatever was recorded before untouched.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://box.yourdomain.com/v1/relay \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"relay_url":"https://api.pingtower.com/v1/relay/rk_<id>",
       "relay_secret":"<48 hex>",
       "hosted_tenant_id":"<hosted tenant>",
       "box_public_url":"https://box.yourdomain.com"}'
POST /v1/relay/{key_id}

Relay a self-hosted alert to its tenant's phones

The one inbound route a self-hosted box calls, and the reason self-hosting can page a phone at all. The box's rule fires, its ladder resolves, its outbox delivers the resulting event here, and pingtower.com sends the push under the operator's APNs credential — which is the one thing a box cannot do for itself, because Apple accepts a push for an app's bundle id only from a key issued under the team that owns it.

Clients do not call this. A box does, and it does so as an ordinary HMAC-signed webhook delivery: the body is the same `WebhookEvent` any webhook endpoint receives, signed the same way, retried by the same durable outbox. The relay key id rides in the path because a webhook endpoint holds a URL and a secret and nothing else, so there is nowhere to put a header — which is what keeps the box's half of the relay configuration rather than code.

The tenant comes from the key id and never from the body. A tenant id read out of relayed JSON would let any box page any tenant's phones.

An unknown key id, a revoked key id, and a bad signature answer alike, so the route cannot be used to enumerate live keys. A timestamp more than five minutes from now is refused separately — only a caller already holding the secret can reach that check. The body is capped at 64KB and refused on Content-Length before it is read, and each tenant's relay traffic is rate limited on the far side of the key lookup.

Delivery is at-least-once, so the route is idempotent on (tenant, alert, event, timestamp): a replayed delivery answers 200 without sending a second push. A delivery whose push fails answers 5xx *and* forgets the claim, so the box's retry is treated as a first attempt.

Whose phones ring is decided by `box_pairings`, on every event kind. An escalation's `page_accounts` arrive as box account ids and are translated; every other event kind fans out to the tenant's members narrowed to those holding a live pairing. A box account with no live pairing resolves to no devices either way, which is what makes revoking a pairing stop pages immediately.

Path parameters

key_idREQUIRED
string

The relay credential's id, as minted at pairing or rotation.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

409

The write conflicts with an entity that already exists.

413

The request body exceeds the size limit.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

502

Status 502.

503

Status 503.

Example

# A box does this; the URL is what its operator pasted at pairing.
curl -X POST https://api.pingtower.com/v1/relay/rk_<id> \
  -H "X-Pingtower-Timestamp: 1754870400" \
  -H "X-Pingtower-Signature: <hmac-sha256 of ts.body>" \
  -d '{"event":"alert.opened","alert_id":"a1","project":"api", ...}'
POST /v1/relay/{key_id}/heartbeat

Report that a self-hosted box is alive

A paired box calls this every five minutes, signed exactly like a relay page, so pingtower.com can tell a quiet, healthy box from a dead one — a box otherwise calls only when an alert fires. The body carries the box's version and nothing else: no counts, no tenant data. It pages nobody.

A box opts out with `--relay-heartbeat=false` (`PINGTOWER_RELAY_HEARTBEAT=0`). pingtower.com never contacts a box to learn the same thing.

Refusals match the relay page route's: an unknown or revoked key id and a bad signature answer alike, and a stale timestamp separately.

Path parameters

key_idREQUIRED
string

The relay credential's id, as minted at pairing or rotation.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

413

The request body exceeds the size limit.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

Example

# A box does this on its own timer.
curl -X POST https://api.pingtower.com/v1/relay/rk_<id>/heartbeat \
  -H "X-Pingtower-Timestamp: 1754870400" \
  -H "X-Pingtower-Signature: <hmac-sha256 of ts.body>" \
  -d '{"version":"1.4.2"}'

reports

Incident reports and the shareable public status page behind each one.

GET /v1/projects/{id}/reports API keyviewer role or above

List a project's reports

Every report on the project, with its public UUID and whether it is password-protected.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/reports \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/reports API keymember role or above

Create an incident report

A report gets an unguessable public UUID and a shareable status page at /public/reports/{uuid}. Setting view_password gates that page behind a bcrypt check; without one, anyone holding the UUID can read it. A view_password is 8 to 72 bytes — long enough that a script cannot walk it in minutes, and within bcrypt's limit — and a shorter one is a 400.

alert_id, if set, must name a live alert in the same project.

status_page (default false) publishes the report on the team's status pages. components names the status page components it affects, each with an impact of degraded, partial or major. While a published report has no view password and is not resolved, those components show its impact from the report's creation. A resolution entry, or an entry with stage resolved, ends it.

A project holds at most 200 reports; past that the create answers 429 naming the limit — "max_reports_per_project: a project may hold at most 200 reports and 200 exist" — with no Retry-After, because waiting frees nothing.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/reports \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"title":"API degraded","description":"Gateway timeouts from 03:12 UTC.",
       "alert_id":"<alert_id>"}'
GET /v1/projects/{id}/reports/{rid} API keyviewer role or above

Read one report

The report with its authored entries, newest last.

Path parameters

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/reports/<rid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/reports/{rid} API keymember role or above

Update a report

view_password is tri-state: omitting the key keeps the current password, an explicit empty string clears it and reopens the public page, and any other value replaces it. A replacement is held to the create route's 8 to 72 bytes, and a shorter one is a 400; a report whose password was set shorter before that minimum keeps it, and its page still opens with it.

status_page and components are kept when omitted; components, when sent, replaces the whole list. Setting a view password takes the report off every status page.

Path parameters

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/reports/<rid> \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"title":"API degraded (resolved)","view_password":""}'
DELETE /v1/projects/{id}/reports/{rid} API keyadmin role or above

Delete a report

Removes the report and unpublishes its status page — the public URL 404s from then on.

Path parameters

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/reports/<rid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/reports/{rid}/entries API keymember role or above

Append a report entry

One authored update on the incident timeline. The markdown is rendered to sanitized HTML exactly once, here at write — standard formatting survives, scripts and event handlers do not.

A report holds at most 1000 entries; past that the create answers 429 naming the limit — "max_entries_per_report: a report may hold at most 1000 entries and 1000 exist" — with no Retry-After, because waiting frees nothing.

Path parameters

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"content_md":"Root cause identified; rolling back."}'
PUT /v1/projects/{id}/reports/{rid}/entries/{eid} API keymember role or above

Revise a report entry

Re-renders the entry's markdown through the same sanitizer.

Path parameters

eidREQUIRED
string

The entry id.

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries/<eid> \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"content_md":"Rollback complete; monitoring."}'
DELETE /v1/projects/{id}/reports/{rid}/entries/{eid} API keymember role or above

Delete a report entry

Removes the entry from the public timeline.

Path parameters

eidREQUIRED
string

The entry id.

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries/<eid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/reports/{rid}/rotate-uuid API keymember role or above

Rotate a report's public link

The response to a leaked status-page URL. A fresh UUID is registered, the report re-keyed, and the old UUID dropped, so every copy of the old link 404s immediately.

Path parameters

idREQUIRED
string

The project name.

ridREQUIRED
string

The report id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/reports/<rid>/rotate-uuid \
  -H "Authorization: Bearer ptk_<tenant>_..."

roles

GET /v1/roles API keyviewer role or above

The role table

Returns the caller's role on this tenant, every role from most to least privileged (owner, admin, member, responder, viewer), and the table the server enforces: for each route whose credential carries a membership role, keyed by its exact method and pattern ("POST /v1/alerts/{id}/ack"), the least role it admits and whether a caller below member may use it only for themselves (self_only). A role below a route's min_role gets 403 there. A viewer reads everything and changes nothing; a responder also acknowledges and resolves alerts, mutes rules, works incidents and takes on-call overrides for themselves. Clients use this to show only what the caller can do.

Responses

200

Success.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl "https://api.pingtower.com/v1/roles" \
  -H "Authorization: Bearer ptk_<tenant>_..."

rules

Match conditions that turn events into alerts, and their escalation ladders.

GET /v1/projects/{id}/mutes API keyviewer role or above

List a project's mutes

The project's mutes still in force, one per rule, ordered by rule name, each with when it ends and who set it.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/mutes \
  -H "Authorization: Bearer ptk_<tenant>_..."
GET /v1/projects/{id}/rules API keyviewer role or above

List a project's rules

Every rule in the project, with its match condition, renotify interval, and escalation ladder.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/rules \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/rules API keymember role or above

Create a rule

A rule turns matching events into alerts. Any account a ladder step names is checked against the tenant's memberships at write time: a ladder routing through a non-member would silently page nobody, so it is rejected with a 422 instead.

trigger selects what the rule watches, and there are two kinds. An omitted trigger — or an explicit {"type":"event"} — is the ordinary rule above: it is evaluated against each arriving event. A trigger of {"type":"source_silent","sources":[...],"silent_for_minutes":N} is evaluated on a timer instead, and opens an alert when a named source has *not* reported for N minutes. It needs at least one source, every source must exist, names must not repeat, and N runs from 1 to 525600 (a year).

The two are exclusive, not blendable: on a source_silent rule the event-only fields — match, conditions, fire_after, identifier_keys, dedup_window_minutes and auto_resolve_quiet_minutes — are each rejected by name with a 422. auto_resolve_quiet_minutes is the one worth calling out: a source-health alert is meant to close when ingestion resumes, so closing it merely because the already-quiet alert stayed quiet would hide the outage. Everything describing what happens after an alert opens stays valid — investigate, actions, renotify_minutes, max_renotify and escalations.

One rule naming three sources is three independent monitors: each opens and resolves its own alert, and acknowledging one leaves the others alone. Two rules may watch one source at different horizons, which is how a short warning and a long paging incident coexist.

The clock starts at the later of the source's last_seen_at and the moment this rule began watching it, so a rule added to a source that has never reported — or whose last event is already old — gets one full interval before it can open, rather than paging on history it was not yet observing. Editing a rule's horizon, actions or ladder does not grant a fresh interval; adding a source to the trigger starts that source's own.

Evaluation runs on the daemon's background sweep, so an alert appears within one sweep interval of the horizon rather than exactly on it, and timestamps rather than timers are the authority — a restart loses no progress. Recovery is automatic and immediate: the first accepted ingest from the source resolves the alert in that request's own transaction, whether it was firing or acknowledged, and a later quiet period opens a new alert with a new id rather than reviving the old one.

Every list and number is bounded when the rule is written: match.sources and match.tags hold at most 64 entries (1–128 and 1–256 characters), conditions at most 32 with keys of at most 128 characters, identifier_keys and actions at most 32 entries of 1–128 characters, and every minutes field — escalations[].after_minutes included — and max_renotify at most 525600; fire_after.count is at most 100000. A condition value must be a string of at most 1024 bytes, a number, a boolean or null. A rule stored before a bound existed still loads and evaluates; it is refused only when next written.

A project holds at most 500 rules; past that the create answers 429 naming the limit — "max_rules_per_project: a project may hold at most 500 rules and 500 exist" — with no Retry-After, because waiting frees nothing. Rules a check's alert_if wrote count toward it.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/rules \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"errors","match":{"min_level":400},"renotify_minutes":15}'

curl -X POST https://api.pingtower.com/v1/projects/demo/rules \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"api-quiet","trigger":{"type":"source_silent","sources":["api"],"silent_for_minutes":5}}'
GET /v1/projects/{id}/rules/{name} API keyviewer role or above

Read one rule

The rule as stored, including any escalation ladder.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The rule name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/rules/errors \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/rules/{name} API keymember role or above

Update a rule

A rule's name is immutable and comes from the path. Sending a different name in the body is rejected rather than treated as a rename; delete and recreate instead.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The rule name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/rules/errors \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"match":{"min_level":500},"renotify_minutes":5}'
DELETE /v1/projects/{id}/rules/{name} API keymember role or above

Delete a rule

Stops the rule opening further alerts. Alerts it already opened stay where they are.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The rule name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/rules/errors \
  -H "Authorization: Bearer ptk_<tenant>_..."
PUT /v1/projects/{id}/rules/{name}/mute API keyresponder role or above

Mute a rule

Records without paging until the mute ends: the rule's alerts still open, count and show in every feed, but no integration, push or runbook action runs for them and the escalation ladder does not climb. Give exactly one of minutes or until (Unix seconds), at most 90 days out; muting again replaces the mute. A check or a heartbeat is muted through its rule.

When the mute ends — at until, or on DELETE — an alert that opened during it and is still firing and unacknowledged pages as it would have. One acknowledged or resolved meanwhile pages no one. An alert that paged before the mute still reports its resolution.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The rule name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/rules/nightly-backup%20is%20quiet/mute \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"minutes":120}'
DELETE /v1/projects/{id}/rules/{name}/mute API keyresponder role or above

Unmute a rule

Ends the rule's mute now. The alerts it held back that are still firing and unacknowledged page at the next escalation tick. 404 when the rule is not muted.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The rule name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/rules/errors/mute \
  -H "Authorization: Bearer ptk_<tenant>_..."

sources

Named event producers within a project, each holding an ingest token.

GET /v1/projects/{id}/sources API keyviewer role or above

List a project's sources

Each source is one named event producer. The listing never returns a source's ingest token — that is shown once, when the source is created or its token is rotated.

last_seen_at is when the source last reported, as a Unix timestamp. It advances on every accepted ingest, whether or not that event's line is retained and whether or not it matched a rule, so it is the authority on source activity rather than the log tail — a source configured not to retain logs writes no log row at all. The key is absent, never null and never zero, for a source that has never reported.

monitoring_ack_at is when somebody answered the question "should this source alert if it goes quiet?", either way. It is absent while the question is unanswered, which is the state of every source created before the feature existed; see PUT on this route for how it is set.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/sources \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/sources API keymember role or above

Create a source

Mints the source's pti_ ingest token and returns it once. Store it when you see it; there is no route that reads it back, only rotate-token, which replaces it.

retain_logs decides whether this source's events are kept for GET /v1/projects/{id}/logtail. Omitted on create it means "retain", so a new source keeps its lines unless it asks not to; send false to keep none. It is optional here and required on update. Kept lines are still bounded by the daemon's retention days and per-source cap, which every logtail response reports in `retention`.

A project holds at most 200 sources; past that the create answers 429 naming the limit — "max_sources_per_project: a project may hold at most 200 sources and 200 exist" — with no Retry-After, because waiting frees nothing. The fleet regions a check provisions count toward it but are never refused by it.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

422

The request is well-formed but references something that cannot be used.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/sources \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"name":"api","retain_logs":true}'
PUT /v1/projects/{id}/sources/{name} API keymember role or above

Update a source

retain_logs must be sent explicitly. An omitted field is rejected with a 400 rather than read as false, which would otherwise turn log retention off by accident on any partial update.

monitoring_acknowledged is the opposite shape, and optional. It carries intent rather than a value: send true to record that somebody has decided whether this source's silence should alert — including deciding that it should not — and the server stamps its own clock into monitoring_ack_at. The request is one-way. Sending it again keeps the first answer's timestamp, sending false means "unchanged" rather than "undecide it", and no other field on this route can clear it. Only the call that actually records the answer is written to the audit trail, under source.monitoring_ack rather than the generic source.update.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The source name.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PUT https://api.pingtower.com/v1/projects/demo/sources/api \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"retain_logs":false}'
DELETE /v1/projects/{id}/sources/{name} API keyadmin role or above

Delete a source

Retires the source and its ingest token. Anything still holding that token starts failing authentication on POST /v1/ingest. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The source name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/sources/api \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/sources/{name}/agent-token API keyadmin role or above

Mint a source's agent credential

Issues the source's ptp_ token and returns it once. It is not the ingest token and is accepted nowhere the ingest token is: it lets an agent on that source ask GET /v1/agent/config what the source is expected to monitor and post results to POST /v1/agent/results. Calling again re-mints, which is the rotate — the previous token stops resolving in the same write. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The source name.

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/sources/probe/agent-token \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/sources/{name}/heartbeat-token API keyadmin role or above

Mint a source's heartbeat credential

Issues the source's pth_ token and returns it once — the credential a cron job puts in the URL it pings, GET /v1/heartbeat/{token}. It is neither the ingest token nor the agent token and is accepted only on the heartbeat routes: all it can do is record that this source's job ran or failed. Calling again re-mints, which is the rotate — the previous token stops resolving in the same write. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The source name.

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/sources/nightly-backup/heartbeat-token \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/sources/{name}/rotate-token API keyadmin role or above

Rotate a source's ingest token

Issues a new pti_ token and returns it once, invalidating the previous one immediately — this is the response to a leaked token, so there is no overlap window. Requires an owner or admin key.

Path parameters

idREQUIRED
string

The project name.

nameREQUIRED
string

The source name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/sources/api/rotate-token \
  -H "Authorization: Bearer ptk_<tenant>_..."

webhooks

The plain delivery-endpoint surface over the shared outbox.

GET /v1/projects/{id}/webhooks API keyviewer role or above

List delivery endpoints

Every endpoint on the project, with its enabled state and failure streak.

Path parameters

idREQUIRED
string

The project name.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl https://api.pingtower.com/v1/projects/demo/webhooks \
  -H "Authorization: Bearer ptk_<tenant>_..."
POST /v1/projects/{id}/webhooks API keymember role or above

Create a delivery endpoint

The plain endpoint surface over the shared delivery outbox. Omitting secret makes the server mint one and return it once; omitting events subscribes the endpoint to every event.

A row created here is the same row the typed integrations routes see — the two families are two views onto one table, not two schemas.

A project holds at most 50 webhooks and integrations; past that the create answers 429 naming the limit — "max_integrations_per_project: a project may hold at most 50 webhooks and integrations and 50 exist" — with no Retry-After, because waiting frees nothing. The cap is shared with POST /v1/projects/{id}/integrations, since both write one list.

Path parameters

idREQUIRED
string

The project name.

Request body

Responses

201

Created.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X POST https://api.pingtower.com/v1/projects/demo/webhooks \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"url":"https://example.internal/hooks/pingtower"}'
PATCH /v1/projects/{id}/webhooks/{wid} API keymember role or above

Enable or disable a delivery endpoint

enabled must be sent explicitly — an omitted field is rejected rather than read as false, which would disable the endpoint by accident. Re-enabling clears the endpoint's failure streak, which is the only way to recover a circuit-broken endpoint short of deleting it.

The relay row PUT /v1/relay writes is not changed here, whatever the key's role: it answers 409 "managed by /v1/relay", and writing the relay again is how it is re-enabled.

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The delivery endpoint id.

Request body

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X PATCH https://api.pingtower.com/v1/projects/demo/webhooks/<wid> \
  -H "Authorization: Bearer ptk_<tenant>_..." \
  -d '{"enabled":true}'
DELETE /v1/projects/{id}/webhooks/{wid} API keymember role or above

Delete a delivery endpoint

Removes the endpoint. Deliveries already queued for it are dropped. The relay row PUT /v1/relay writes cannot be deleted here, whatever the key's role; it answers 409 "managed by /v1/relay".

Path parameters

idREQUIRED
string

The project name.

widREQUIRED
string

The delivery endpoint id.

Responses

200

Success.

400

The request body, a path parameter, or a query parameter failed validation.

401

The request's credential is missing, malformed, or not recognized.

403

The key's membership role does not permit this operation.

404

The addressed project or entity does not exist.

409

The write conflicts with an entity that already exists.

429

The tenant is over its plan's rate limit; see Retry-After.

500

The daemon could not complete the request.

503

Status 503.

Example

curl -X DELETE https://api.pingtower.com/v1/projects/demo/webhooks/<wid> \
  -H "Authorization: Bearer ptk_<tenant>_..."
Last updated 1 Sep 2026 Report a problem with this page