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.
- Create the integration. Copy the token, which starts with
cki_. It is shown once. If you lose it, rotate it. - 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. - 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
| GET | Parameters | Returns |
|---|---|---|
/communities | none | Every community of your organization, with the id you mapped (external_id) where one is set. No resident data. |
/pools | community_id or external_id | The pools of one mapped community. |
/checkins | community_id or external_id; since, until, limit, offset | Check-ins with names, oldest first, with an exact total. |
/reports | community_id or external_id; kind, since, until, limit, offset | Reports, including the fields that hold names. |
/reports/{id} | community_id or external_id | One report with its photos as signed links that last 5 minutes. |
/households | community_id or external_id; page, page_size, q | The household directory. |
/residents | community_id or external_id; limit, offset | The 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
429with aRetry-Afterheader. 401for a missing, malformed or revoked token.403when the organization is not active.404for an unmapped community.503when 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-Oflabel. 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
| Type | Sent when | What it carries |
|---|---|---|
checkin.created | A 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.created | A vendor checks in. | Visit id, pool, the vendor company and trade, and the check-in time. |
vendor_visit.closed | A vendor checks out. | The same fields, with the check-out time. Its entity version is 2. |
report.submitted | A 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 yourwhsec_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
2xxwithin 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-idyou 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.