Skip to content

API reference

Endpoints for your server: send a notification, read its status back, and poll decision answers. They use a project API key.

Authentication

Send the key as Authorization: Bearer $PINGLOOM_API_KEY. Sending needs the notifications:send scope; reading status needs events:read. Keys start with pl_live_ in production and pl_test_ everywhere else.

Careful: Never put an API key in a mobile app or browser code. Call PingLoom from your server.

Headers

  • Content-Type: application/json is required. Anything else returns 415.
  • Idempotency-Key is optional: 1 to 255 printable ASCII characters. The same key with the same body returns the original event for 24 hours, with the response header Idempotent-Replayed: true. The same key with a different body returns 409.
  • The 202 response carries Location, the URL to read the status from.

Fields

POST /v1/notifications takes a JSON object. Only title is required.

FieldTypeRequiredRule
channelstringNoDefaults to general. Must match ^[a-z0-9][a-z0-9-]{0,39}$. A well-formed slug that does not exist in the key's project returns 422 INVALID_CHANNEL.
titlestringYes1 to 120 Unicode code points after trimming. No control characters.
bodystringNoUp to 2000 code points. New lines are allowed.
prioritystringNolow, normal or high. Defaults to normal.
event_typestringNoMatches ^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$.
external_idstringNo1 to 200 printable characters. Your own reference for the event.
dataobjectNoA JSON object. Its compact UTF-8 form is at most 8192 bytes. Data is not sent in the push.
action_urlstringNoAn absolute https URL with a host and no credentials, up to 2048 characters. PingLoom never fetches it.
decisionobjectNoAttach a question for your team. prompt (up to 200 characters), options (2 to 5 items of {id, label}, id a slug up to 32 characters, label up to 40), allow_comment (boolean) and expires_in (2h, 30m, 1d or PT2H; 5 minutes to 7 days, default 24 hours). Owners and admins answer; you read the answer back by polling. policy names a decision policy (slug or id) an owner or admin created in the dashboard (Business plan); it sets who may decide and how many approvals are needed. You can only name a policy, never define one, so roles is rejected. requested_by (a member's user id or email) stops a policy's requester from approving their own request. confirm_twice, require_note and require_device_unlock (booleans) add safeguards: a second confirmation screen, a required note of up to 500 characters returned with the answer, and a phone unlock check in the mobile app (checked on the phone only, not verifiable by the server). A card can only switch these on, never turn off a policy's setting. A decision raises priority low to normal. Plan limit: decision cards a month.
occurred_atstringNoRFC 3339 time with an offset, within 365 days in the past and 1 day in the future.

The whole request body is at most 32768 bytes. Any other key returns 422 UNKNOWN_FIELD.

Response

PingLoom answers 202 only after the event is stored. Delivery to devices happens afterwards.

202 response
HTTP/1.1 202 Accepted
Location: /v1/notifications/01J9ZQ8M3K7W4X2B6N5T0R1VCD

{
  "event_id": "01J9ZQ8M3K7W4X2B6N5T0R1VCD",
  "status": "accepted",
  "created_at": "2026-10-22T09:30:00Z",
  "status_url": "/v1/notifications/01J9ZQ8M3K7W4X2B6N5T0R1VCD",
  "idempotent_replay": false,
  "deduplicated": false
}

idempotent_replay is true when the response repeats an earlier request. deduplicated is true when an existing event with the same external_id was returned because the project's deduplication window is on.

Errors

Every error uses one envelope. request_id is what support needs. Validation errors list each problem in details[].

Error envelope
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request has invalid fields.",
    "request_id": "req_01J9ZQ8M3K",
    "details": [
      { "field": "title", "message": "Title is required.", "code": "REQUIRED" }
    ]
  }
}

HTTPCodeWhen
400INVALID_JSONThe body is not valid UTF-8 JSON, or is not an object.
400INVALID_IDEMPOTENCY_KEYThe Idempotency-Key header is not 1 to 255 printable ASCII characters.
401INVALID_API_KEYThe key is missing, unknown, revoked or expired.
403INSUFFICIENT_SCOPEThe key does not have the notifications:send scope.
409IDEMPOTENCY_CONFLICTThe same Idempotency-Key was used with a different request body.
409PROJECT_ARCHIVEDThe key's project is archived.
413PAYLOAD_TOO_LARGEThe request body is over the size limit.
415UNSUPPORTED_MEDIA_TYPEThe Content-Type is not application/json.
422VALIDATION_ERRORA field is invalid. Every problem is listed in details[], each with a field, a message and a code.
422INVALID_CHANNELThe channel slug is well-formed but does not exist in the key's project.
429RATE_LIMITEDToo many requests for this key or IP. Wait for the Retry-After header.
429QUOTA_EXCEEDEDThe workspace used its monthly events. Retry-After and X-PingLoom-Quota-Reset say when the period resets.
5xxINTERNAL_ERRORSomething failed on our side. Nothing was counted. Retry with the same Idempotency-Key.

Validation detail codes

details[].codeWhen
REQUIREDA required field is missing or null.
TOO_LONGA text field is over its maximum length.
TOO_SHORTA text field is shorter than its minimum.
INVALID_FORMATA value has the wrong type or does not match its pattern.
INVALID_PRIORITYpriority is not low, normal or high.
DATA_NOT_OBJECTdata is not a JSON object.
DATA_TOO_LARGEdata is over the size limit.
HTTPS_REQUIREDaction_url uses http.
INVALID_URLaction_url is not a valid https URL.
INVALID_TIMESTAMPoccurred_at is not an RFC 3339 time in range.
UNKNOWN_FIELDThe request has a field PingLoom does not know.

Status endpoint

GET /v1/notifications/{event_id} returns the event's status and a delivery summary. It needs the events:read scope and only sees events from the key's own project. An unknown id, or one from another project, returns 404. See Status codes for the words.

Decision cards

Add a decision object to POST /v1/notifications to ask your team a question. Owners and admins answer from the app or dashboard; PingLoom records the answer and never acts on it. Read the answer back in either of two ways, both with the events:read scope:

  • GET /v1/notifications/{event_id} includes a decision block: status (pending, decided, expired or cancelled), option_id, label, comment, decided_by (user_id and name), decided_at and via.
  • GET /v1/decisions?status=&since=&limit= lists your project's decisions, oldest change first, with the same block plus event_id, external_id, channel and your data. limit is 1 to 100 (default 50). The response has next_cursor and has_more. Send next_cursor back as since; the cursor is opaque, and an empty page returns the cursor you sent. A decision appears again, in its new state, after it is decided or expires.

Limits: 2 to 5 options, option id a slug up to 32 characters, label up to 40, prompt up to 200, comment up to 500, expires_in from 5 minutes to 7 days (default 24 hours). A decision raises priority low to normal. The push never carries the prompt or options. Over the plan's monthly decision limit returns 429 QUOTA_EXCEEDED; decision.policy names a decision policy (slug or id) that an owner or admin created in the dashboard (Business plan). It sets who may decide and how many different people must approve; you can name one but never define one (decision.roles returns 422). Add decision.requested_by so a policy can stop that person approving their own request. The decision then carries policy, outcome and votes. Signed-in users answer with POST /v1/events/{event_id}/decision (409 DECISION_CLOSED once someone has answered).

Downloads