API reference Equipment
Equipment Service API Endpoints
The production API base URL is https://api.tradesmentechnologies.com/.
Authentication
All endpoints require a Bearer token in the Authorization header:
Authorization: Bearer <your_token>
Mutating requests may carry an Idempotency-Key header so a retry never acts twice — see Idempotent Retries in the Accounts API documentation.
Every employee type (ADMIN, LEAD_TECHNICIAN, TECHNICIAN, CREW_TECHNICIAN) may call every equipment-record and photo endpoint — there is no role restriction on equipment records. The one exception is Update Equipment Catalog, which is ADMIN only.
Base Path
/prod/equipment
Concepts
Equipment records track equipment left at a customer's property (A/C units, water heaters, softeners, etc.). Each record belongs to one customer of the authenticated company and carries an optional photo set.
Equipment record shape
{
"companyId": "company-uuid",
"equipmentId": "equipment-uuid",
"customerId": "customer-uuid",
"name": "Upstairs A/C",
"brand": "Carrier",
"model": "24ACC636A003",
"serialNumber": "1234E56789",
"installDate": "2021-06-14",
"warrantyExpirationDate": "2031-06-14",
"locationOnProperty": "Attic, north side",
"notes": "Filter size 16x25x1",
"address": {
"streetAddress": "456 Avenue",
"city": "City",
"state": "State",
"zipCode": "12345"
},
"attributes": {
"hvacType": "HEAT_PUMP",
"tonnage": 3,
"seer": 16,
"refrigerant": "R-410A",
"filterSize": "16x25x1"
},
"photos": [
{
"photoId": "photo-uuid",
"s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg",
"fileName": "unit.jpg",
"contentType": "image/jpeg",
"size": 123456,
"uploadedAt": "2026-08-23T12:00:00.000Z"
}
],
"createdAt": "2026-08-23T12:00:00.000Z",
"updatedAt": "2026-08-23T12:00:00.000Z"
}
Field notes:
nameis the only required client field besidescustomerId; it must be a non-empty string.brand,model,serialNumber,locationOnProperty, andnotesare optional strings (nullwhen unset).installDateandwarrantyExpirationDateare optional date-onlyYYYY-MM-DDstrings (nullwhen unset) — never full timestamps.addressis an optional snapshot copy of the property address (the common Address shape); it is stored verbatim and never re-derived from the customer record.nullwhen unset.attributesis an optional flat map of extra specs defined by the client app (the HVAC app storeshvacType,fuelType,tonnage,seer,refrigerant,filterSize; the backend imposes no vocabulary). Validation is shape-only: at most 50 keys, each key a short identifier (^[A-Za-z][A-Za-z0-9_]*$, ≤ 64 chars), each value a string (≤ 500 chars), a finite number, ornull. Nested objects/arrays/booleans are rejected. Stored verbatim;nullwhen unset. On update the map is replaced as a whole (never merged per key) — send the full map,nullto clear, or omit it to keep the stored value. Records created before this field existed return it as absent/null.photos,createdAt,updatedAt,companyId, andequipmentIdare server-managed.photosis written only by the photo endpoints below — it is never writable via create/update.customerIdis immutable after creation; acustomerIdsent on update is ignored.
The full company equipment list is also returned on the login bootstrap as the top-level customerEquipment array (see accounts-endpoints.md).
Equipment catalog
Superseded 2026-09-21 — kept for shipped app versions only. Current app versions no longer read or write the catalog: the equipment a company installs is an inventory item flagged
isEquipment(withbrand,model,notes,attributes— seeinventory-endpoints.md), and that item is the prefill source for a new customer-equipment record. TheequipmentCataloglist, its bootstrap field, and the two/equipment/catalogendpoints below stay fully functional so released app versions keep working; a one-time server-side migration copies catalog entries into inventory (the catalog itself is left untouched). Do not build new features on the catalog.
Separate from the per-customer records, a company can keep an equipment catalog — the equipment it installs often (e.g. the A/C models it sells). The app offers the catalog as a prefill source when adding equipment to a customer; nothing on the backend links a customer record back to a catalog item. The catalog is stored as the equipmentCatalog list on the company row, returned on the login bootstrap as the top-level equipmentCatalog array, and managed via the two /equipment/catalog endpoints below.
{
"catalogItemId": "catalog-item-uuid",
"name": "Carrier 3-ton A/C",
"brand": "Carrier",
"model": "24ACC636A003",
"notes": "Filter size 16x25x1",
"attributes": { "hvacType": "AIR_CONDITIONER", "tonnage": 3, "seer": 16 }
}
nameis required (non-empty string; trimmed).brand,model, andnotesare optional strings (trimmed; blank →null).attributesis the same optional flat spec map as on equipment records (same validation,nullwhen unset). BecausePUT /equipment/catalogreplaces the whole list, an item sent withoutattributesis stored withattributes: null.catalogItemIdis server-generated when an item is sent without one (or withnull/""); send the stored id back to keep an item's identity across saves. Ids must be unique within the list.- At most 200 items.
HVAC model catalog
A global (not per-company) reference list of certified HVAC models that powers the app's model-number auto-complete — searching it never reads or changes company data. It is built from public government sources: ENERGY STAR open data (heat pumps, furnaces, boilers — refreshed weekly) and the DOE Compliance Certification Database (every certified central A/C, heat pump and furnace — refreshed periodically). If the catalog has not been built yet, searches return no models.
Each model:
model— the model number as certified, which may contain wildcards:*stands for zero or one character and(A,B)for one of the listed alternatives (25VNA448A*031*,(C,H,T)5A1V36*K*WAA*), and an accessory suffix may follow a+. Searching understands these patterns; when prefilling a form, prefer what the user typed from the nameplate over the pattern.brands— the brands the unit's maker(s) certify it under (at most 8; one canonical spelling per brand across the catalog), the brand matching the query first, otherwise the maker with the most certifications first; component makers (coil / air-handler brands that certify pairings with the unit) list after them.[]when only component makers certify the model — the maker is unknown (typically a discontinued line), so leave the brand to the user rather than prefilling a coil maker.hvacType—FURNACE|AIR_CONDITIONER|HEAT_PUMP|MINI_SPLIT|BOILER, ornull.fuelType—GAS|ELECTRIC|OIL|PROPANE, ornull. Both use the vocabulary of the equipmentattributesmap.tonnage— nominal tons (the size encoded in the model number when it agrees with the certified capacity, otherwise the certified capacity rounded to the nearest half ton);nullfor heating-only equipment.coolingBtu— certified cooling capacity in BTU/h.seer2,hspf2,afue— the best certified rating among the model's combinations at itscoolingBtu(the most common certified capacity; a lone outlier row never sets the size);nullwhen not applicable. Ratings are SEER2 / HSPF2 (the 2023 test procedure), never legacy SEER.refrigerant—R-410A|R-22|R-32|R-454B, ornullwhen no source states it (it is never inferred).
Endpoints
Search HVAC Models
Model-number auto-complete over the HVAC model catalog (see Concepts). Any employee. Safe to call on every keystroke.
- Method: GET
- Path:
/prod/equipment/models/search - Headers:
Authorization: Bearer <token> - Query Parameters:
q— the search text (at most 100 characters). Every whitespace-separated word must match the model number (prefix of the certified pattern, or contained in it when the word has 3+ characters) or the start of a brand word, ignoring case and punctuation:25vna,lennox el18,25VNA448A003100. Fewer than 2 letters/digits in total returns no models — unlessbrandis given, in which case an emptyqlists that brand's models.type— optional; only models of thishvacType(FURNACE|AIR_CONDITIONER|HEAT_PUMP|MINI_SPLIT|BOILER). The form passes the type the user already selected so "goodman gs" stops returning furnaces when an A/C is wanted.brand— optional (at most 100 characters); only models one of whose brands starts with this text, case and punctuation ignored (goodmatchesGoodman). The form passes its Brand field.limit— optional, positive integer, default10, capped at25.
- Success Response:
200 OK
Note: model-prefix matches rank above contains matches; with a{ "models": [ { "model": "25VNA448A*031*", "brands": ["Carrier"], "hvacType": "HEAT_PUMP", "fuelType": "ELECTRIC", "tonnage": 4, "coolingBtu": 44000, "seer2": 21, "hspf2": 10.5, "afue": null, "refrigerant": "R-410A" } ], "catalogBuiltAt": "2026-09-20T07:00:00.000Z" }brandfilter the matching brand is listed first inbrands.modelsis[]when nothing matches;catalogBuiltAtisnullwhile no catalog has been built yet. A failure to read the catalog also yields an emptymodelsrather than an error, so the form the user is filling in keeps working. - Error Responses:
400 Bad Request-qorbrandlonger than 100 characters,typenot one of the listed values, orlimitnot a positive integer.401 Unauthorized- Missing or invalid token.500 Internal Server Error- Unexpected server error.
Search HVAC Brands
Brand auto-complete over the same catalog (any employee): the distinct brands whose name starts with q, most models first. Brand spellings are canonical across the catalog (Goodman, never both GOODMAN and Goodman).
- Method: GET
- Path:
/prod/equipment/models/brands - Headers:
Authorization: Bearer <token> - Query Parameters:
q— optional prefix (at most 100 characters), case and punctuation ignored; empty lists every brand (up tolimit).type— optional; count and list only brands with models of thishvacType(same values as above).limit— optional, positive integer, default10, capped at25.
- Success Response:
200 OK
Note:{ "brands": [ { "brand": "Goodman", "models": 412 }, { "brand": "Gree", "models": 96 } ], "catalogBuiltAt": "2026-09-20T07:00:00.000Z" }modelscounts the brand's catalog models within thetypefilter.
Get Equipment Catalog
Return the company's equipment catalog (see Concepts). Any employee.
- Method: GET
- Path:
/prod/equipment/catalog - Headers:
Authorization: Bearer <token> - Success Response:
200 OK
Note:{ "equipmentCatalog": [ { "catalogItemId": "catalog-item-uuid", "name": "Carrier 3-ton A/C", "brand": "Carrier", "model": "24ACC636A003", "notes": "Filter size 16x25x1" } ] }[]when the company has never configured a catalog. - Error Responses:
401 Unauthorized- Missing or invalid token.404 Not Found- Company not found.500 Internal Server Error- Unexpected server error.
Update Equipment Catalog
Replace the company's equipment catalog with the provided list. ADMIN only.
- Method: PUT
- Path:
/prod/equipment/catalog - Headers:
Authorization: Bearer <token> - Request Body:
Note: The whole list is replaced — send every item that should remain (an empty array clears the catalog). Items without a{ "equipmentCatalog": [ { "catalogItemId": "catalog-item-uuid", "name": "Carrier 3-ton A/C", "brand": "Carrier", "model": "24ACC636A003", "notes": "Filter size 16x25x1", "attributes": { "hvacType": "AIR_CONDITIONER", "tonnage": 3, "seer": 16 } }, { "name": "Rheem 50 gal water heater", "brand": "Rheem", "model": "XE50T06ST45U1" } ] }catalogItemIdget a server-generated one. Validation rules are in Concepts. - Success Response:
200 OK
Note: The response is the stored, normalized list (ids assigned, strings trimmed, blank optionals as{ "equipmentCatalog": [ { "catalogItemId": "catalog-item-uuid", "name": "Carrier 3-ton A/C", "brand": "Carrier", "model": "24ACC636A003", "notes": "Filter size 16x25x1", "attributes": { "hvacType": "AIR_CONDITIONER", "tonnage": 3, "seer": 16 } }, { "catalogItemId": "new-catalog-item-uuid", "name": "Rheem 50 gal water heater", "brand": "Rheem", "model": "XE50T06ST45U1", "notes": null, "attributes": null } ] }null). - Error Responses:
400 Bad Request- Missing body,equipmentCatalognot an array, an item without a non-emptyname, a non-string field, an invalidattributesmap, a duplicatedcatalogItemId, or more than 200 items.401 Unauthorized- Missing or invalid token.403 Forbidden- Caller is not anADMIN.404 Not Found- Company not found.500 Internal Server Error- Unexpected server error.
Create Equipment
Create an equipment record for a customer.
- Method: POST
- Path:
/prod/equipment/equipment - Headers:
Authorization: Bearer <token> - Request Body:
Note:{ "customerId": "customer-uuid", "name": "Upstairs A/C", "brand": "Carrier", "model": "24ACC636A003", "serialNumber": "1234E56789", "installDate": "2021-06-14", "warrantyExpirationDate": "2031-06-14", "locationOnProperty": "Attic, north side", "notes": "Filter size 16x25x1", "address": { "streetAddress": "456 Avenue", "city": "City", "state": "State", "zipCode": "12345" }, "attributes": { "hvacType": "HEAT_PUMP", "tonnage": 3, "seer": 16, "refrigerant": "R-410A" } }customerIdand a non-emptynameare required; every other field is optional (see Concepts for per-field validation, including theattributesmap). The customer must exist for the authenticated company.photosstarts as an empty array and cannot be set here. - Success Response:
201 Created{ "equipment": { "companyId": "company-uuid", "equipmentId": "equipment-uuid", "customerId": "customer-uuid", "name": "Upstairs A/C", "brand": "Carrier", "model": "24ACC636A003", "serialNumber": "1234E56789", "installDate": "2021-06-14", "warrantyExpirationDate": "2031-06-14", "locationOnProperty": "Attic, north side", "notes": "Filter size 16x25x1", "address": { "streetAddress": "456 Avenue", "city": "City", "state": "State", "zipCode": "12345" }, "attributes": { "hvacType": "HEAT_PUMP", "tonnage": 3, "seer": 16, "refrigerant": "R-410A" }, "photos": [], "createdAt": "2026-08-23T12:00:00.000Z", "updatedAt": "2026-08-23T12:00:00.000Z" } } - Error Responses:
400 Bad Request- Missing body, missing/invalidcustomerId, missing/emptyname, or a field fails validation (non-string string field, malformed date, non-objectaddress, invalidattributesmap).401 Unauthorized- Missing or invalid token.404 Not Found- The customer does not exist for this company.500 Internal Server Error- Unexpected server error.
Get Equipment
List the company's equipment records, optionally filtered by customer.
- Method: GET
- Path:
/prod/equipment/equipment - Query Parameters:
customerId- optional; when present, only that customer's equipment is returned. - Headers:
Authorization: Bearer <token> - Success Response:
200 OK
Note:{ "equipment": [] }equipmentis an array of equipment records (shape in Concepts). - Error Responses:
401 Unauthorized- Missing or invalid token.500 Internal Server Error- Unexpected server error.
Update Equipment
Partially update an equipment record.
- Method: PUT
- Path:
/prod/equipment/equipment/{equipmentId} - Headers:
Authorization: Bearer <token> - Request Body:
Note: Only the fields present on the request change; omitted fields keep their stored values. An explicit{ "name": "Downstairs A/C", "notes": null }nullclears a nullable field.attributes, when present, replaces the stored map as a whole (see Concepts). Whennameis present it must be a non-empty string.customerIdis immutable and is ignored if sent;photos,createdAt, and other server-managed fields are never writable. - Success Response:
200 OK
Note:{ "equipment": { "companyId": "company-uuid", "equipmentId": "equipment-uuid", "customerId": "customer-uuid", "name": "Downstairs A/C", "brand": "Carrier", "model": "24ACC636A003", "serialNumber": "1234E56789", "installDate": "2021-06-14", "warrantyExpirationDate": "2031-06-14", "locationOnProperty": "Attic, north side", "notes": null, "address": null, "attributes": { "hvacType": "HEAT_PUMP", "tonnage": 3 }, "photos": [], "createdAt": "2026-08-23T12:00:00.000Z", "updatedAt": "2026-08-23T12:30:00.000Z" } }updatedAtis refreshed on every update. - Error Responses:
400 Bad Request- Missing body, or a field fails validation.401 Unauthorized- Missing or invalid token.404 Not Found- Equipment not found for this company.500 Internal Server Error- Unexpected server error.
Delete Equipment
Delete an equipment record and its stored photos.
- Method: DELETE
- Path:
/prod/equipment/equipment/{equipmentId} - Headers:
Authorization: Bearer <token> - Success Response:
200 OK
Note: The record's S3 photo objects are deleted best-effort — an S3 failure is logged server-side but never blocks the record deletion.{ "message": "Equipment deleted" } - Error Responses:
401 Unauthorized- Missing or invalid token.404 Not Found- Equipment not found for this company.500 Internal Server Error- Unexpected server error.
Generate Equipment Photo Upload URL
Mint a presigned S3 PUT URL for one equipment photo. Upload flow: call this endpoint, PUT the photo bytes to uploadUrl (with the same Content-Type and exact byte size declared here — the URL signs the Content-Length, so a different size fails), then record the photo via Add Equipment Photo Metadata.
- Method: POST
- Path:
/prod/equipment/equipment/{equipmentId}/photos/upload-url - Headers:
Authorization: Bearer <token> - Request Body:
Note:{ "contentType": "image/jpeg", "size": 123456 }contentTypemust be one ofimage/jpeg,image/png,image/heic,image/heif,image/webp.sizeis the exact upload size in bytes (positive integer, maximum 20971520 — 20 MB). The stored object key's extension is derived fromcontentType, never from a client file name. - Success Response:
200 OK
Note: The URL expires after 1 hour.{ "uploadUrl": "https://s3.presigned.example/put", "photoId": "photo-uuid", "s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg" } - Error Responses:
400 Bad Request- Invalid JSON, unsupportedcontentType, or missing/invalid/over-capsize.401 Unauthorized- Missing or invalid token.404 Not Found- Equipment not found for this company.500 Internal Server Error- Unexpected server error.
Add Equipment Photo Metadata
Record an uploaded photo on the equipment record. Call after the presigned PUT succeeds.
- Method: POST
- Path:
/prod/equipment/equipment/{equipmentId}/photos - Headers:
Authorization: Bearer <token> - Request Body:
Note:{ "photoId": "photo-uuid", "s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg", "fileName": "unit.jpg", "contentType": "image/jpeg" }photoIdands3Keyare required and must match the values minted by the upload-url endpoint for this equipment record — a key belonging to another company/equipment/photo is rejected.fileNameandcontentTypeare optional (defaults: a generated file name andimage/jpeg). The server verifies the object exists withHeadObjectand stores the size S3 reports, never a client-sent size; an object over the 20 MB cap is deleted from S3 and the request rejected. - Success Response:
200 OK{ "photo": { "photoId": "photo-uuid", "s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg", "fileName": "unit.jpg", "contentType": "image/jpeg", "size": 123456, "uploadedAt": "2026-08-23T12:00:00.000Z" } } - Error Responses:
400 Bad Request- Missing/invalidphotoIdors3Key, a key not minted for this equipment photo, unsupportedcontentType, the object was never uploaded, or the uploaded object exceeds the size cap (it is deleted).401 Unauthorized- Missing or invalid token.404 Not Found- Equipment not found for this company.500 Internal Server Error- Unexpected server error.
Get Equipment Photo Download URL
Mint a presigned GET URL for a stored equipment photo.
- Method: GET
- Path:
/prod/equipment/equipment/{equipmentId}/photos/{photoId} - Headers:
Authorization: Bearer <token> - Success Response:
200 OK
Note: The URL expires after 1 hour.{ "downloadUrl": "https://s3.presigned.example/get", "photoId": "photo-uuid", "fileName": "unit.jpg", "contentType": "image/jpeg" } - Error Responses:
401 Unauthorized- Missing or invalid token.404 Not Found- Equipment or photo not found for this company.500 Internal Server Error- Unexpected server error.
Delete Equipment Photo
Delete a photo from an equipment record.
- Method: DELETE
- Path:
/prod/equipment/equipment/{equipmentId}/photos/{photoId} - Headers:
Authorization: Bearer <token> - Success Response:
200 OK
Note: The S3 object is deleted best-effort (a failure is logged server-side); the photo entry is always removed from the record.{ "message": "Photo deleted" } - Error Responses:
401 Unauthorized- Missing or invalid token.404 Not Found- Equipment or photo not found for this company.500 Internal Server Error- Unexpected server error.