Tradesmen Technologies Tradesmen Technologies

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:

  • name is the only required client field besides customerId; it must be a non-empty string.
  • brand, model, serialNumber, locationOnProperty, and notes are optional strings (null when unset).
  • installDate and warrantyExpirationDate are optional date-only YYYY-MM-DD strings (null when unset) — never full timestamps.
  • address is an optional snapshot copy of the property address (the common Address shape); it is stored verbatim and never re-derived from the customer record. null when unset.
  • attributes is an optional flat map of extra specs defined by the client app (the HVAC app stores hvacType, 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, or null. Nested objects/arrays/booleans are rejected. Stored verbatim; null when unset. On update the map is replaced as a whole (never merged per key) — send the full map, null to clear, or omit it to keep the stored value. Records created before this field existed return it as absent/null.
  • photos, createdAt, updatedAt, companyId, and equipmentId are server-managed. photos is written only by the photo endpoints below — it is never writable via create/update.
  • customerId is immutable after creation; a customerId sent 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 (with brand, model, notes, attributes — see inventory-endpoints.md), and that item is the prefill source for a new customer-equipment record. The equipmentCatalog list, its bootstrap field, and the two /equipment/catalog endpoints 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 }
}
  • name is required (non-empty string; trimmed). brand, model, and notes are optional strings (trimmed; blank → null).
  • attributes is the same optional flat spec map as on equipment records (same validation, null when unset). Because PUT /equipment/catalog replaces the whole list, an item sent without attributes is stored with attributes: null.
  • catalogItemId is server-generated when an item is sent without one (or with null/""); 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, or null. fuelType — GAS | ELECTRIC | OIL | PROPANE, or null. Both use the vocabulary of the equipment attributes map.
  • 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); null for heating-only equipment. coolingBtu — certified cooling capacity in BTU/h.
  • seer2, hspf2, afue — the best certified rating among the model's combinations at its coolingBtu (the most common certified capacity; a lone outlier row never sets the size); null when not applicable. Ratings are SEER2 / HSPF2 (the 2023 test procedure), never legacy SEER.
  • refrigerant — R-410A | R-22 | R-32 | R-454B, or null when 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 — unless brand is given, in which case an empty q lists that brand's models.
    • type — optional; only models of this hvacType (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 (good matches Goodman). The form passes its Brand field.
    • limit — optional, positive integer, default 10, capped at 25.
  • Success Response: 200 OK
    {
      "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"
    }
    
    Note: model-prefix matches rank above contains matches; with a brand filter the matching brand is listed first in brands. models is [] when nothing matches; catalogBuiltAt is null while no catalog has been built yet. A failure to read the catalog also yields an empty models rather than an error, so the form the user is filling in keeps working.
  • Error Responses:
    • 400 Bad Request - q or brand longer than 100 characters, type not one of the listed values, or limit not 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 to limit).
    • type — optional; count and list only brands with models of this hvacType (same values as above).
    • limit — optional, positive integer, default 10, capped at 25.
  • Success Response: 200 OK
    {
      "brands": [
        { "brand": "Goodman", "models": 412 },
        { "brand": "Gree", "models": 96 }
      ],
      "catalogBuiltAt": "2026-09-20T07:00:00.000Z"
    }
    
    Note: models counts the brand's catalog models within the type filter.

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
    {
      "equipmentCatalog": [
        {
          "catalogItemId": "catalog-item-uuid",
          "name": "Carrier 3-ton A/C",
          "brand": "Carrier",
          "model": "24ACC636A003",
          "notes": "Filter size 16x25x1"
        }
      ]
    }
    
    Note: [] 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:
    {
      "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"
        }
      ]
    }
    
    Note: The whole list is replaced — send every item that should remain (an empty array clears the catalog). Items without a catalogItemId get a server-generated one. Validation rules are in Concepts.
  • Success Response: 200 OK
    {
      "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
        }
      ]
    }
    
    Note: The response is the stored, normalized list (ids assigned, strings trimmed, blank optionals as null).
  • Error Responses:
    • 400 Bad Request - Missing body, equipmentCatalog not an array, an item without a non-empty name, a non-string field, an invalid attributes map, a duplicated catalogItemId, or more than 200 items.
    • 401 Unauthorized - Missing or invalid token.
    • 403 Forbidden - Caller is not an ADMIN.
    • 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:
    {
      "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" }
    }
    
    Note: customerId and a non-empty name are required; every other field is optional (see Concepts for per-field validation, including the attributes map). The customer must exist for the authenticated company. photos starts 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/invalid customerId, missing/empty name, or a field fails validation (non-string string field, malformed date, non-object address, invalid attributes map).
    • 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
    {
      "equipment": []
    }
    
    Note: equipment is 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:
    {
      "name": "Downstairs A/C",
      "notes": null
    }
    
    Note: Only the fields present on the request change; omitted fields keep their stored values. An explicit null clears a nullable field. attributes, when present, replaces the stored map as a whole (see Concepts). When name is present it must be a non-empty string. customerId is immutable and is ignored if sent; photos, createdAt, and other server-managed fields are never writable.
  • Success Response: 200 OK
    {
      "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"
      }
    }
    
    Note: updatedAt is 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
    {
      "message": "Equipment deleted"
    }
    
    Note: The record's S3 photo objects are deleted best-effort — an S3 failure is logged server-side but never blocks the record deletion.
  • 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:
    {
      "contentType": "image/jpeg",
      "size": 123456
    }
    
    Note: contentType must be one of image/jpeg, image/png, image/heic, image/heif, image/webp. size is the exact upload size in bytes (positive integer, maximum 20971520 — 20 MB). The stored object key's extension is derived from contentType, never from a client file name.
  • Success Response: 200 OK
    {
      "uploadUrl": "https://s3.presigned.example/put",
      "photoId": "photo-uuid",
      "s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg"
    }
    
    Note: The URL expires after 1 hour.
  • Error Responses:
    • 400 Bad Request - Invalid JSON, unsupported contentType, or missing/invalid/over-cap size.
    • 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:
    {
      "photoId": "photo-uuid",
      "s3Key": "equipment/company-uuid/equipment-uuid/photo-uuid.jpg",
      "fileName": "unit.jpg",
      "contentType": "image/jpeg"
    }
    
    Note: photoId and s3Key are 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. fileName and contentType are optional (defaults: a generated file name and image/jpeg). The server verifies the object exists with HeadObject and 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/invalid photoId or s3Key, a key not minted for this equipment photo, unsupported contentType, 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
    {
      "downloadUrl": "https://s3.presigned.example/get",
      "photoId": "photo-uuid",
      "fileName": "unit.jpg",
      "contentType": "image/jpeg"
    }
    
    Note: The URL expires after 1 hour.
  • 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
    {
      "message": "Photo deleted"
    }
    
    Note: The S3 object is deleted best-effort (a failure is logged server-side); the photo entry is always removed from the record.
  • 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.