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

FieldTypeRequiredDescription
formula_idstringYesWhich formula to run. See formula IDs.
readingsobjectYesCurrent water readings as {reading_id: number}. See reading IDs.
poolobjectYesPool volume and surface. See pool object.
target_levelsarrayNoOverride default target ranges. See target levels.
substitutionsobjectNoOverride the default treatment for a reading. See substitutions.

Pool object

FieldTypeRequiredDescription
gallonsnumberYesPool volume in US gallons. Scales every dose.
water_typestringNoSanitization 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_typestringNoPool 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:

FieldTypeDescription
idstringThe reading ID to override (e.g. "fc", "ph").
minnumberMinimum target value.
maxnumberMaximum target value.

Substitutions

A map from reading ID to the treatment to use instead of the default:

{
  "fc": { "up": "dichlor" },
  "ph": { "down": "m_acid" }
}
FieldTypeDescription
upstringTreatment ID to use when raising this reading.
downstringTreatment 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."
        }
      ]
    }
  ]
}

ounces is a raw float — round it for display (51.99999999999999 above is 52 oz). Actions are already in the order they should be performed.

Action object

FieldTypeDescription
readingsarrayThe readings this action addresses; each has id, name, short_name, and optional units. Empty for wait actions.
typestring"raise", "lower", "special" (shock, drain, wait), or "warning".
treatmentobjectThe selected treatment. See treatment object.
ouncesnumberAmount 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_optionsarrayEvery treatment available for this reading; use their IDs as substitutions in a later request.

Treatment object

FieldTypeDescription
idstringTreatment identifier. See treatment IDs.
namestringDisplay name.
typestring"dryChemical", "liquidChemical", "task", "wait", or "warning".
concentrationnumberPercent active ingredient (0–100). Present on chemical treatments.
descriptionstringInstructions for applying the treatment.
waitMinutesnumberMinutes to wait after this treatment before the next one.
taskNamestringShort imperative label, present on task-type treatments (e.g. "Add chlorine tablets").

Formula IDs

IDDescription
chlorine_cal_hypoStandard chlorine pools. The most common formula.
saltSalt water / salt chlorine generator pools. CYA target 60–80 ppm; default FC treatment is turning up the generator.
bromineBromine-sanitized pools and hot tubs.
mineralsMineral 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.

IDNameUnitsTest rangeDefault targetNotes
fcFree Chlorineppm0 – 101 – 3Warning buffer +2 ppm above max. Minerals formula: 0.5 – 4.
tcTotal Chlorineppm0 – 10Used with fc to infer combined chlorine.
ccCombined Chlorineppm0 – 70 – 0.1Computed as TC − FC; above 0.1 triggers shock.
phpH5 – 97.2 – 7.8
taTotal Alkalinityppm0 – 25080 – 150Warning buffer +20 above, +10 below.
cyaStabilizer (CYA)ppm0 – 30030 – 50Warning buffer ±10. Salt formula: 60 – 80.
chCalcium Hardnessppm0 – 1000175 – 225Warning buffer ±25. Plaster/concrete/pebble: 200 – 275.
saltSalt Levelppm0 – 50002700 – 3500Salt formula only.
broBromineppm0 – 73 – 5Bromine formula only.
copperCopperppm0 – 70.4 – 0.7
disoxDissolved Oxygenppm0 – 156 – 7

Treatment IDs

Names and types exactly as the engine returns them.

IDNameType
chlorine_tabletsChlorine Tabletstask
calc_hypoChlorine GranulesdryChemical
na_hcloLiquid ChlorineliquidChemical
dichlorDichlordryChemical
swg_upSalt Generator Uptask
bromineBrominedryChemical
shockChlorine ShockdryChemical
ph_increaserpH IncreaserdryChemical
soda_ashSoda AshdryChemical
sodium_bisulfatepH DecreaserdryChemical
m_acidMuriatic AcidliquidChemical
alk_increaserAlkalinity Increaser¹dryChemical
baking_sodaBaking SodadryChemical
cyaStabilizerdryChemical
cal_chlorHardness IncreaserdryChemical
saltSaltdryChemical
drainDilute Pooltask
drain_fcDilute Pooltask
drain_cyaDilute Pooltask
drain_chDilute Pooltask
waitWaitwait
warningWarningwarning

¹ 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" }