API Documentation
Connect your own software to Aquaticus. Read and update projects, clients, estimates, inventory and pricing profiles with an API key created in the app.
Last updated: September 12, 2026
Contents
Getting Started
The Aquaticus API is a JSON REST API served at https://api.aquaticus.tech. It is the same API the Aquaticus app uses, so everything you can see in the app you can also read — and, with the right permission, change — from your own code.
- Create an API key. In the app open Settings → API Keys and press Create API key. Give it a name and tick the sections it may access. Only the account owner or an Admin can do this.
- Copy the key. It is shown exactly once, right after creation. Aquaticus stores only a hash of it, so if you lose it you have to create a new one.
- Send it as a bearer token on every request.
Example
curl https://api.aquaticus.tech/projects \
-H "Authorization: Bearer aq_YOUR_KEY_HERE"
Response
[
{
"id": 41,
"name": "Henderson Residence",
"clientId": 7,
"variables": { "shape": "rectangle", "length": 32.0, "width": 16.0 },
"createdAt": 1757203200000,
"lastUpdatedAt": 1757635200000,
"tags": ["gunite", "2026"],
"isInProgress": true
}
]
Tip: Start with a key that only has View permissions and add Edit or Delete per section once your integration needs them. A key can be deleted at any time from Settings → API Keys; every request made with it fails from that moment on.
Authentication & API Keys
Every endpoint in this reference requires an API key in the Authorization header:
Authorization: Bearer aq_AbCdEfGh0123456789AbCdEfGh0123456789AbCd
A key is the string aq_ followed by 40 letters and digits. The app lists each key by its first characters (for example aq_AbCdEfGh) so you can tell them apart; that prefix on its own cannot authenticate.
Whose data a key sees
A key belongs to your company account, not to the person who created it. Requests made with it act as the account owner: they see every project, client, estimate and inventory item of the company, and anything they create is owned by the company. Deleting the admin who created a key does not revoke the key — delete the key itself.
The account owner must have a verified email address and an approved Aquaticus account, exactly as for using the app. Otherwise every request is refused with 403 EmailNotVerified or 403 AlphaAccessRequired.
Managing keys
- Keys are created, listed and deleted in the app under Settings → API Keys, by the account owner or a member with the Admin role.
- An account can hold up to 50 keys. Names are 1–100 characters.
- The app shows when each key was created, by whom, and when it was last used (updated at most once a minute).
- Key management itself is not available through the API: a key cannot list, create or delete keys.
Authentication failures
| Status | error | Meaning |
|---|---|---|
| 401 | Unauthorized | No bearer token, or the key is unknown (mistyped, or deleted). |
| 403 | ApiKeyPermissionDenied | The key is valid but lacks the permission this request needs. The description names the missing permission, e.g. This API key does not have 'delete' permission for 'Clients'. |
| 403 | ApiKeyEndpointForbidden | The endpoint is outside every section a key can be granted (account settings, team members, sessions, API keys, the AI assistant). Use the app instead. |
| 403 | EmailNotVerified / AlphaAccessRequired | The account behind the key is not yet verified or approved. |
Permissions
When you create a key you choose, per section, whether it may View, Edit and Delete. The section is decided by the URL and the action by the HTTP method, so you can tell from this reference alone what a request needs — every endpoint below is labelled with it.
Sections
| Section | Covers | Paths |
|---|---|---|
| Projects and Model | Projects, their pool model (variables), project history, project tags, calculations, the formula JavaScript, the scratch model (/tools) and spa presets. |
/projects, /project/…, /calculate…, /formulas/js, /formulas/jscode, /tags, /tag/…, /tools, /spa-presets… |
| Clients | The people and companies projects are built for. | /clients, /client/… |
| Tax and Markup Profiles | Tax profiles and markup presets used by estimates. | /tax-profiles…, /markup-presets… |
| Estimates and Templates | Estimates, change orders, templates, sections, line items, estimate tags and formula validation. | /estimates…, /templates…, /sections/…, /line-items/…, /estimate-tags…, /formulas/validate, /formulas/variables |
| Inventory | Inventory items, categories, notes, vendor prices and suppliers. | /inventory…, /suppliers… |
Actions
| HTTP method | Permission needed |
|---|---|
GET, HEAD | View |
POST, PUT, PATCH | Edit — including duplicating, reordering, rolling back history, setting a default, refreshing prices and CSV import. |
DELETE | Delete — any DELETE request, also on sub-resources such as a section, a note or a vendor price. |
POST /calculate, POST /formulas/validate | View — these two only compute and never write, so a read-only key can use them. |
Rules applied when a key is created
- Edit or Delete includes View. A section you may change you may also read. The app ticks View for you.
- Estimates include View on Tax and Markup Profiles and on Inventory. An estimate references tax profiles, markup presets and inventory items, so a key that can read estimates can also read those — never write them unless you grant that separately.
- A key must have at least one permission.
Example: a key with Estimates: View, Edit and nothing else can list and update estimates, add line items, list tax profiles, markup presets and inventory items — but gets 403 ApiKeyPermissionDenied on DELETE /estimates/{id}, on POST /inventory and on GET /projects.
Conventions & Errors
Requests and responses
- Base URL:
https://api.aquaticus.tech. There is no version prefix. - JSON everywhere. Send
Content-Type: application/jsonwith a body. Responses are JSON unless stated (PDF and CSV downloads, the formula JavaScript). - Identifiers. Projects, clients and history entries use integer ids. Estimates, templates, sections, line items, tags, tax profiles, markup presets, inventory items, categories, prices and suppliers use UUID strings.
- Timestamps (
createdAt,updatedAt,lastUpdatedAt,validUntil, …) are Unix epoch milliseconds. - Omitted defaults. Responses leave out fields that hold their default value — a
null, an empty string or list,0, or a default boolean. Treat a missing field as its documented default rather than as an error. Objects whose fields have no defaults (clients, history entries) always carry every field. - Unknown fields in a body are rejected with
400 BadJsonBody. Send only the fields listed for the endpoint. - Body size is capped at 10 MB (
413 PayloadTooLarge). - Rates and percentages in tax profiles, markup presets and line-item overrides are fractions:
0.0875means 8.75%. - Success codes. Most reads and updates answer
200. Creates in the estimate and inventory areas answer201; creating a project or client answers200. Deletes answer204(no body), except deleting a project or a client (200). - Pagination. Every list endpoint accepts
?limit=(how many to return, at least 1) and?offset=(how many to skip, default 0), independently of each other. Withoutlimita list returns everything, exceptGET /estimates(50) andGET /inventory(100). A page shorter thanlimitis the last one.
Error shape
Every error carries a stable error code and a human-readable description:
HTTP/1.1 404 Not Found
{
"error": "NotFound",
"description": "Project 42 not found"
}
The full list of codes is in the Error Reference. A malformed id in a URL (for example a UUID that does not parse) currently answers 500 rather than 400; validate ids on your side.
Projects and Model
A project is one job: a name, a client, tags and the model — the variables object describing the pool (shape, dimensions, depth profile, structure, steel, hydraulics, features and spa). The model is what the calculators run on and what estimates snapshot. Its full field reference is in The Pool Model below.
Permission section: Projects and Model.
| Endpoint | Permission | Description |
|---|---|---|
GET/projects | View | List all projects (?limit=, ?offset=) |
GET/projects/{clientId} | View | List a client's projects (?limit=, ?offset=) |
GET/project/{id} | View | Get one project |
POST/project | Edit | Create a project |
PATCH/project/{id} | Edit | Update name, model, tags or status |
POST/project/{id}/duplicate | Edit | Copy a project |
DELETE/project/{id} | Delete | Delete a project and its history |
GET/project/{id}/export-pdf | View | Project report as PDF |
GET/project/{id}/history | View | Change history (?limit=, ?offset=) |
POST/project/{id}/history/{historyId}/rollback | Edit | Restore an earlier version |
GET/calculate/{projectId} | View | Run the calculators on a saved project |
POST/calculate | View | Run the calculators on an ad-hoc model |
GET/formulas/js | View | Calculator JavaScript + formula descriptions |
GET/formulas/jscode | View | Calculator JavaScript only |
GET/tags | View | Tag vocabulary (?limit=, ?offset=) |
POST/tag | Edit | Add a tag to the vocabulary |
DELETE/tag/{tag} | Delete | Remove a tag from the vocabulary |
GET/tools | View | The account's scratch model |
POST/tools | Edit | Save the scratch model |
GET/spa-presets | View | Saved spa configurations (?limit=, ?offset=) |
POST/spa-presets | Edit | Save a spa preset |
GET/spa-presets/{id} | View | Get a spa preset |
PUT/spa-presets/{id} | Edit | Update a spa preset |
DELETE/spa-presets/{id} | Delete | Delete a spa preset |
The project object
| Field | Type | Description |
|---|---|---|
id | integer | Project id. |
name | string | 2–100 characters. |
clientId | integer | The client the project belongs to. Cannot be changed after creation. |
variables | object | The pool model. See The Pool Model. Fields left unset are omitted; an empty model is {}. |
tags | string[] | Free-form labels, each up to 30 characters. Default []. |
isInProgress | boolean | true while the job is active. Default true. |
createdAt, lastUpdatedAt | integer | Epoch milliseconds. lastUpdatedAt moves on every update and rollback. |
All projects of the account, as an array of project objects, optionally paged with ?limit= and ?offset=. GET /projects/{clientId} returns the same shape filtered to one client; an unknown client yields [].
Create a project for an existing client. Answers 200 with the created project and writes a created entry to its history.
| Field | Type | Notes |
|---|---|---|
namerequired | string | 2–100 characters. |
clientIdrequired | integer | Must exist, else 404 NotFound. |
variablesrequired | object | The model; {} is allowed. When shape is omitted or a rectangle, oval or kidney, missing length/width default to 30 × 15 ft; a circle gets radius 15 ft. |
tags | string[] | Optional, each ≤ 30 characters. |
POST /project
{
"name": "Henderson Residence",
"clientId": 7,
"tags": ["gunite", "2026"],
"variables": {
"shape": "rectangle",
"length": 32,
"width": 16,
"depthProfileType": "standard",
"shallowDepth": 3.5,
"deepDepth": 8,
"shallowEndLength": 10,
"deepEndLength": 8
}
}
Partial update. Send only the fields to change; each one you send replaces the stored value (the model and the tag list are replaced whole, not merged). Every call bumps lastUpdatedAt and writes an updated history entry.
| Field | Type | Notes |
|---|---|---|
name | string | 2–100 characters. |
variables | object | The complete new model. |
tags | string[] | The complete new tag list. |
isInProgress | boolean |
Removes the project and its whole history. Answers 200 {"success": true}, or 404 when there is no such project. Estimates bound to the project keep their snapshot of the model.
The project report as application/pdf (an attachment named aquaticus_project_<name>.pdf). The calculators run server-side, so the report always reflects the stored model.
| Query | Default | Notes |
|---|---|---|
sections | all | Comma-separated subset of project, pool, depth, spa, features, dig, concrete, rebar, hydraulics. |
drawings | plan,profile | plan needs the pool section, profile needs depth. |
detail | both | Comma-separated subset of rebarDetail, cages. When present, only the listed blocks are printed. |
units | feet-inches | decimal prints lengths as decimal feet. |
Every change to the project, newest first (page with ?limit= and ?offset=). Each entry has id, projectId, userId, userEmail, userName, action (created, updated or rollback), the model before and after (previousVariables, newVariables), previousName/newName, previousTags/newTags and createdAt. Changes made with an API key are attributed to the account owner.
POST /project/{id}/history/{historyId}/rollback (Edit) restores the state before that entry (or the initial state for a created entry), appends a rollback entry and answers with the project. 400 InvalidHistoryEntry when the entry belongs to another project.
Runs every calculator on the model in the body and returns the results as one flat JSON object; nothing is stored. GET /calculate/{projectId} does the same for a saved project. Unlike saving a project, calculating validates the shape strictly (400 InvalidParameter when, say, a rectangle has no length).
The result keys are defined by the formula set and include, among others, perimeter, floorArea, wallArea, totalSurfaceArea, averageDepth, volume, maxGallons, digVolume, gravelTons, concreteFloorYards, concreteWallAndCornersYards, concreteYards, rebarFeet, gpmMainFilterLoop, numberOfSkimmers, numberOfReturns, pipeSizeMainFiltrationSuction and pipeSizeMainFiltrationReturn. GET /formulas/js returns the calculator source together with a description object documenting each output (name, formula, inputs, unit, category, example).
POST /calculate
{
"shape": "rectangle", "length": 32, "width": 16,
"depthProfileType": "standard", "shallowDepth": 3.5, "deepDepth": 8,
"shallowEndLength": 10, "deepEndLength": 8,
"wallThickness": 8, "floorThickness": 6, "turnoverHours": 8
}
200 OK
{
"perimeter": 96,
"floorArea": 561.4,
"volume": 2718.7,
"maxGallons": 20336,
"concreteYards": 31.3,
"gpmMainFilterLoop": 42.4,
"numberOfSkimmers": 2,
"rebarFeet": { "totalFeet": 3241.0, "bar3": 0, "bar4": 3241.0, "bar5": 0, "bar6": 0 },
"…": "…"
}
The reusable tag vocabulary as an array of strings. POST /tag adds one — the body is the tag as plain text (Content-Type: text/plain), and re-adding an existing tag is a no-op. DELETE /tag/{tag} removes it from the vocabulary without touching projects that already carry it. Both answer {"status": "ok"}.
Saved spa configurations, sorted by name. A preset is { id, userId, name, notes, config, createdAt, updatedAt } where config is a spa object. Create with POST /spa-presets (name 2–255 characters, optional notes, config with at least depth) → 201; update with PUT /spa-presets/{id} (any subset of name, notes, config — config is replaced whole); remove with DELETE /spa-presets/{id} → 204. Projects keep a copy of the spa, so deleting a preset never changes a project.
The Pool Model (ProjectVariables)
The variables object of a project — also the body of POST /calculate and POST /tools, and the projectVariableSnapshot of an estimate. Every field is optional; send only what you know. Fields you leave out are absent from responses too.
Units: thicknesses, beamHeight, uBarSpacing, a feature's heightFromBeam and thickness, and a spa's raiseHeight are in inches. Every other length is in feet. Percentages are whole numbers (20 = 20%). The calculators convert for you; never pre-convert.
Validation: no numeric field may be negative. Saving a project checks that much and the spa rules below; calculating additionally requires the dimensions that match shape (see Shape and dimensions).
Shape and dimensions
| Field | Type | Unit | Description |
|---|---|---|---|
hasPool | boolean | Default true. false means a spa-only project; then spa is required. | |
shape | string | rectangle, circle, oval, kidney or custom. | |
length, width | number | ft | Required by the calculators for rectangle, oval and kidney; must be absent for a circle. |
radius | number | ft | Circle only; must be absent for other shapes. |
kidneyWidthA, kidneyWidthB, kidneyWidthC | number | ft | Kidney head, waist and foot widths. |
customShape | object | For shape: "custom": { name, nodes, splitLines, depthRegions } with at least 3 nodes of { x, y, smoothness } (feet; smoothness 0–1 rounds the corner). Area, perimeter and length are derived from the outline. | |
spa | object | See Spa. |
Depth profile
| Field | Type | Unit | Description |
|---|---|---|---|
depthProfileType | string | standard (shallow → slope → deep), vShaped (shallow edges, deep centre) or uniform (flat). | |
shallowDepth, deepDepth | number | ft | Standard profile. |
shallowEndLength, deepEndLength | number | ft | Flat runs before and after the slope. |
secondSlopeBreakLength, secondSlopeBreakDepth | number | ft | Optional second break in the slope; the length is measured from where the slope starts. |
vLeftEdgeDepth, vLeftEdgeLength, vCenterDepth, vCenterLength, vRightEdgeDepth, vRightEdgeLength | number | ft | V-shaped profile. |
uniformDepth | number | ft | Uniform profile. |
customAverageDepth | number | ft | Overrides the average depth derived from the profile. |
Structure and concrete
| Field | Type | Unit | Calculator default | Description |
|---|---|---|---|---|
wallThickness | number | in | 8 | |
floorThickness | number | in | 6 | |
beamHeight | number | in | 6 | Bond beam height. |
beamThickness | number | in | 12 | Bond beam thickness. |
finishThickness | number | in | 0.5 | Interior finish; reduces water volume. |
gravelThickness | number | in | 6 | Gravel bed under the floor. |
concreteWastePercent | number | % | 20 | |
overdig | number | ft | 0 | Excavation added on each side of the footprint. |
exteriorSurfaces | object[] | Exposed exterior wall faces to finish: { label, length (ft), exposedHeight (ft), includeCap }. |
Steel
| Field | Type | Unit | Calculator default | Description |
|---|---|---|---|---|
steelPatternWall, steelPatternFloor | string | Must contain a positive spacing in inches, e.g. "#4 @ 12\" OC" or "12". | ||
steelWastePercent | number | % | 20 | |
uBarSize | number | bar # | 4 | 4 = #4 bar. |
uBarSpacing | number | in | 12 | On centre. |
topBarSize | number | bar # | 4 | |
topBarCount | number | count | computed | |
wallCage1Pattern, wallCage1HorizontalBar, wallCage1VerticalBar | string, number, number | grid, bar #, bar # | "12x12", 4, 4 | First wall cage. wallCage2… and wallCage3… add cages. |
floorCage1Pattern, floorCage1HorizontalBar, floorCage1VerticalBar | string, number, number | grid, bar #, bar # | "12x12", 4, 4 | First floor cage. floorCage2… and floorCage3… add cages. |
Hydraulics
| Field | Type | Unit | Calculator default | Description |
|---|---|---|---|---|
turnoverHours | number | h | 8 | Time for one full filtration turnover. |
minVelocitySuction, maxVelocitySuction | number | ft/s | 3, 5 | |
minVelocityReturn, maxVelocityReturn | number | ft/s | 6, 8 | |
pipeRunLength | number | ft | Equivalent run from the pool to the equipment pad. | |
maxFlowPerSkimmer | number | GPM | 55 | |
maxAreaPerSkimmer | number | sq ft | 500 | |
maxFlowPerReturn | number | GPM | 20 | |
perimeterPerReturn | number | ft | 20 | |
customPipeSizeMainFiltrationSuction, customPipeSizeMainFiltrationReturn | number | in | computed | Override the computed main line sizes. |
Features
features is an array of steps, benches, sun shelves and ledges:
| Field | Type | Unit | Description |
|---|---|---|---|
typerequired | string | step, bench, sunshelf or ledge. | |
lengthrequired | number | ft | Side to side; must be > 0. |
widthrequired | number | ft | Front to back (tread depth for steps); > 0. |
heightFromBeamrequired | number | in | Drop from the top of the bond beam to the feature surface; > 0. |
depthAtLocationrequired | number | ft | Water depth where the feature sits; > 0. |
count | integer | Default 1; number of risers for steps. | |
location | string | shallowEnd (default), center, deepEnd, leftWall, rightWall, custom. | |
includeSteel | boolean | Default: yes for everything but a ledge. | |
steelPattern, steelBarSize | string, integer | grid, bar # | e.g. "12x12", 4. |
thickness | number | in | Ledges only; default 6. |
stepConfiguration | string | Steps only: straight, corner90, weddingCake or fullWidth. | |
remainingTreadDepth | number | ft | Tread depth of steps 2+ when it differs from the top step. |
Spa
spa describes an attached or freestanding spa. Structural, steel and hydraulic fields left null inherit the pool's value (or the calculator default for a spa-only project). Only depth is required.
| Field | Type | Unit | Description |
|---|---|---|---|
attachment | string | interior (default; needs a pool) or freestanding. | |
shape | string | Default circle; same values and dimension rules as the pool (radius for a circle, length/width otherwise, customShape for custom). | |
depthrequired | number | ft | > 0. |
raiseHeight | number | in | How far an interior spa rises above the pool beam. |
commonWallLength, commonWallThickness | number | ft, in | Wall shared with the pool. The length cannot exceed the spa perimeter. |
spillwayLength | number | ft | At most commonWallLength. |
insidePoolPercent | number | % | 0–100; how much of the spa sits inside the pool footprint (default 100). |
poolDepthAtSpa | number | ft | Defaults to the pool's average depth. |
wallThickness, floorThickness, beamHeight, beamThickness, finishThickness | number | in | Inherit the pool's when omitted. |
concreteWastePercent, steelWastePercent | number | % | Inherit. |
wallCage1…3, floorCage1…3, commonWallCagePattern, commonWallCageHorizontalBar, commonWallCageVerticalBar | string / number | grid, bar # | Same layout as the pool's cages; inherit when omitted. |
equipment | string | dedicated (default) or shared with the pool (needs a pool). | |
turnoverHours | number | h | Default 0.5 for a spa. |
minVelocitySuction, maxVelocitySuction, minVelocityReturn, maxVelocityReturn | number | ft/s | Inherit. |
jetCount | integer | ≥ 0. | |
gpmPerJet | number | GPM | Default 12. |
features | object[] | Benches and steps inside the spa; same shape as pool features. |
Complete example
{
"hasPool": true,
"shape": "rectangle",
"length": 34,
"width": 16,
"depthProfileType": "standard",
"shallowDepth": 3.5,
"deepDepth": 8.0,
"shallowEndLength": 10,
"deepEndLength": 8,
"wallThickness": 8,
"floorThickness": 6,
"beamHeight": 6,
"beamThickness": 12,
"gravelThickness": 6,
"finishThickness": 0.5,
"concreteWastePercent": 20,
"overdig": 2,
"steelPatternWall": "#4 @ 12\" OC",
"steelPatternFloor": "#4 @ 12\" OC",
"steelWastePercent": 20,
"uBarSize": 4,
"uBarSpacing": 12,
"wallCage1Pattern": "12x12",
"wallCage1HorizontalBar": 4,
"wallCage1VerticalBar": 4,
"floorCage1Pattern": "12x12",
"floorCage1HorizontalBar": 4,
"floorCage1VerticalBar": 4,
"turnoverHours": 8,
"pipeRunLength": 85,
"features": [
{
"type": "step",
"length": 8,
"width": 1.5,
"heightFromBeam": 12,
"count": 3,
"location": "shallowEnd",
"depthAtLocation": 3.5,
"stepConfiguration": "straight"
},
{
"type": "bench",
"length": 10,
"width": 1.5,
"heightFromBeam": 20,
"location": "leftWall",
"depthAtLocation": 5.0
}
],
"exteriorSurfaces": [
{ "label": "Raised back wall", "length": 16, "exposedHeight": 2.5, "includeCap": true }
],
"spa": {
"attachment": "interior",
"shape": "circle",
"radius": 4,
"depth": 3.5,
"raiseHeight": 12,
"commonWallLength": 6,
"spillwayLength": 2,
"equipment": "shared",
"jetCount": 6,
"gpmPerJet": 12
}
}
Clients
The people and companies projects are built for. Permission section: Clients.
| Endpoint | Permission | Description |
|---|---|---|
GET/clients | View | List clients (?search=, ?limit=, ?offset=) |
GET/client/{id} | View | Get one client |
POST/client | Edit | Create a client |
PATCH/client/{id} | Edit | Update a client |
DELETE/client/{id} | Delete | Delete a client and all their projects |
The client object
A client always carries all six fields; unset ones are null.
{
"id": 7,
"name": "Henderson, James",
"email": "j.henderson@example.com",
"phoneNumber": "+15551234567",
"address": "418 Palm Ridge Dr, Scottsdale AZ",
"notes": null
}
All clients sorted by name. ?search= filters by name or email (case-insensitive, substring). Page with ?limit= (how many to return, at least 1) and ?offset= (how many to skip, default 0); either works on its own. A page shorter than limit is the last one.
GET /clients?search=henderson&limit=25&offset=50
Answers 200 with the created client.
| Field | Type | Notes |
|---|---|---|
namerequired | string | 2–40 characters. |
email | string | Must be a valid address when given (400 InvalidParameter otherwise). |
phoneNumber | string | 7–15 digits, optional leading +; spaces and dashes are ignored. |
address | string | 2–500 characters. |
notes | string | Free text. |
Partial update with one quirk: all five keys must be present — send null for the ones you are not changing, or the request is rejected with 400 BadJsonBody. A null leaves the field alone; an empty string clears phoneNumber, address or notes. The same validation as on create applies. 409 EmailExists when another client already uses the email.
PATCH /client/7
{
"name": "Henderson, James R.",
"email": null,
"phoneNumber": "+15559876543",
"address": null,
"notes": null
}
Answers 200 with an empty body. Every project of the client, and each project's history, is deleted with it. Check GET /projects/{clientId} first if that matters to you.
Tax and Markup Profiles
Estimates price line items with a markup preset (a material rate and a labor rate) and a tax profile (material and labor tax rates with on/off switches). Each account has one default of each, used wherever an estimate or section does not override it. Permission section: Tax and Markup Profiles. Keys with Estimates · View can read both lists as well.
| Endpoint | Permission | Description |
|---|---|---|
GET/tax-profiles | View | List tax profiles (?limit=, ?offset=) |
POST/tax-profiles | Edit | Create a tax profile |
GET/tax-profiles/{id} | View | Get one |
PUT/tax-profiles/{id} | Edit | Update (partial) |
PUT/tax-profiles/{id}/default | Edit | Make it the default |
DELETE/tax-profiles/{id} | Delete | Delete (not the default) |
GET/markup-presets | View | List markup presets (?limit=, ?offset=) |
POST/markup-presets | Edit | Create a markup preset |
GET/markup-presets/{id} | View | Get one |
PUT/markup-presets/{id} | Edit | Update (partial) |
PUT/markup-presets/{id}/default | Edit | Make it the default |
DELETE/markup-presets/{id} | Delete | Delete (not the default) |
Tax profile
| Field | Type | Notes |
|---|---|---|
id | uuid | |
namerequired | string | 2–255 characters. |
staterequired | string | Up to 120 characters; may be empty. |
locality | string | Up to 120 characters. Default "". |
materialTaxRaterequired | number | Fraction, e.g. 0.0875. |
laborTaxRate | number | Fraction. Default 0. |
taxMaterials | boolean | Default true. When false material lines are not taxed even if a rate is set. |
taxLabor | boolean | Default false. |
isDefault | boolean | On create only; the first profile of an account becomes the default automatically. Change later with PUT …/default. |
notes | string | Up to 10,000 characters. |
createdAt, updatedAt | integer | Epoch milliseconds. |
POST /tax-profiles
{
"name": "CA – Los Angeles",
"state": "CA",
"locality": "Los Angeles County",
"materialTaxRate": 0.0875,
"laborTaxRate": 0.0,
"taxMaterials": true,
"taxLabor": false
}
201 Created
{
"id": "5e7c9a02-4b18-49d3-bb61-7f2c8e105d3a",
"userId": 42,
"name": "CA – Los Angeles",
"state": "CA",
"locality": "Los Angeles County",
"materialTaxRate": 0.0875,
"isDefault": true,
"createdAt": 1757500800000,
"updatedAt": 1757500800000
}
Updates (PUT /tax-profiles/{id}) accept any subset of name, state, locality, materialTaxRate, laborTaxRate, taxMaterials, taxLabor and notes. Deleting the default profile is refused with 400 BadRequest — make another one the default first.
Markup preset
| Field | Type | Notes |
|---|---|---|
id | uuid | |
namerequired | string | 2–255 characters. |
materialRaterequired | number | Markup on material lines as a fraction: 0.30 = 30%. |
laborRaterequired | number | Markup on labor lines as a fraction. |
color | string | Optional swatch for the app, up to 20 characters (e.g. "#2E86AB"). |
isDefault | boolean | Same rules as tax profiles. |
createdAt, updatedAt | integer | Epoch milliseconds. |
Updates accept any subset of name, materialRate, laborRate and color. Deleting the default preset is refused with 400 BadRequest.
Estimates and Templates
An estimate is a tree of sections holding line items. Each line item selects an inventory item with a small formula (for example {Excavation Labor} * digVolume) that also yields the quantity, so prices and quantities follow the inventory and the bound project's model. A template is a reusable section tree that can be dropped into any estimate. A change order is a full copy of an estimate linked to the original, so a chain of revisions can be compared.
Permission section: Estimates and Templates. View on it also grants View on Tax and Markup Profiles and on Inventory.
| Endpoint | Permission | Description |
|---|---|---|
GET/estimates | View | List estimates (filters below) |
POST/estimates | Edit | Create an estimate |
GET/estimates/{id} | View | Full estimate with sections and line items |
PUT/estimates/{id} | Edit | Update name, client, notes, validity, tags, preferred vendors |
DELETE/estimates/{id} | Delete | Delete an estimate |
PATCH/estimates/{id}/status | Edit | Set DRAFT, SENT, ACCEPTED or DECLINED |
POST/estimates/{id}/duplicate | Edit | Copy as a new, unrelated draft |
POST/estimates/{id}/change-order | Edit | Copy as the next revision of the chain |
GET/estimates/{id}/chain | View | The original and all its change orders |
GET/estimates/{id}/changes | View | Diff against the previous revision |
POST/estimates/{id}/bind-project | Edit | Attach a project and snapshot its model |
POST/estimates/{id}/unbind-project | Edit | Detach the project |
POST/estimates/{id}/refresh-prices | Edit | Re-price a draft from current inventory |
GET/estimates/{id}/totals | View | Subtotals, tax, total and margins |
GET/estimates/{id}/export-pdf | View | PDF (?view=, ?scope=) |
POST/estimates/{estimateId}/sections | Edit | Add a top-level section |
POST/estimates/{estimateId}/sections/from-template/{templateId} | Edit | Instantiate a template (?parentSectionId=) |
GET/templates | View | List templates (?search=, ?limit=, ?offset=) |
POST/templates | Edit | Create an empty template |
GET/templates/{id} | View | Template with its whole tree |
PUT/templates/{id} | Edit | Rename, describe, re-tag |
DELETE/templates/{id} | Delete | Delete a template |
POST/templates/{id}/duplicate | Edit | Copy a template |
POST/templates/from-estimate/{estimateId} | Edit | Save an estimate as a template |
GET/sections/{id} | View | A section with its subtree |
PUT/sections/{id} | Edit | Rename, describe, reorder, re-parent |
PUT/sections/{id}/overrides | Edit | Set or clear markup/tax overrides |
DELETE/sections/{id} | Delete | Delete a section and everything in it |
POST/sections/{id}/sections | Edit | Add a child section |
POST/sections/{id}/sections/reorder | Edit | Reorder child sections |
POST/sections/{id}/items | Edit | Add a line item |
POST/sections/{id}/items/reorder | Edit | Reorder line items |
PUT/line-items/{id} | Edit | Update a line item |
PUT/line-items/{id}/overrides | Edit | Set or clear its markup/tax overrides |
DELETE/line-items/{id} | Delete | Delete a line item |
GET/estimate-tags | View | Tags for estimates, templates and inventory (?limit=, ?offset=) |
POST/estimate-tags | Edit | Create (or fetch by name) a tag |
GET/estimate-tags/{id}/usage | View | Where a tag is used |
PUT/estimate-tags/{id} | Edit | Rename or recolour |
DELETE/estimate-tags/{id} | Delete | Delete a tag |
POST/formulas/validate | View | Check a line-item formula |
GET/formulas/variables | View | Model variables usable in formulas |
The estimate object
| Field | Type | Description |
|---|---|---|
id | uuid | |
name | string | 2–255 characters. |
status | string | DRAFT (default), SENT, ACCEPTED, DECLINED. Leaving DRAFT freezes every line item's cost; only drafts can be re-priced or re-bound to a project. |
clientId, clientName | integer, string | The client, either set directly or taken from the bound project. |
projectId, projectName | integer, string | The bound project, if any. |
projectVariableSnapshot | object | Copy of the project's model taken by bind-project; formulas evaluate against it. |
preferredVendorIds, preferredVendorNames | uuid[], string[] | Suppliers whose prices win, in priority order. |
notes | string | Up to 10,000 characters. |
validUntil | integer | Epoch milliseconds, optional. |
originalEstimateId | uuid | Absent on an original; the chain root's id on a change order. |
rootSectionId | uuid | The invisible root section that carries the estimate-level overrides. Change them with PUT /sections/{rootSectionId}/overrides. |
rootMarkupPresetId, rootMarkupPresetName, rootMarkupPresetMaterialRate, rootMarkupPresetLaborRate | mixed | The estimate-level markup preset, if set. |
rootTaxProfileId, rootTaxProfileName | uuid, string | The estimate-level tax profile, if set; otherwise the account default applies. |
tags | object[] | Each { id, name, color, createdAt }. |
totalCost | number | Only in GET /estimates: the sum of quantity × unit cost over all line items. |
sections | object[] | Only in GET /estimates/{id}: the top-level sections, each with children and lineItems. |
creatorName, creatorEmail | string | Who created it. |
createdAt, updatedAt | integer | Epoch milliseconds. |
Newest-updated first. Query: status (one of the four statuses), search (substring of the name), tagId, limit (default 50) and offset (default 0). Items carry totalCost but no sections.
Creates a DRAFT with its root section. Answers 201 with the estimate. To price it against a project, follow up with bind-project.
| Field | Type | Notes |
|---|---|---|
namerequired | string | 2–255 characters. |
taxProfileIdrequired | uuid | The estimate-level tax profile. |
markupPresetId | uuid | The estimate-level markup preset. Without one the account's default markups apply. |
clientId | integer | |
projectId | integer | Stored as-is; does not snapshot the model (use bind-project). |
notes | string | Up to 10,000 characters. |
validUntil | integer | Epoch milliseconds. |
tagIds | uuid[] |
POST /estimates
{
"name": "Johnson Backyard Pool",
"taxProfileId": "5e7c9a02-4b18-49d3-bb61-7f2c8e105d3a",
"markupPresetId": "b2f4a610-77c1-4e39-9d85-0a3f61c8d244",
"clientId": 118,
"notes": "Includes spa."
}
201 Created
{
"id": "3f9b1c2e-5d41-4a77-9f0b-6c2d8a1e0b53",
"userId": 42,
"name": "Johnson Backyard Pool",
"clientId": 118,
"clientName": "Dana Johnson",
"status": "DRAFT",
"notes": "Includes spa.",
"rootSectionId": "c41d9b77-2e58-4f10-8b6a-1d7e3c095aa2",
"rootMarkupPresetId": "b2f4a610-77c1-4e39-9d85-0a3f61c8d244",
"rootMarkupPresetName": "Standard",
"rootMarkupPresetMaterialRate": 0.3,
"rootMarkupPresetLaborRate": 0.5,
"rootTaxProfileId": "5e7c9a02-4b18-49d3-bb61-7f2c8e105d3a",
"rootTaxProfileName": "CA – Los Angeles",
"createdAt": 1757500800000,
"updatedAt": 1757500800000
}
Partial update: any subset of name, clientId, notes, validUntil, tagIds (replaces the tag set) and preferredVendorIds (replaces the ordered vendor list; each must be one of your suppliers). Fields you omit are unchanged; this endpoint cannot clear a field.
Body {"status": "SENT"}. Moving out of DRAFT snapshots every line item's resolved inventory item and unit cost, so later inventory price changes no longer move the estimate. Any status may be set from any other.
Copies the estimate (sections, items, overrides, snapshot) as a new DRAFT named "<name> — Change order N" and linked to the chain root. Refused with 409 Conflict while any revision in the chain is SENT. GET /estimates/{id}/chain lists the chain oldest first as { id, originalEstimateId, name, status, creatorName, creatorEmail, createdAt, updatedAt }; GET /estimates/{id}/changes diffs a change order against the revision before it, returning { baselineName, revisedName, sections: [{ name, depth, type, items: [{ description, type, oldQty, newQty, oldUnitPrice, newUnitPrice, oldTotal, newTotal }], oldTotal, newTotal }], baselineTotal, revisedTotal } where type is ADDED, REMOVED, MODIFIED or UNCHANGED.
Body {"projectId": 91}. Sets the project and copies its current model into projectVariableSnapshot; binding again refreshes the snapshot. Drafts only (400 otherwise). POST …/unbind-project clears both.
{
"sections": [
{ "name": "Excavation", "materialCost": 0.0, "laborCost": 3145.0, "materialSell": 0.0, "laborSell": 4717.5, "sectionTax": 0.0, "sectionTotal": 4717.5 },
{ "name": "Equipment", "materialCost": 4820.0, "laborCost": 0.0, "materialSell": 6266.0, "laborSell": 0.0, "sectionTax": 548.28, "sectionTotal": 6814.28 }
],
"summary": { "materialSubtotal": 6266.0, "laborSubtotal": 4717.5, "subtotal": 10983.5, "totalTax": 548.28, "grandTotal": 11531.78 },
"taxProfile": { "name": "CA – Los Angeles", "materialTaxRate": 0.0875, "laborTaxRate": 0.0 },
"profitAnalysis": { "totalCost": 7965.0, "totalRevenue": 11531.78, "totalTax": 548.28, "grossProfit": 3018.5, "grossMarginPercent": 26.2, "materialMarginPercent": 23.1, "laborMarginPercent": 33.3 }
}
sections lists every section at every depth, in tree order. Margin percentages are whole percents.
application/pdf. ?view= is client-itemized (default), client-phase, client-lump or builder; ?scope=changes renders the change-order diff instead of the whole estimate.
Sections
A section is { id, estimateId, parentSectionId, name, description, sortOrder, isTemplate, isRoot, templateSourceId, markupPresetId, markupPresetName, markupPresetMaterialRate, markupPresetLaborRate, taxProfileId, taxProfileName, tags, children, lineItems, createdAt, updatedAt }. Templates are sections with isTemplate: true and no estimateId. Markup and tax resolve nearest-ancestor first: a line item's own override, then the closest section with one, then the estimate's root section, then the account defaults.
Adds a section under the estimate's root (or under parentSectionId). POST /sections/{id}/sections adds a child to any section, template sections included. Answers 201 with the section.
| Field | Type | Notes |
|---|---|---|
namerequired | string | 2–255 characters. |
description | string | Up to 5,000 characters. |
sortOrder | integer | Omit to append. |
parentSectionId | uuid | Nest under this section instead of the root. |
markupPresetId, taxProfileId | uuid | Overrides for this section; omit to inherit. |
templateSourceId | uuid | Copy this template's tree instead of creating an empty section. |
No body. Copies the template's tree into the estimate, under ?parentSectionId= or the root. Every formula in the template is first checked against the estimate's model snapshot; if one references a calculator output the model cannot produce, the whole call is refused with 400 and the description names the outputs.
Body {"markupPresetId": "…", "taxProfileId": null}. This is a replace: a field you omit or set to null is cleared back to inherit. Use it with estimate.rootSectionId to change the estimate-level overrides. PUT /sections/{id}, by contrast, only sets what you send (name, description, sortOrder, parentSectionId).
Body {"sectionIds": [...]} — exactly the current child ids, in the new order. POST /sections/{id}/items/reorder takes {"itemIds": [...]} the same way. Both answer {"status": "reordered"}; a list that is not exactly the current set is refused with 400.
Line items
A line item is { id, sectionId, formula, descriptionOverride, inventoryItemIds, resolvedInventoryItemId, resolvedInventoryItemName, unit, quantity, unitCost, liveCost, unitPriceOverride, costType, taxStatus, taxRateOverride, markupOverride, sortOrder, createdAt, updatedAt }. The resolved fields, unit, quantity, unitCost and costType are computed from the formula and the inventory — you set the formula, Aquaticus fills the rest. liveCost appears on drafts whose snapshot differs from today's inventory price.
| Field | Type | Notes |
|---|---|---|
sectionIdrequired | uuid | Must be present in the body; the path wins if they differ. |
formularequired | string | See Formulas. Validated on save; the first problem is returned as 400. |
descriptionOverride | string | Replaces the inventory item's description on output. Up to 5,000 characters. |
unitPriceOverride | number | Explicit sell price per unit; bypasses markup. |
taxStatus | string | TAXABLE (default) or EXEMPT. |
taxRateOverride | number | Fraction; beats the tax profile for this line. |
markupOverride | number | Fraction; one number for either cost type. |
sortOrder | integer | Omit to append. |
POST /sections/7b21e4d9-0a53-4c68-9e17-8f4b2d60c1a5/items
{
"sectionId": "7b21e4d9-0a53-4c68-9e17-8f4b2d60c1a5",
"formula": "{Excavation Labor} * digVolume"
}
201 Created
{
"id": "e0c7a914-6b32-4d85-a1f7-52d9b3e08c46",
"sectionId": "7b21e4d9-0a53-4c68-9e17-8f4b2d60c1a5",
"inventoryItemIds": ["9d13f6c8-2a47-4e91-b035-7c6e148af2d0"],
"formula": "{Excavation Labor} * digVolume",
"resolvedInventoryItemName": "Excavation Labor",
"unit": "yd³",
"quantity": 135.2,
"unitCost": 42.5,
"taxStatus": "TAXABLE",
"createdAt": 1757500800000,
"updatedAt": 1757500800000
}
PUT /line-items/{id} accepts any subset of the same fields plus sectionId to move the item; omitted fields stay. PUT /line-items/{id}/overrides takes { markupOverride, taxStatus, taxRateOverride } as a replace — omitted overrides are cleared and an omitted taxStatus resets to TAXABLE.
Formulas
A line-item formula names exactly one inventory item in braces and, optionally, multiplies it by an expression over the estimate's model:
{Variable-Speed Pump 2.7HP}
{Rebar #4} * rebarFeet / 20
{Excavation Labor} * digVolume
if length > 40 use {Large Skimmer} else {Standard Skimmer}
{Name}refers to an inventory item by its exact name.- Model variables (
length,deepDepth, …) and calculator outputs (digVolume,rebarFeet,perimeter, …) come from the bound project's snapshot.GET /formulas/variableslists the variables with descriptions, units and advisory ranges;?scope=alladds the nested spa and feature fields. - An
if … use … else …chain may pick different items per branch, as long as each branch selects exactly one item.
Body {"formula": "…", "estimateId": "…"}. With estimateId the formula is checked against that estimate's snapshot; without it, against every known variable and output. Always 200 — read ok:
{
"ok": false,
"errors": [{ "message": "Unknown inventory item '{Pmup}'", "start": 0, "end": 6 }],
"projectVariables": [],
"inventoryItemNames": [],
"projectOutputs": []
}
Templates
Body { "name", "description", "tagIds" } creates an empty template (201); build it up with POST /sections/{id}/sections and POST /sections/{id}/items. PUT /templates/{id} updates name, description and tags. POST /templates/from-estimate/{estimateId} with { "name", "description" } turns an estimate into a template, keeping its markup and tax overrides but dropping the frozen prices so the template re-prices wherever it is used. GET /templates lists templates without their trees; GET /templates/{id} includes the whole tree.
Estimate tags
GET /estimate-tags returns [{ id, name, color, createdAt, usageCount }], pageable with ?limit= and ?offset=. POST /estimate-tags with { "name", "color" } creates a tag, or returns the existing one when a tag with that name (case-insensitive) already exists. PUT /estimate-tags/{id} renames or recolours it (409 Conflict on a name clash); GET /estimate-tags/{id}/usage answers { inventoryCount, estimateCount, templateCount }; DELETE removes the tag from everything.
Inventory
Inventory items are the materials and labor rates estimates are built from. A material item carries one or more vendor prices (one per supplier), exactly one of which is preferred and sets the item's effective unitCost; a labor item has a plain cost. Items are grouped in categories (two levels) and can carry tags and notes. Permission section: Inventory.
| Endpoint | Permission | Description |
|---|---|---|
GET/inventory | View | List items (filters below; ?limit= default 100, ?offset=) |
POST/inventory | Edit | Create an item |
GET/inventory/{id} | View | Get one item |
PUT/inventory/{id} | Edit | Update an item (partial) |
DELETE/inventory/{id} | Delete | Deactivate an item |
POST/inventory/bulk | Edit | Bulk delete / tag / categorise / adjust cost |
POST/inventory/refresh-prices | Edit | Pull current prices from live vendors |
POST/inventory/apply-vendor | Edit | Attach a live vendor to many items |
GET/inventory/export.csv | View | Download the inventory as CSV |
POST/inventory/import.csv | Edit | Upload a CSV |
GET/inventory/{id}/price-history | View | Cost changes over time (?limit=, ?offset=) |
GET/inventory/{id}/notes | View | Notes on an item (?limit=, ?offset=) |
POST/inventory/{id}/notes | Edit | Add a note |
DELETE/inventory/{id}/notes/{noteId} | Delete | Delete a note |
GET/inventory/{itemId}/prices | View | Vendor prices of an item |
POST/inventory/{itemId}/prices | Edit | Add a vendor price |
PUT/inventory/{itemId}/prices/{priceId} | Edit | Update a vendor price |
DELETE/inventory/{itemId}/prices/{priceId} | Delete | Remove a vendor price |
GET/inventory/categories | View | All categories (flat; ?limit=, ?offset=) |
POST/inventory/categories | Edit | Create a category |
PUT/inventory/categories/{id} | Edit | Update a category |
DELETE/inventory/categories/{id} | Delete | Delete a category |
GET/suppliers | View | Your suppliers (?limit=, ?offset=) |
GET/suppliers/predefined | View | Vendor integrations Aquaticus ships with |
POST/suppliers | Edit | Add a supplier |
PUT/suppliers/{id} | Edit | Update a supplier |
DELETE/suppliers/{id} | Delete | Delete a supplier and its prices |
GET/suppliers/{id}/search | View | Search a live vendor catalogue |
GET/suppliers/{id}/categories | View | A live vendor's category tree |
The inventory item
| Field | Type | Description |
|---|---|---|
id | uuid | |
name | string | 2–255 characters; may not contain { or } (formulas reference items by name in braces). |
description, sku, mpn | string | Free text, internal SKU and manufacturer part number. Default "". |
unit | string | Normalised on save to each, ft, sq ft, hr, day, flat, ton or yd³ when it matches a known alias (ea, lf, sqft, cy, …); other values are kept as given. |
costType | string | MATERIAL or LABOR. |
unitCost | number | Effective cost: the preferred vendor's price for a material item, the plain cost for labor. |
vendorCosts | object[] | The item's vendor prices (see below), preferred first. |
preferredVendorName, preferredVendorCost | string, number | Convenience copies of the preferred price. |
categoryId, categoryName, subcategoryId, subcategoryName | uuid, string | |
taxStatusDefault | string | TAXABLE (default) or EXEMPT. |
isActive | boolean | DELETE sets it to false; set it back to true with PUT to restore. |
autoUpdateFromApi | boolean | Default true; lets refresh-prices update this item. |
tags | object[] | { id, name, color, createdAt }. |
createdAt, updatedAt | integer | Epoch milliseconds. |
Vendor price
{ id, inventoryItemId, supplierId, supplierName, supplierAdapterType, supplierSku, cost, isPreferred, autoUpdate, notes, lastUpdated, createdAt }. Exactly one price per item is isPreferred. supplierAdapterType is magento for a live catalogue or manual.
| Query | Default | Notes |
|---|---|---|
search | Substring of name, SKU, MPN, description or a vendor SKU. | |
categoryId, subcategoryId, tagId | Exact matches. | |
costType | MATERIAL or LABOR. | |
activeOnly | true | false includes deactivated items. |
sort | NAME_ASC | NAME_ASC, NAME_DESC, COST_ASC, COST_DESC, UPDATED_DESC, SKU_ASC, SKU_DESC, MPN_ASC, MPN_DESC, CATEGORY_ASC, CATEGORY_DESC, VENDOR_ASC. |
limit, offset | 100, 0 | |
withCount | false | true wraps the result as { "data": [...], "total": n }. |
Answers 201 with the item. A MATERIAL item needs at least one entry in vendorCosts; a LABOR item takes unitCost directly and ignores vendorCosts.
| Field | Type | Notes |
|---|---|---|
namerequired | string | 2–255 characters, no braces. |
unitrequired | string | 1–50 characters. |
costTyperequired | string | MATERIAL or LABOR. |
vendorCosts | object[] | Each { supplierId, cost, supplierSku, isPreferred, autoUpdate }; one per supplier; the first isPreferred wins (or the first entry). Required for materials. |
unitCost | number | Labor only. |
description, sku, mpn | string | Up to 5,000 / 100 / 100 characters. |
categoryId, subcategoryId | uuid | |
taxStatusDefault | string | Default TAXABLE. |
autoUpdateFromApi | boolean | Default true. |
tagIds | uuid[] |
POST /inventory
{
"name": "Pentair IntelliFlo VSF Pump",
"sku": "PMP-3HP-VSF",
"unit": "each",
"costType": "MATERIAL",
"categoryId": "3a6c1f80-11d2-4c3e-8a2b-9c0f77b1a201",
"vendorCosts": [
{ "supplierId": "5d2b8c91-4e07-4f3a-9b11-2a8e6c4d0f77", "cost": 1289.5, "supplierSku": "PEN-011056", "isPreferred": true }
]
}
201 Created
{
"id": "8f14e45f-ceea-467a-9a36-dedd4bea2543",
"userId": 42,
"name": "Pentair IntelliFlo VSF Pump",
"sku": "PMP-3HP-VSF",
"unit": "each",
"unitCost": 1289.5,
"costType": "MATERIAL",
"categoryId": "3a6c1f80-11d2-4c3e-8a2b-9c0f77b1a201",
"categoryName": "Equipment",
"preferredVendorName": "Heritage Pool Plus",
"preferredVendorCost": 1289.5,
"vendorCosts": [
{
"id": "e1c9a77b-2f40-4b8d-9a6e-77c1d0e3b512",
"inventoryItemId": "8f14e45f-ceea-467a-9a36-dedd4bea2543",
"supplierId": "5d2b8c91-4e07-4f3a-9b11-2a8e6c4d0f77",
"supplierName": "Heritage Pool Plus",
"supplierAdapterType": "magento",
"supplierSku": "PEN-011056",
"cost": 1289.5,
"isPreferred": true,
"lastUpdated": 1757635200000,
"createdAt": 1757635200000
}
],
"createdAt": 1757635200000,
"updatedAt": 1757635200000
}
PUT /inventory/{id} takes any subset of the same fields plus isActive and clearSubcategory. A vendorCosts or tagIds you send replaces the whole list. Every cost change is recorded in the price history.
Body { "op", "itemIds", ... } where op is DELETE (deactivate), ADD_TAGS / REMOVE_TAGS (with tagIds), SET_CATEGORY (with categoryId, subcategoryId, clearSubcategory) or ADJUST_COST (with costMultiplier > 0, applied to the item and all its vendor prices). Answers { "op", "affected" }.
Re-reads the price of every vendor cost that opted in (autoUpdate on the price, autoUpdateFromApi on the item, a live vendor and a vendor SKU) and is older than ?maxAgeMs= (default 6 hours), up to 100 per call. Answers { "updated", "unchanged", "failed", "errors" }. POST /inventory/apply-vendor with { "supplierId", "itemIds", "makePreferred" } looks up to 200 material items up in a live vendor's catalogue and attaches the matches, answering { "applied", "unmatched", "skipped", "unmatchedNames", "errors" }.
text/csv of all active items with the columns name, sku, mpn, description, unit, unitCost, costType, vendor, category, subcategory, taxStatus, tags (tags separated by |).
POST /inventory/import.csv takes the same layout as a raw CSV body (Content-Type: text/csv, up to 5 MB and 50,000 rows). name, unit, unitCost and costType are required columns; vendor is required for material rows. Unknown categories, tags and vendors are created on the fly. ?updateExistingBySku=true updates items whose SKU matches instead of skipping them. Always answers 200 { "created", "updated", "skipped", "errors": ["Line 14: …"] } — check errors.
Newest first: [{ id, inventoryItemId, oldCost, newCost, source, note, changedAt }] with source one of MANUAL, API, IMPORT, BULK. Notes work the same way: GET /inventory/{id}/notes → [{ id, inventoryItemId, body, source, createdAt }], POST with { "body" } (up to 10,000 characters) → 201, DELETE …/notes/{noteId} → 204.
Body { "inventoryItemId", "supplierId", "cost", "supplierSku", "isPreferred", "autoUpdate", "notes" } (inventoryItemId must be present; the path value is used). PUT …/prices/{priceId} updates any subset of cost, supplierSku, isPreferred, autoUpdate, notes; DELETE removes the price. Prefer PUT /inventory/{id} with a full vendorCosts list when you also need the item's unitCost and price history kept in step.
Categories
A category is { id, userId, name, sortOrder, parentId }; parentId makes it a subcategory (one level deep). POST /inventory/categories with { "name", "sortOrder", "parentId" } → 201; PUT accepts name, sortOrder, parentId and clearParent; DELETE → 204. Items keep their category ids after a delete, so re-categorise them first.
Suppliers
A supplier is { id, userId, name, adapterType, vendorKey, baseUrl, isActive, hasCustomerLogin, createdAt, updatedAt }; credentials are never returned. GET /suppliers/predefined lists the vendor integrations Aquaticus ships with (currently Heritage Pool Plus, key heritage_pool_plus).
A manual supplier just needs { "name" }. A live one names a predefined vendor and its customer login: { "name", "vendorKey": "heritage_pool_plus", "customerEmail", "customerPassword" }; the login is verified with the vendor before saving (401 SupplierAuthFailed when it is rejected). PUT /suppliers/{id} updates name, baseUrl and the credentials. DELETE /suppliers/{id} also removes every vendor price that references the supplier. GET /suppliers/{id}/search?q=&category=&page=&pageSize= and GET /suppliers/{id}/categories query a live vendor's catalogue.
Error Reference
| Status | error | When |
|---|---|---|
| 400 | BadRequest | A value failed validation; the description says which. |
| 400 | BadJsonBody | The body is not valid JSON for this endpoint: a missing required field, a wrong type or an unknown key. |
| 400 | InvalidParameter | A path parameter or a model value is invalid (e.g. a non-numeric project id, a negative dimension). |
| 400 | InvalidHistoryEntry, NoVariables | Rollback: the entry belongs to another project, or holds no model to restore. |
| 401 | Unauthorized | Missing or unknown API key. |
| 401 | SupplierAuthFailed | A live vendor rejected the stored customer login. |
| 403 | ApiKeyPermissionDenied | The key lacks the View, Edit or Delete permission this request needs. |
| 403 | ApiKeyEndpointForbidden | The endpoint is not available to API keys at all. |
| 403 | EmailNotVerified, AlphaAccessRequired | The account behind the key is not verified or approved. |
| 403 | Forbidden | The action is not allowed for this account or object. |
| 404 | NotFound | No such object in your account. |
| 409 | Conflict, ClientExists, EmailExists | The change collides with existing data (a change order while one is SENT, a duplicate tag name, a client email already in use). |
| 413 | PayloadTooLarge | Body over 10 MB. |
| 500 | exception class name | Something unexpected, including a malformed UUID in a URL. Retry with corrected input; report persistent failures. |
Need Help?
Questions about the API, or a use case the permissions don't cover yet? We'd like to hear it.
- Email: support@aquaticus.tech
- App guide: Documentation