Pooli Mobile App Background Pooli Mobile App Background

Pool Club Test strips can ship with your plan. The world's fastest, most accurate test strip scanner.

See the scanner

API Overview

Welcome to the Pooli API, a complementary extension to our intuitive Pooli app interface. While the app provides a user-friendly way to manage your pool, our API opens up additional possibilities, enabling advanced interactions with your pool data programmatically. This powerful combination means that you can now automate routine tasks, integrate with other systems, or even create custom applications, all while continuing to enjoy the ease and familiarity of the Pooli app. The Pooli API serves as your versatile gateway to comprehensive and efficient pool management.

Version: 1.0 (Alpha) - Date: August 4, 2025

Note: This is a beta version of the Pooli API and may be subject to changes and updates.

OpenAPI specification

The machine-readable definition is an OpenAPI 3 spec.

The development host is https://dev-api.pooli.app. api-dev.pooli.app does not resolve, so do not use it.

The spec documents every route, including the legacy alias paths. This page is the full guide.

Key Features

  • Programmatic Pool Management: Seamlessly create and delete pools using our API. This feature allows for dynamic management of pool data, offering flexibility and control to automate your pool maintenance and management tasks.
  • Reading Inventory: Get real-time access to your pool inventory. View available supplies, equipment status, and reorder levels with simple API calls.
  • Managing To-Dos: Stay on top of your pool maintenance tasks. The API provides functionalities to manage and update your to-do lists, helping you prioritize and schedule maintenance activities efficiently.
  • Adding Logs: Easily add detailed logs to track pool maintenance, chemical levels, and more. Our API allows for quick log entries, ensuring you have up-to-date information about your pool's condition.
  • Accessing All Logs: Retrieve comprehensive log data for any specified time range. This feature is ideal for analyzing trends, monitoring pool health over time, and making informed decisions about pool care.

With the Pooli API, you can create custom scripts and applications that leverage these features, streamlining your pool management process and enhancing your pool experience. Our robust, easy-to-use API is designed to empower users, developers, and pool enthusiasts alike to innovate and optimize pool care.

For feature requests, bug reports, or any questions, please email support@pooli.app.

Rate Limiting and Quotas

At Pooli, we have implemented rate limiting to ensure fair and efficient usage of our API. The rate limits vary based on the user's membership status in the Pool Club.

Pool Club Members

Pool Club members enjoy the benefit of unlimited API requests. This privilege allows for more extensive use of the API, facilitating advanced automation, integration, and application development without the concern of hitting usage caps. Discover the huge list of features and benefits of being a Pool Club member and consider signing up to enjoy these advantages.

Non-Club Members

Non-club members are subject to a rate limit of 60 requests per hour. This limit ensures that all users have fair access to the API while preventing excessive use that could impact service quality.

It is important for all users to be aware of these limits to avoid interruptions in service. If you encounter any issues or have questions about our rate limiting policies, please contact support@pooli.app.

Authentication

The Pooli API authenticates every request with a Bearer credential. A long-lived API key is a valid Bearer credential. An access token from POST /v1/auth/token is also a valid Bearer credential. Each key belongs to one user.

Step 1: Generate Your API Key

Create your API key in the Pooli app. The key belongs to the user who created it and provides access to that user's documents, plus any organization where that user is an owner, an admin, or an active technician.

Rate Limiting

Rate limiting is applied on a per-user basis to ensure fair usage and system stability. Be mindful of your request frequency to stay within the rate limits. See Rate Limiting and Quotas.

API Base URLs

The Pooli API is available at the following endpoints:

  • Production: https://api.pooli.app
  • Development: https://dev-api.pooli.app

Use dev-api.pooli.app for development. api-dev.pooli.app does not resolve.

Canonical user paths are /v1/users/{uid}/.... Canonical organization paths are /v1/organizations/{orgId}/.... The legacy aliases /users/{uid}/... and /organizations/{orgId}/... stay supported and are listed in the OpenAPI spec. New integrations should use the /v1 paths.

Using the API Key

Send the long-lived API key on every request that should run as that key:

Authorization: Bearer YOUR_API_KEY

Personal routes under /v1/users are for the key's own user only. The user id in the path must be the user id on the key.

API keys do not expire. A key's expiresAt field is reserved for a future maximum lifetime and is not enforced today. A key stops working only when you revoke it, or when a rotation grace period ends. Access tokens are different: each one has its own expiresAt, 15 minutes by default.

Versioning

API versions are specified in the url. The current version is v1. For example, to fetch all pools for a user:

curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/users/YOUR_USER_ID/pools'

To fetch a specific pool:

curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/users/YOUR_USER_ID/pools/YOUR_POOL_ID'

Replace YOUR_API_KEY, YOUR_USER_ID, and YOUR_POOL_ID with your API key, user ID, and pool ID. An access token can stand in for the API key in the same Authorization header.

Access tokens

Endpoint: POST /v1/auth/token

Description: Exchanges a long-lived API key for an access token. The token lasts 15 minutes. You can request a longer lifetime, up to 60 minutes. Send the access token as Authorization: Bearer YOUR_ACCESS_TOKEN.

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/auth/token'

Endpoint: POST /v1/auth/token/refresh

Description: Issues a new access token from a current access token. The new token follows the same lifetime: 15 minutes, with a maximum of 60 minutes.

curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/auth/token/refresh'

API key lifecycle

These routes manage the caller's API key. Authenticate with that key, or with an access token issued for it.

Regenerate

Endpoint: POST /v1/auth/keys/regenerate

Description: Invalidates outstanding access tokens and keeps the same API key string.

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/auth/keys/regenerate'

Rotate

Endpoint: POST /v1/auth/keys/rotate

Description: Issues a new API key. The previous key keeps working until graceEndsAt. The grace period defaults to 7 days. Set it from 0 to 90 days with gracePeriodDays. The response includes graceEndsAt. After graceEndsAt, the next request with the old key revokes it and its access tokens, and that request returns 403.

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"gracePeriodDays":7}' \
  'https://api.pooli.app/v1/auth/keys/rotate'

Revoke

Endpoint: POST /v1/auth/keys/revoke

Description: Revokes the API key and its access tokens. Revocation takes effect immediately, including during a rotation grace period.

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/auth/keys/revoke'

Acting as another user

On organization routes, the organization owner can send X-Pooli-Act-As set to the user id to act as. The header is owner-only. It is rejected on /v1/users routes.

curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Pooli-Act-As: USER_ID" \
  -H "Content-Type: application/json" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools'

Idempotency-Key

Idempotency-Key is required on creates for customers, organization pools, stops, inventory, and reminders. The key must be 8 to 200 characters. Send a new key for each new create. Send the same key again when you retry that create. The same key with the same body replays the first response. The same key with a different body returns 409.

Idempotency-Key: 6f1c2b4e-9a7d-4c31-8e2a-1b5d0f3a7c88

Roles

Personal routes under /v1/users are for the key's own user only.

Organization routes accept an organization owner, an organization admin, or an active technician, within these limits:

  • Owner and admin: billing (invoices and payments), estimates, service subscriptions, customer create, organization pool create, patch, and delete, CSV import, and technician status.
  • Owner, admin, and active technicians: creating stops, and creating, patching, and deleting organization logs.

An active technician is a user in the organization's technicians collection with status active. Technician status values are active, inactive, and rejected. Sending approved returns 400.

Organization access

There is no separate organization key. A personal API key works on /v1/organizations/{orgId}/... routes when its user is the organization owner, an admin, or an active technician.

  • Read-only resources: customers, routes, stops, invoices, estimates, subscriptions, payments, products, equipment, service visits, and service reports. These routes do not create or update records.
  • Billing reads: invoices, payments, estimates, service subscriptions, and billing fields on customers are limited to owners and admins. Technicians still receive customer name, phone, email, address, notes, and pools.
  • Card data: responses never include card numbers, CVC, expiry, or last4.
  • Writes: no API write calls Stripe or accepts billing fields. Stops store the pool object the app embeds. Reminder create uses the same reminder scheduler as the app. Personal /v1/users writes (pools, logs, inventory, and reminders) require the path uid to match the key user and reject X-Pooli-Act-As.

Route dates: routes use the organization's timeZone when it is an IANA name such as America/Los_Angeles, or timeZoneOffset in minutes (local minus UTC, so PDT is -420). If the organization has neither, dates use UTC, and route responses show timeZoneSource.

See Organization Endpoints for each route.

Errors

Errors are JSON with the HTTP status repeated in the body. Some errors add a details field.

{ "error": "Invalid idempotency key", "message": "Idempotency-Key must be between 8 and 200 characters", "status": 400 }

Pagination

Every list accepts limit (positive integer, maximum 200), cursor, startAfter (an alias of cursor), and envelope=1. By default the body is a JSON array. An empty log page and an empty inventory return null instead of an empty array. envelope=1 returns { data, page }, where page has limit, nextCursor, and hasMore, and data is always an array. Every list also sets the X-Next-Cursor and X-Has-More headers.

Legacy field spellings

supplimentalSanitizers is the intentional field name. cartrige is the intentional filter spelling used by the app, CSV import, and the value stored when filterCategory is omitted on create. Do not correct either spelling in requests.

Endpoint reference

Canonical paths use the /v1 prefix. /users/{uid} is a legacy alias of /v1/users/{uid}, and /organizations/{orgId} is a legacy alias of /v1/organizations/{orgId}. Those aliases stay supported. Each route below is listed once, in the /v1 form.

List endpoints accept limit (positive integer, maximum 200), cursor, startAfter, and envelope. envelope=1 returns { data, page }. Omit envelope to keep the JSON array. Send Authorization: Bearer with an API key or an access token. See Authentication, Idempotency-Key, Act as, and Roles.

Read only. Invoices, payments, estimates, and service subscriptions are read only. There are no invoice, payment, refund, or Stripe writes.

User endpoints

Personal routes under /v1/users are for the key's own user only.

Pools 5

Personal pools for the key user. The user id in the path must be the user id on the key.

GET /v1/users/{uid}/pools

Access: Key user only

Idempotency-Key: Not required

List pools for a user.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools'

# 200
[{"id":"POOL_ID","name":"Main pool"}]

POST /v1/users/{uid}/pools

Access: Key user only

Idempotency-Key: Not required

Create a user pool.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pool":{"name":"Main pool","gallonEstimate":20000}}' \
  'https://api.pooli.app/v1/users/USER_ID/pools'

# 200
{"id":"POOL_ID","name":"Main pool"}

GET /v1/users/{uid}/pools/{poolId}

Access: Key user only

Idempotency-Key: Not required

Get one user pool.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID'

# 200
{"id":"POOL_ID","name":"Main pool"}

PATCH /v1/users/{uid}/pools/{poolId}

Access: Key user only

Idempotency-Key: Not required

Update a user pool.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Main pool"}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID'

# 200
{"id":"POOL_ID","name":"Main pool"}

DELETE /v1/users/{uid}/pools/{poolId}

Access: Key user only

Idempotency-Key: Not required

Soft-delete a user pool.

Example
curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID'

# 200
{"success":true}

Logs 5

Personal pool logs for the key user. Create requires the type query. Implemented types: test, product, maintenance, note, water, temperature, pressure, swg, feeder, swim. For type=maintenance, load task ids from GET /v1/users/{uid}/maintenance-catalog and copy id, name, category, and duration into each maintenances item.

A swim create sends numSwimmers (positive integer), duration (positive integer seconds), and an optional note. The server stores swimTime as numSwimmers times duration and ignores a client swimTime. Invalid values return 400 with error "Validation Error".

GET /v1/users/{uid}/pools/{poolId}/logs

Access: Key user only

Idempotency-Key: Not required

List logs for a user pool.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs'

# 200
[{"id":"LOG_ID","type":"test"}]

POST /v1/users/{uid}/pools/{poolId}/logs

Access: Key user only

Idempotency-Key: Not required

Create a user pool log.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"chemicalReading":{"ph":7.2}}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs?type=test'

# 201
{"success":true,"message":"created","data":"LOG_ID"}
Example: swim
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numSwimmers":2,"duration":1800,"note":"Morning laps","creatorName":"Alex"}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs?type=swim'

# 201
{"success":true,"message":"Log created successfully","data":"6f1c0c2e-4b7a-4e1d-9c3a-8b2d5e7f1a90"}

GET /v1/users/{uid}/pools/{poolId}/logs/{logId}

Access: Key user only

Idempotency-Key: Not required

Get one user pool log.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"id":"LOG_ID","type":"test"}

PATCH /v1/users/{uid}/pools/{poolId}/logs/{logId}

Access: Key user only

Idempotency-Key: Not required

Update a user pool log.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note":"Brushed walls"}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"id":"LOG_ID","type":"test"}

DELETE /v1/users/{uid}/pools/{poolId}/logs/{logId}

Access: Key user only

Idempotency-Key: Not required

Delete a user pool log.

Example
curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"success":true,"message":"ok"}

Inventory 3

Personal inventory for the key user. Create requires Idempotency-Key.

GET /v1/users/{uid}/inventory

Access: Key user only

Idempotency-Key: Not required

List inventory for a user.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/inventory'

# 200
[{"id":"ITEM_ID"}]

POST /v1/users/{uid}/inventory

Access: Key user only

Idempotency-Key: Required

Create a user inventory item.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-001" \
  -H "Content-Type: application/json" \
  -d '{"name":"Chlorine tabs","quantity":1}' \
  'https://api.pooli.app/v1/users/USER_ID/inventory'

# 201
{"id":"ITEM_ID"}

PATCH /v1/users/{uid}/inventory/{itemId}

Access: Key user only

Idempotency-Key: Not required

Update a user inventory item.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity":2}' \
  'https://api.pooli.app/v1/users/USER_ID/inventory/ITEM_ID'

# 200
{"id":"ITEM_ID"}

Reminders 3

Personal reminders for the key user. Create requires reminderType and Idempotency-Key.

GET /v1/users/{uid}/pools/{poolId}/reminders

Access: Key user only

Idempotency-Key: Not required

List reminders for a user pool.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/reminders'

# 200
[{"id":"REMINDER_ID","poolId":"POOL_ID"}]

POST /v1/users/{uid}/pools/{poolId}/reminders

Access: Key user only

Idempotency-Key: Required

Create a user pool reminder.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-001" \
  -H "Content-Type: application/json" \
  -d '{"reminderType":"test","message":"Test the water"}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/reminders'

# 201
{"id":"REMINDER_ID","poolId":"POOL_ID"}

PATCH /v1/users/{uid}/pools/{poolId}/reminders/{reminderId}

Access: Key user only

Idempotency-Key: Not required

Update a user pool reminder.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"Test the water"}' \
  'https://api.pooli.app/v1/users/USER_ID/pools/POOL_ID/reminders/REMINDER_ID'

# 200
{"id":"REMINDER_ID","poolId":"POOL_ID"}

Maintenance catalog 1, read only

Built-in maintenance tasks plus this user's custom tasks (users/{uid}.prefs.customMaintenance). Hidden, disabled, deleted, isDeleted rows, blank names, and custom rows whose category is not custom are omitted. Silencing a task is stored on the pool and does not remove it. aliases is present only when the stored task already has that array. duration is seconds.

GET /v1/users/{uid}/maintenance-catalog

Access: Key user only

Idempotency-Key: Not required

List the maintenance catalog. Copy id, name, category, duration, and aggregationName (when present) into each maintenances item on POST /v1/users/{uid}/pools/{poolId}/logs?type=maintenance. group and custom are ignored on create. Supports limit, cursor, startAfter, and envelope=1. Legacy alias GET /users/{uid}/maintenance-catalog.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/users/USER_ID/maintenance-catalog'

# 200
[{"id":"cleanSkimmers","name":"Empty Skimmer Basket(s)","category":"emptySkimmers","group":"Weekly","custom":false,"duration":345600},{"id":"sweepPool","name":"Brush Walls","category":"sweepWalls","group":"Weekly","custom":false,"duration":1814400},{"id":"oasis-walls","name":"Brush the oasis walls","category":"custom","group":"Weekly","custom":true,"duration":604800}]

Organization endpoints

These routes require an organization owner, an admin, or an active technician, within the limits in Roles. The organization owner can send X-Pooli-Act-As on these routes. See Act as.

Technician management 2

Listing is available to an owner, an admin, or an active technician. If the status query is present, only pending technicians are returned and the value is ignored. Setting status is owner or admin. Write values are active, inactive, or rejected.

GET /v1/organizations/{orgId}/technicians

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Lists technicians. If status is present, only pending technicians are returned and the value is ignored.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/technicians'

# 200
[{"id":"TECH_ID","status":"active"}]

POST /v1/organizations/{orgId}/technicians/{techId}/status

Access: Owner or admin

Idempotency-Key: Not required

Set a technician status.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"active"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/technicians/TECH_ID/status'

# 200
{"success":true}

Pool management 5

List and get are available to an owner, an admin, or an active technician. Create, update, and delete are owner or admin. Create requires Idempotency-Key and returns HTTP 200. The assigned query is compared to isAssigned as a string.

GET /v1/organizations/{orgId}/pools

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List organization pools.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools'

# 200
[{"id":"POOL_ID","name":"Main pool"}]

POST /v1/organizations/{orgId}/pools

Access: Owner or admin

Idempotency-Key: Required

Create an organization pool.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-001" \
  -H "Content-Type: application/json" \
  -d '{"pool":{"name":"Main pool","gallonEstimate":20000}}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools'

# 200
{"id":"POOL_ID","name":"Main pool"}

GET /v1/organizations/{orgId}/pools/{poolId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one organization pool.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID'

# 200
{"id":"POOL_ID","name":"Main pool"}

PATCH /v1/organizations/{orgId}/pools/{poolId}

Access: Owner or admin

Idempotency-Key: Not required

Update an organization pool.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Main pool"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID'

# 200
{"id":"POOL_ID","name":"Main pool"}

DELETE /v1/organizations/{orgId}/pools/{poolId}

Access: Owner or admin

Idempotency-Key: Not required

Soft-delete an organization pool.

Example
curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID'

# 200
{"success":true}

Log management 5

List and get are available to an owner, an admin, or an active technician. Create, update, and delete are too. Create requires the type query. For type=maintenance, load task ids from GET /v1/organizations/{orgId}/maintenance-catalog and copy id, name, category, and duration into each maintenances item.

Implemented types include swim. The swim body matches the user route: numSwimmers, duration in seconds, and an optional note. The stored userId is the key user.

GET /v1/organizations/{orgId}/pools/{poolId}/logs

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List logs for an organization pool.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs'

# 200
[{"id":"LOG_ID","type":"test"}]

POST /v1/organizations/{orgId}/pools/{poolId}/logs

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Create an organization pool log.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"chemicalReading":{"ph":7.2}}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs?type=test'

# 201
{"success":true,"message":"created","data":"LOG_ID"}
Example: swim
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numSwimmers":2,"duration":1800,"note":"Morning laps","creatorName":"Alex"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs?type=swim'

# 201
{"success":true,"message":"Log created successfully","data":"6f1c0c2e-4b7a-4e1d-9c3a-8b2d5e7f1a90"}

GET /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one organization pool log.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"id":"LOG_ID","type":"test"}

PATCH /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Update an organization pool log.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note":"Brushed walls"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"id":"LOG_ID","type":"test"}

DELETE /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Delete an organization pool log.

Example
curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/pools/POOL_ID/logs/LOG_ID'

# 200
{"success":true,"message":"ok"}

Maintenance catalog 1, read only

Owner, admin, or active technician. Same catalog. Built-in defaults shorter than 7 days are raised to 7 days (matches the pro app). Custom tasks come from the effective user's users/{uid}.prefs.customMaintenance (API key user, or the X-Pooli-Act-As user when the owner acts as someone else). There is no organization collection.

GET /v1/organizations/{orgId}/maintenance-catalog

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List the maintenance catalog. Supports limit, cursor, startAfter, and envelope=1. Legacy alias GET /organizations/{orgId}/maintenance-catalog.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/maintenance-catalog'

# 200
[{"id":"cleanSkimmers","name":"Empty Skimmer Basket(s)","category":"emptySkimmers","group":"Weekly","custom":false,"duration":604800},{"id":"sweepPool","name":"Brush Walls","category":"sweepWalls","group":"Weekly","custom":false,"duration":1814400},{"id":"oasis-filter","name":"Clean the filter at the oasis","category":"custom","group":"Monthly","custom":true,"duration":2592000}]

Customers 4

List and get are read only. An active technician receives service fields (name, phone, email, address, notes, and pools). Billing fields are omitted for an active technician. Create and CSV import are owner or admin. Create requires Idempotency-Key and rejects billing fields and payment methods.

GET /v1/organizations/{orgId}/customers

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List organization customers. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/customers'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

POST /v1/organizations/{orgId}/customers

Access: Owner or admin

Idempotency-Key: Required

Create an organization customer.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-001" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sample Customer"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/customers'

# 201
{"id":"RECORD_ID","name":"Sample"}

GET /v1/organizations/{orgId}/customers/{customerId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one organization customer. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/customers/CUSTOMER_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

POST /v1/organizations/{orgId}/import/csv

Access: Owner or admin

Idempotency-Key: Not required

Import customers and pools from CSV.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"csvText":"name,pool_name\nSample Customer,Main pool"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/import/csv'

# 200
{"jobId":"JOB_ID","counts":{"created":1}}

Routes 2, read only

Read only. A route is the active stops for one technician on one local calendar date, derived from stops. Dates use the organization time zone, or UTC when the organization has no usable zone.

GET /v1/organizations/{orgId}/routes

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List routes. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/routes?date=2026-10-19'

# 200
[{"id":"2026-10-19:USER_ID","organizationId":"ORG_ID","date":"2026-10-19","stopIds":["STOP_ID"],"timeZoneSource":"utc-fallback"}]

GET /v1/organizations/{orgId}/routes/{routeId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one route. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/routes/2026-10-19:USER_ID'

# 200
{"id":"2026-10-19:USER_ID","organizationId":"ORG_ID","date":"2026-10-19","stopIds":["STOP_ID"],"timeZoneSource":"utc-fallback"}

Stops 3

List and get are read only. Create is available to an owner, an admin, or an active technician and requires Idempotency-Key. poolId and scheduledDateTime are required.

GET /v1/organizations/{orgId}/stops

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List route stops. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/stops'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

POST /v1/organizations/{orgId}/stops

Access: Owner, admin, or active technician

Idempotency-Key: Required

Create a route stop.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-001" \
  -H "Content-Type: application/json" \
  -d '{"poolId":"POOL_ID","scheduledDateTime":"2026-10-19T15:00:00.000Z"}' \
  'https://api.pooli.app/v1/organizations/ORG_ID/stops'

# 201
{"id":"RECORD_ID","name":"Sample"}

GET /v1/organizations/{orgId}/stops/{stopId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one route stop. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/stops/STOP_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Billing

Read only. Invoices, payments, estimates, and service subscriptions are read only. There are no invoice, payment, refund, or Stripe writes.

Invoices 2, read only

Read only. Owner or admin. An active technician receives 403.

GET /v1/organizations/{orgId}/invoices

Access: Owner or admin

Idempotency-Key: Not required

List invoices. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/invoices'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/invoices/{invoiceId}

Access: Owner or admin

Idempotency-Key: Not required

Get one invoice. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/invoices/INVOICE_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Payments 2, read only

Read only. Owner or admin. An active technician receives 403. The payment id is parentType~parentId~paymentId.

GET /v1/organizations/{orgId}/payments

Access: Owner or admin

Idempotency-Key: Not required

List payments. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/payments?invoiceId=INVOICE_ID'

# 200
[{"id":"invoice~INVOICE_ID~PAY_ID","amount":120}]

GET /v1/organizations/{orgId}/payments/{paymentId}

Access: Owner or admin

Idempotency-Key: Not required

Get one payment. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/payments/invoice~INVOICE_ID~PAY_ID'

# 200
{"id":"invoice~INVOICE_ID~PAY_ID","amount":120}

Estimates 2, read only

Read only. Owner or admin. An active technician receives 403.

Read only. Invoices, payments, estimates, and service subscriptions are read only. There are no invoice, payment, refund, or Stripe writes.

GET /v1/organizations/{orgId}/estimates

Access: Owner or admin

Idempotency-Key: Not required

List estimates. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/estimates'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/estimates/{estimateId}

Access: Owner or admin

Idempotency-Key: Not required

Get one estimate. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/estimates/ESTIMATE_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Service subscriptions 2, read only

Read only. Owner or admin. An active technician receives 403. These are organization service billing subscriptions, not Pool Club subscriptions.

Read only. Invoices, payments, estimates, and service subscriptions are read only. There are no invoice, payment, refund, or Stripe writes.

GET /v1/organizations/{orgId}/subscriptions

Access: Owner or admin

Idempotency-Key: Not required

List service subscriptions. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/subscriptions'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/subscriptions/{subscriptionId}

Access: Owner or admin

Idempotency-Key: Not required

Get one service subscription. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/subscriptions/SUBSCRIPTION_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Products 2, read only

Read only product catalog for an owner, an admin, or an active technician.

GET /v1/organizations/{orgId}/products

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List the organization product catalog. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/products'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/products/{productId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one catalog product. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/products/PRODUCT_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Equipment 2, read only

Read only installed equipment for an owner, an admin, or an active technician.

GET /v1/organizations/{orgId}/equipment

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List installed equipment. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/equipment'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/equipment/{equipmentId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one installed equipment item. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/equipment/EQUIPMENT_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Service visits 2, read only

Read only. Customer signatures are omitted. Available to an owner, an admin, or an active technician.

GET /v1/organizations/{orgId}/service-visits

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List service visits. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/service-visits'

# 200
[{"id":"RECORD_ID","name":"Sample"}]

GET /v1/organizations/{orgId}/service-visits/{visitId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one service visit. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/service-visits/VISIT_ID'

# 200
{"id":"RECORD_ID","name":"Sample"}

Service reports 2, read only

Read only projection of service visits that are completed, sending a report, or already have report fields.

GET /v1/organizations/{orgId}/service-reports

Access: Owner, admin, or active technician

Idempotency-Key: Not required

List service reports. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/service-reports'

# 200
[{"id":"VISIT_ID","poolId":"POOL_ID","visitId":"VISIT_ID"}]

GET /v1/organizations/{orgId}/service-reports/{visitId}

Access: Owner, admin, or active technician

Idempotency-Key: Not required

Get one service report. Read only.

Example
curl -X GET \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/organizations/ORG_ID/service-reports/VISIT_ID'

# 200
{"id":"VISIT_ID","poolId":"POOL_ID","visitId":"VISIT_ID"}

Other routes

Content ingest is not organization scoped. Token and key routes are listed here in canonical form. The notes under Authentication stay the guide for tokens, rotation, act-as, and roles.

Content ingest 2

Service route for the key user. If the key has a non-empty scopes array, it must include content_management. Keys with no scopes array are allowed. Act-as is rejected.

POST /v1/content-ingest

Access: Key user only. If scopes is non-empty, it must include content_management.

Idempotency-Key: Not required

Create a content ingest item.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}' \
  'https://api.pooli.app/v1/content-ingest'

# 201
{"id":"ITEM_ID"}

PATCH /v1/content-ingest/{id}

Access: Key user only. If scopes is non-empty, it must include content_management.

Idempotency-Key: Not required

Patch a content ingest item.

Example
curl -X PATCH \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}' \
  'https://api.pooli.app/v1/content-ingest/ITEM_ID'

# 200
{"id":"ITEM_ID"}

Token and key routes 5

Canonical paths for the access token and API key routes described under Authentication. Idempotency-Key is not used on these routes.

POST /v1/auth/token

Access: Key user only

Idempotency-Key: Not required

Exchange a long-lived API key for a short-lived access token.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expiresInSeconds":900}' \
  'https://api.pooli.app/v1/auth/token'

# 201
{"accessToken":"ACCESS_TOKEN","tokenType":"Bearer","expiresAt":"2026-10-19T15:00:00.000Z","expiresInSeconds":900}

POST /v1/auth/token/refresh

Access: Key user only

Idempotency-Key: Not required

Refresh a short-lived access token.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expiresInSeconds":900}' \
  'https://api.pooli.app/v1/auth/token/refresh'

# 201
{"accessToken":"ACCESS_TOKEN","tokenType":"Bearer","expiresAt":"2026-10-19T15:00:00.000Z","expiresInSeconds":900}

POST /v1/auth/keys/regenerate

Access: Key user only

Idempotency-Key: Not required

Revoke short-lived tokens for this API key.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/auth/keys/regenerate'

# 200
{"generation":1,"tokensRevoked":1,"keyRotated":false}

POST /v1/auth/keys/rotate

Access: Key user only

Idempotency-Key: Not required

Issue a new API key and start the grace period on this key.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"gracePeriodDays":7}' \
  'https://api.pooli.app/v1/auth/keys/rotate'

# 200
{"apiKey":"NEW_API_KEY","previousKeyId":"PREVIOUS_KEY_ID","gracePeriodDays":7,"graceEndsAt":"2026-10-19T15:00:00.000Z","status":"rotating"}

POST /v1/auth/keys/revoke

Access: Key user only

Idempotency-Key: Not required

Revoke an API key before its grace period ends.

Example
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  'https://api.pooli.app/v1/auth/keys/revoke'

# 200
{"apiKeyId":"API_KEY_ID","status":"revoked"}

Spec and docs 4

Public files. No API key is required.

GET /docs

Access: Public

Idempotency-Key: Not required

API documentation page.

Example
curl -X GET \
  'https://api.pooli.app/docs'

# 200
# HTML documentation page

GET /openapi.json

Access: Public

Idempotency-Key: Not required

OpenAPI document.

Example
curl -X GET \
  'https://api.pooli.app/openapi.json'

# 200
# OpenAPI JSON document

GET /v1/docs

Access: Public

Idempotency-Key: Not required

API documentation page (versioned alias).

Example
curl -X GET \
  'https://api.pooli.app/v1/docs'

# 200
# HTML documentation page

GET /v1/openapi.json

Access: Public

Idempotency-Key: Not required

OpenAPI document (versioned alias).

Example
curl -X GET \
  'https://api.pooli.app/v1/openapi.json'

# 200
# OpenAPI JSON document

Customers & Pools CSV Import

Bulk import customers and pools for an organization. Each row typically represents one pool; customer columns may repeat.

The REST route is POST /v1/organizations/{orgId}/import/csv. Access is organization owner or admin. Send JSON with csvText, or a raw CSV body. See Customers for the endpoint card.

Access: Organization owner or admin.

  • Upload in the app (Customers/Pools screen → Import menu)
  • Or upload on the web in your Organization dashboard ("Import Customers & Pools CSV")
  • API: Firebase Callable customers-importCsv (group prefix required)

Request payload:

{
  organizationId: "ORG_ID",
  csvText: "organizationId,customer_name,...\n...",
  // Optional advanced mapping/defaults; see guide for details
  mapping: { /* header alias overrides */ },
  defaults: { /* e.g., organizationId if not in every row */ }
}

Example client call:

// Web / Firebase Functions client
const importFn = firebase.functions().httpsCallable('customers-importCsv');
await importFn({ organizationId: 'abc123', csvText });

See the CSV Import Guide for exact fields, header aliases, defaults, and full examples.

Products CSV Import

Bulk create and update organization products from a CSV. Designed for retailers to upload their catalog in one click.

  • Available via app and web dashboard only (no public callable API)
  • Web: use the Products section on the Organization dashboard ("Import Products CSV")

Required columns: organizationId and either product_id (for updates) or both name and category (for new products).

Optional columns: description, defaultPrice, isTaxable, type (oneTime|subscription), stripeProductId, stripePriceId, cost, msrp, notes, tags (comma/semicolon/pipe separated), useMsrpForMarkup, deliverable, activeInStore, photos (comma/semicolon/pipe separated URLs).

Example header + rows (showing advanced fields on second row):

organizationId,product_id,name,category,defaultPrice,isTaxable,type,deliverable,activeInStore,tags,cost,msrp,notes,useMsrpForMarkup,photos
abc123,,Chlorine Tabs,Chemicals,19.99,true,oneTime,true,true,chlorine;tabs,,,,,
abc123,P123,Monthly Service,Services,120.00,true,subscription,false,false,service;recurring,80.00,150.00,"Includes filter cleaning",true,https://example.com/img1.jpg|https://example.com/img2.jpg