Menu

Developers

Two ways to get your data out of Pool Check-in and into your own systems: a read-only REST API you call, and signed webhooks we send. Both come with Pool Check-in Plus.

Get a token

An organization admin creates an integration in the admin area, under Integrations. That one integration holds an API token, an optional webhook address and the list of communities it may read.

  1. Create the integration. Copy the token, which starts with cki_. It is shown once. If you lose it, rotate it.
  2. If you want webhooks, give an https address and choose the events. Copy the signing secret, which starts with whsec_. It is also shown once.
  3. Map each community the integration may use to a reference name of your own (the external_id). Data requests only work for mapped communities.

Plans. Creating an integration needs at least one community on Pool Check-in Plus. Organizations that VendoRFP set up directly are not held to a plan.

REST API

Read-only. Nothing you send can change anything in Pool Check-in. The base address is https://app.poolcheckin.com/api/v1.

Authentication

Send the token as a bearer token: Authorization: Bearer cki_…. A token belongs to one organization and sees only the communities mapped to its integration. A community that is not mapped, or belongs to someone else, answers 404 the same way as one that does not exist.

You can add an X-On-Behalf-Of header with a label of your choosing, such as the name of a job. It is written to the audit log and never decides access.

Endpoints

GETParametersReturns
/communitiesnoneEvery community of your organization, with the id you mapped (external_id) where one is set. No resident data.
/poolscommunity_id or external_idThe pools of one mapped community.
/checkinscommunity_id or external_id; since, until, limit, offsetCheck-ins with names, oldest first, with an exact total.
/reportscommunity_id or external_id; kind, since, until, limit, offsetReports, including the fields that hold names.
/reports/{id}community_id or external_idOne report with its photos as signed links that last 5 minutes.
/householdscommunity_id or external_id; page, page_size, qThe household directory.
/residentscommunity_id or external_id; limit, offsetThe roster, with an exact total.

Name the community with community_id (the Pool Check-in id, listed by /communities) or external_id (your own reference name). Time values are ISO 8601.

Paging

limit defaults to 100 and is at most 500 (200 for reports). Use offset to move on. Check-ins, reports and residents return an exact total, so a page is never mistaken for the whole list.

Limits and errors

  • Each token has a per-minute limit, 120 requests by default. Past it you get 429 with a Retry-After header.
  • 401 for a missing, malformed or revoked token. 403 when the organization is not active. 404 for an unmapped community. 503 when we could not answer safely, so retry shortly.
  • Every call is written to an audit log with the time, the address it came from and your X-On-Behalf-Of label. If the audit entry cannot be written, the data is not returned.
  • Check-ins, reports, households and residents include names and addresses. Treat them as personal data.

Example

curl "https://app.poolcheckin.com/api/v1/checkins?external_id=heron-lake&since=2026-07-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer cki_your_token" \
  -H "X-On-Behalf-Of: nightly-export"

The response is { community_id, data: [...], total, limit, offset }.

Webhooks

We post an event to your https address when something happens. Events follow the Standard Webhooks scheme, so its libraries can verify them. The address must resolve to a public server; private addresses are refused.

Events

TypeSent whenWhat it carries
checkin.createdA resident or custom check-in is recorded. Vendors send vendor_visit events instead.Ids, the pool, the kind, when it happened and counts of residents and guests by age group. No names or addresses.
vendor_visit.createdA vendor checks in.Visit id, pool, the vendor company and trade, and the check-in time.
vendor_visit.closedA vendor checks out.The same fields, with the check-out time. Its entity version is 2.
report.submittedA report is submitted: a half-hour round, start or end of day, an amenity inspection or an incident.Report id, pool, kind, schema version, times, how late it was, the photo count and only the fields marked safe to share. Free text is left out.

Events carry ids, counts and times, never names. Each also has community_external_id, the reference name you mapped. An occupancy.updated type is reserved and is not sent yet. Events go only to integrations that subscribed to that type and have the community mapped.

What a delivery looks like

A POST with a JSON body and these headers:

  • webhook-id: the event id. It is the same on every retry.
  • webhook-timestamp: Unix seconds of this attempt.
  • webhook-signature: v1, and a base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the base64 part of your whsec_ secret.
{
  "id": "evt_<event id>",
  "type": "checkin.created",
  "timestamp": "2026-07-04T15:02:11.000Z",
  "entity": { "type": "checkin", "id": "<check-in id>", "version": 1 },
  "data": {
    "checkin_id": "<check-in id>",
    "community_id": "<community id>",
    "pool_id": "<pool id>",
    "kind": "resident",
    "occurred_at": "2026-07-04T15:02:09.000Z",
    "received_at": "2026-07-04T15:02:11.000Z",
    "counts": { "residents_16plus": 2, "residents_under16": 1, "guests_16plus": 0, "guests_under16": 0, "total_party": 3 },
    "community_external_id": "heron-lake"
  }
}

Verify it

Check the signature against the raw body, and reject a timestamp more than five minutes from your clock.

import crypto from "node:crypto";

// secret: the whsec_ value shown once when you created the integration
// rawBody: the request body exactly as received, before any JSON parsing
export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const ts = Number(headers["webhook-timestamp"]);
  if (!id || !Number.isInteger(ts)) return false;
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5 minutes
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(id + "." + ts + "." + rawBody).digest();
  return (headers["webhook-signature"] || "").split(" ").some((part) => {
    const [version, sig] = part.split(",");
    if (version !== "v1" || !sig) return false;
    const got = Buffer.from(sig, "base64");
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}

Answer, retries and repeats

  • Reply with any 2xx within 10 seconds. Anything else, or no answer, counts as a failure.
  • We send the event right after it happens. After a failure we retry after 5 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours and 10 hours, then 10 hours once more: nine attempts in all. After that the event is marked failed.
  • A retry is the same event. Ignore a webhook-id you have already handled.
  • Events can arrive out of order. Each carries entity.version; keep the highest for an entity.

Rotating the secret or token, pausing or revoking an integration is done in the same admin area.

MCP server In development

An MCP server would let an AI assistant set up a community for you. It does not exist yet, and how an assistant would sign in is not decided. Use the API and webhooks above, or Zapier and Make, which take the same webhooks without code.

Ask us about access