Calculate endpoint
Calculate pool chemical dosing from current water readings. One request in, an ordered list of treatments out — amounts in ounces, sequenced so the chemicals don't fight each other, with wait periods between steps. For the concepts behind the numbers, see how formulas work; for AI assistants, see the MCP server.
Request
POST https://api.poolcloud.com/api/calculate/
Content-Type: application/json
No authentication required. The trailing slash is required.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
formula_id | string | Yes | Which formula to run. See formula IDs. |
readings | object | Yes | Current water readings as {reading_id: number}. See reading IDs. |
pool | object | Yes | Pool volume and surface. See pool object. |
target_levels | array | No | Override default target ranges. See target levels. |
substitutions | object | No | Override the default treatment for a reading. See substitutions. |
Pool object
| Field | Type | Required | Description |
|---|---|---|---|
gallons | number | Yes | Pool volume in US gallons. Scales every dose. |
water_type | string | No | Sanitization method. Informational — the formula is chosen by formula_id. Values the formulas recognize: chlorine, salt_water, bromine, minerals, copper, ozone, uv. Not validated by the endpoint. |
wall_type | string | No | Pool surface. plaster, concrete, and pebble raise the calcium hardness target to 200–275 ppm; vinyl and fiberglass leave it at 175–225 ppm. Not validated by the endpoint. |
Target levels
An array of overrides for default target ranges:
| Field | Type | Description |
|---|---|---|
id | string | The reading ID to override (e.g. "fc", "ph"). |
min | number | Minimum target value. |
max | number | Maximum target value. |
Substitutions
A map from reading ID to the treatment to use instead of the default:
{
"fc": { "up": "dichlor" },
"ph": { "down": "m_acid" }
}
| Field | Type | Description |
|---|---|---|
up | string | Treatment ID to use when raising this reading. |
down | string | Treatment ID to use when lowering this reading. |
Valid substitutes per formula are listed under substitutions by formula.
Response
200 OK with { "actions": [...] }. This is the actual engine output for the basic request below (10,000 gallon plaster chlorine pool, FC=0, pH=7.2, TA=80, TC=0, CYA=0, CH=0):
{
"actions": [
{
"readings": [
{ "id": "fc", "name": "Free Chlorine", "short_name": "Free Chlorine", "units": "ppm" }
],
"type": "raise",
"treatment": {
"name": "Chlorine Tablets",
"taskName": "Add chlorine tablets",
"id": "chlorine_tablets",
"type": "task",
"description": "Add more chlorine tablets to your chlorinator or floaters. If there is a dial or knob, adjust those as well.",
"waitMinutes": 60,
"concentration": 100
},
"ounces": 1,
"treatment_options": [
{
"name": "Chlorine Tablets",
"taskName": "Add chlorine tablets",
"id": "chlorine_tablets",
"type": "task",
"description": "Add more chlorine tablets to your chlorinator or floaters. If there is a dial or knob, adjust those as well.",
"waitMinutes": 60,
"concentration": 100
},
{
"name": "Chlorine Granules",
"id": "calc_hypo",
"type": "dryChemical",
"description": "Pre-dissolve in a bucket of water and pour into pool.",
"waitMinutes": 15,
"concentration": 67
},
{
"name": "Liquid Chlorine",
"id": "na_hclo",
"type": "liquidChemical",
"description": "Add liquid chlorine to your pool. Pour it slowly in front of a return jet to help it mix.",
"waitMinutes": 60,
"concentration": 10
}
]
},
{
"readings": [],
"type": "special",
"treatment": {
"name": "Wait",
"id": "wait",
"type": "wait",
"concentration": 100,
"description": ".",
"waitMinutes": 0
},
"ounces": 60,
"treatment_options": []
},
{
"readings": [
{ "id": "cya", "name": "Stabilizer (CYA)", "short_name": "CYA", "units": "ppm" }
],
"type": "raise",
"treatment": {
"name": "Stabilizer",
"id": "cya",
"type": "dryChemical",
"concentration": 100,
"description": "Add pool stabilizer or pool conditioner to a 5-gallon bucket of pool water. Then pour all five gallons into the skimmer and run your pool pump for several hours to help it dissolve.",
"waitMinutes": 20
},
"ounces": 51.99999999999999,
"treatment_options": [
{
"name": "Stabilizer",
"id": "cya",
"type": "dryChemical",
"concentration": 100,
"description": "Add pool stabilizer or pool conditioner to a 5-gallon bucket of pool water. Then pour all five gallons into the skimmer and run your pool pump for several hours to help it dissolve.",
"waitMinutes": 20
}
]
},
{
"readings": [],
"type": "special",
"treatment": {
"name": "Wait",
"id": "wait",
"type": "wait",
"concentration": 100,
"description": ".",
"waitMinutes": 0
},
"ounces": 20,
"treatment_options": []
},
{
"readings": [
{ "id": "ch", "name": "Calcium Hardness", "short_name": "Hardness", "units": "ppm" }
],
"type": "raise",
"treatment": {
"name": "Hardness Increaser",
"id": "cal_chlor",
"type": "dryChemical",
"concentration": 100,
"waitMinutes": 480,
"description": "Sprinkle (or “broadcast”) hardness increaser (calcium chloride) directly over the surface of the pool. Do this in small batches to avoid cloudy water and allow the chemical to properly mix."
},
"ounces": 288,
"treatment_options": [
{
"name": "Hardness Increaser",
"id": "cal_chlor",
"type": "dryChemical",
"concentration": 100,
"waitMinutes": 480,
"description": "Sprinkle (or “broadcast”) hardness increaser (calcium chloride) directly over the surface of the pool. Do this in small batches to avoid cloudy water and allow the chemical to properly mix."
}
]
}
]
}
ouncesis a raw float — round it for display (51.99999999999999above is 52 oz). Actions are already in the order they should be performed.
Action object
| Field | Type | Description |
|---|---|---|
readings | array | The readings this action addresses; each has id, name, short_name, and optional units. Empty for wait actions. |
type | string | "raise", "lower", "special" (shock, drain, wait), or "warning". |
treatment | object | The selected treatment. See treatment object. |
ounces | number | Amount of chemical in ounces. For wait actions: minutes to wait. For warnings: the delta from the target range (negative = too low, positive = too high). |
treatment_options | array | Every treatment available for this reading; use their IDs as substitutions in a later request. |
Treatment object
| Field | Type | Description |
|---|---|---|
id | string | Treatment identifier. See treatment IDs. |
name | string | Display name. |
type | string | "dryChemical", "liquidChemical", "task", "wait", or "warning". |
concentration | number | Percent active ingredient (0–100). Present on chemical treatments. |
description | string | Instructions for applying the treatment. |
waitMinutes | number | Minutes to wait after this treatment before the next one. |
taskName | string | Short imperative label, present on task-type treatments (e.g. "Add chlorine tablets"). |
Formula IDs
| ID | Description |
|---|---|
chlorine_cal_hypo | Standard chlorine pools. The most common formula. |
salt | Salt water / salt chlorine generator pools. CYA target 60–80 ppm; default FC treatment is turning up the generator. |
bromine | Bromine-sanitized pools and hot tubs. |
minerals | Mineral sanitization systems. FC target 0.5–4 ppm. |
Reading IDs
Pass any subset — the engine only acts on readings you provide. Defaults are for the chlorine formula; see how formulas work for per-formula overrides.
| ID | Name | Units | Test range | Default target | Notes |
|---|---|---|---|---|---|
fc | Free Chlorine | ppm | 0 – 10 | 1 – 3 | Warning buffer +2 ppm above max. Minerals formula: 0.5 – 4. |
tc | Total Chlorine | ppm | 0 – 10 | — | Used with fc to infer combined chlorine. |
cc | Combined Chlorine | ppm | 0 – 7 | 0 – 0.1 | Computed as TC − FC; above 0.1 triggers shock. |
ph | pH | — | 5 – 9 | 7.2 – 7.8 | |
ta | Total Alkalinity | ppm | 0 – 250 | 80 – 150 | Warning buffer +20 above, +10 below. |
cya | Stabilizer (CYA) | ppm | 0 – 300 | 30 – 50 | Warning buffer ±10. Salt formula: 60 – 80. |
ch | Calcium Hardness | ppm | 0 – 1000 | 175 – 225 | Warning buffer ±25. Plaster/concrete/pebble: 200 – 275. |
salt | Salt Level | ppm | 0 – 5000 | 2700 – 3500 | Salt formula only. |
bro | Bromine | ppm | 0 – 7 | 3 – 5 | Bromine formula only. |
copper | Copper | ppm | 0 – 7 | 0.4 – 0.7 | |
disox | Dissolved Oxygen | ppm | 0 – 15 | 6 – 7 |
Treatment IDs
Names and types exactly as the engine returns them.
| ID | Name | Type |
|---|---|---|
chlorine_tablets | Chlorine Tablets | task |
calc_hypo | Chlorine Granules | dryChemical |
na_hclo | Liquid Chlorine | liquidChemical |
dichlor | Dichlor | dryChemical |
swg_up | Salt Generator Up | task |
bromine | Bromine | dryChemical |
shock | Chlorine Shock | dryChemical |
ph_increaser | pH Increaser | dryChemical |
soda_ash | Soda Ash | dryChemical |
sodium_bisulfate | pH Decreaser | dryChemical |
m_acid | Muriatic Acid | liquidChemical |
alk_increaser | Alkalinity Increaser¹ | dryChemical |
baking_soda | Baking Soda | dryChemical |
cya | Stabilizer | dryChemical |
cal_chlor | Hardness Increaser | dryChemical |
salt | Salt | dryChemical |
drain | Dilute Pool | task |
drain_fc | Dilute Pool | task |
drain_cya | Dilute Pool | task |
drain_ch | Dilute Pool | task |
wait | Wait | wait |
warning | Warning | warning |
¹ The current engine build spells this name "Alakalinity Increaser"; match on id, not name.
Examples
Basic request
curl -X POST https://api.poolcloud.com/api/calculate/ \
-H "Content-Type: application/json" \
-d '{
"formula_id": "chlorine_cal_hypo",
"readings": { "fc": 0, "ph": 7.2, "ta": 80, "tc": 0, "cya": 0, "ch": 0 },
"pool": { "gallons": 10000, "water_type": "chlorine", "wall_type": "plaster" }
}'
Returns the response shown above.
With substitutions
Use dichlor instead of the default chlorine treatment:
curl -X POST https://api.poolcloud.com/api/calculate/ \
-H "Content-Type: application/json" \
-d '{
"formula_id": "chlorine_cal_hypo",
"readings": { "fc": 1, "ph": 7.4 },
"pool": { "gallons": 15000 },
"substitutions": { "fc": { "up": "dichlor" } }
}'
With custom target levels
Run free chlorine higher than the default 1–3 ppm:
curl -X POST https://api.poolcloud.com/api/calculate/ \
-H "Content-Type: application/json" \
-d '{
"formula_id": "chlorine_cal_hypo",
"readings": { "fc": 2, "ph": 7.5 },
"pool": { "gallons": 20000 },
"target_levels": [ { "id": "fc", "min": 3, "max": 5 } ]
}'
Error responses
Every error body has an error string; some add detail.
400 — Bad request
The formula engine rejected the input — most often an unknown formula_id. The engine's own JSON error is embedded as a string inside error:
{
"error": "Calculation failed: {\"error\":\"Unknown formula_id: \\\"invalid\\\". Valid IDs: chlorine_cal_hypo, salt, bromine, minerals\"}"
}
422 — Validation error
The request body failed schema validation (missing formula_id, readings, pool.gallons, or a wrong type):
{
"error": "Validation error",
"detail": "…",
"field_errors": [ { "loc": ["body", "payload", "pool", "gallons"], "msg": "Field required", "type": "missing" } ]
}
500 — Internal error
Either { "error": "Invalid calculation output" } (the engine returned something that wasn't JSON) or { "error": "Internal server error", "detail": "An unexpected error occurred" }.
503 — Engine unavailable
{ "error": "Formula runner not available" }
504 — Timeout
The calculation exceeded 10 seconds:
{ "error": "Calculation timed out" }
