Tradesmen Technologies Tradesmen Technologies

API reference Services

Services Service API Endpoints

The production API base URL is https://api.tradesmentechnologies.com/.

Authentication

Most 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.

Base Path

/prod/services


Program settings on services

When a company uses maintenance programs (allowMaintenancePrograms), linear and 1D non-linear services carry the same optional program settings a program-template line has, so the price book is the single place to define how a service behaves inside a program. 2D non-linear services never carry them (they cannot be program lines). All keys are optional on create and update; sending null clears a value. Absent keys are returned as null.

{
  "programPrice": 45.0,
  "programPrices": { "1": 80, "2": 150 },
  "frequency": { "type": "QUARTERLY", "quarters": [2, 4] },
  "diyCost": 25.0,
  "diyTimeMinutes": 30,
  "variants": [
    { "variantId": "uuid", "label": "Standard" },
    { "variantId": "uuid2", "label": "Deep" }
  ]
}
  • programPrice (linear services only; non-negative number, per unit) — the discounted price charged on program visits. programPrices (1D non-linear only; {quantity: total} like prices, positive-integer keys, non-negative totals; an empty object clears it) is its tiered counterpart. When absent, template lines fall back to the catalog price.
  • frequency — the same calendar object as a program line (MONTHLY, or QUARTERLY with a non-empty quarters subset of 1–4, deduplicated and sorted; quarters is rejected on MONTHLY). null/absent behaves as MONTHLY when copied onto lines.
  • diyCost (non-negative number) / diyTimeMinutes (non-negative integer) — display-only DIY comparison values, same unit semantics as the prices.
  • variants — label-only service options ({variantId?, label}); the server generates variantId when omitted and preserves sent ids; a legacy price is dropped; an empty array clears the options.

Propagation to program templates. PUT /services/services/{serviceId} and PUT /services/services/non-linear/{serviceId} rewrite every program template line whose serviceId (and matching serviceType) references the saved service: name, unit, oneTimePrice (catalog price — non-linear: the tier for the line's quantity, highest tier as fallback), programPrice (the service's program price, or the catalog price when none), the non-linear line's prices/programPrices tier-table snapshots (copied whole; a cleared service table removes the line's copy), frequency, diyCost/diyTimeMinutes, and variants are all copied from the service; the line's lineId and quantity are kept. The response carries service (the full stored row) and updatedTemplates (the full rewritten template records — clients should replace their local copies). The sync runs after the service itself is saved and is best-effort: if it fails, the response is still 200 with updatedTemplates: [] and templateSyncFailed: true. Customer programs are never touched — their lines stay grandfathered snapshots (see programs-endpoints.md).

Inventory items on services

Linear services and 1D non-linear services may carry inventoryItems — the parts consumed per unit of the service, used to derive a job's stock deduction from its invoice/estimate lines (see inventory-endpoints.md, "Job deductions"). 2D non-linear services never carry the field (it is ignored on their create/update).

{
  "inventoryItems": [
    { "itemId": "item-uuid", "quantity": 1 },
    { "itemId": "wire-item-uuid", "quantity": 2.5 }
  ]
}
  • Each entry needs a non-empty itemId and a finite quantity > 0; at most 50 entries; ids unique. Ids are not validated against the inventory table — an id that no longer exists simply derives nothing.
  • Create: omitted/null/[] stores null. Update: omitted keeps the stored list; null or [] clears it.
  • Program-template sync is not affected by this field.

Endpoints

Create Services

Create one or more services for the company.

  • Method: POST
  • Path: /prod/services/services
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "services": [
        {
          "name": "Service Name",
          "unit": "sqft",
          "price": 5.0,
          "cost": 3.0,
          "description": "Optional description",
          "parentCategoryId": "uuid",
          "allowInitials": true,
          "displayOrder": 1
        }
      ]
    }
    
    Note: The services field is required and must be a non-empty array. Each service object requires name and price. The following fields are optional per service: unit (defaults to null — a free-form unit label such as sqft or hour), description (defaults to null), cost (defaults to null — tracks the business's cost for accounting purposes), parentCategoryId (defaults to null), allowInitials (defaults to false — indicates whether customer initials can be captured for this service on documents), displayOrder (defaults to null — an integer used only to determine the order in which the service is shown in the front end; must be an integer when provided). All services are validated before any are created — if validation fails for any service, none are created. Validation errors include the index of the offending service. Each service also accepts the optional program settings described under "Program settings on services" above (programPrice, frequency, diyCost, diyTimeMinutes, variants); all default to null. Each service also accepts the optional inventoryItems list described under "Inventory items on services" (defaults to null).
  • Success Response: 201 Created
    {
      "message": "Services created successfully",
      "serviceIds": ["uuid1", "uuid2"]
    }
    

Delete Service

  • Method: DELETE
  • Path: /prod/services/services/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Success Response: 200 OK
    { "message": "Service deleted successfully" }
    

Update Service

  • Method: PUT
  • Path: /prod/services/services/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Request Body: (All fields are optional)
    {
      "name": "Updated Service Name",
      "unit": "sqft",
      "price": 6.0,
      "cost": 3.5,
      "description": "Updated description",
      "parentCategoryId": "uuid",
      "allowInitials": true,
      "displayOrder": 1
    }
    
    Note: displayOrder must be an integer when provided, and may be set to null to clear it. Also accepts the optional program settings (programPrice, frequency, diyCost, diyTimeMinutes, variants — see "Program settings on services"); each may be set to null to clear it. Also accepts inventoryItems (see "Inventory items on services"): omitted keeps, null/[] clears.
  • Success Response: 200 OK
    {
      "message": "Service updated successfully",
      "service": { "companyId": "uuid", "serviceId": "uuid", "name": "Updated Service Name", "price": 6.0, "programPrice": 4.5 },
      "updatedTemplates": []
    }
    
    service is the full stored row after the update; updatedTemplates lists every program template whose lines were rewritten from this service (see "Program settings on services"). templateSyncFailed: true is added when that propagation failed.
  • Error Responses: 400 Bad Request — missing body, no recognized field in the body, or a field fails validation. 404 Not Found — no service with that id in the company.

Convert Service to Non-Linear

  • Method: POST
  • Path: /prod/services/services/{serviceId}/convert-to-non-linear
  • Headers: Authorization: Bearer <token>
  • Request Body: optional; every field is optional
    {
      "prices": { "1": 110, "5": 450 },
      "costs": { "1": 55 },
      "programPrices": { "1": 90, "5": 380 }
    }
    
    Turns a regular (linear) service into a non-linear service with the same serviceId: the row leaves the services list and appears in the non-linear services list of the session bootstrap. name, unit, description, parentCategoryId, allowInitials, displayOrder, the program settings (frequency, diyCost, diyTimeMinutes, variants), inventoryItems and createdAt are carried over; convertedFromLinearAt records the conversion. The tier tables ({ quantity: amount }, same rules as Create Non-Linear Service) come from the body; each one omitted is seeded from the linear amount as the quantity-1 tier: prices from price ({} when the service has no price), costs from cost, programPrices from programPrice (null when the linear field is absent). A null costs / programPrices clears it. The move is one transaction, so the service is never in both lists. Program template lines that reference the service as LINEAR are re-typed to NON_LINEAR_1D and re-priced from the new tier tables (snapshots included, as on Update Non-Linear Service); customer programs are never touched. Existing estimates and invoices keep their line-item snapshots.
  • Success Response: 200 OK
    {
      "message": "Service converted to non-linear successfully",
      "service": { "companyId": "uuid", "serviceId": "uuid", "name": "Lawn Mowing", "pricingModel": "non_linear_1d", "prices": { "1": 110 }, "costs": { "1": 55 }, "programPrices": null, "convertedFromLinearAt": "2026-10-14T12:00:00.000Z" },
      "updatedTemplates": []
    }
    
    service is the full stored non-linear row; updatedTemplates / templateSyncFailed as on Update Service.
  • Error Responses:
    • 400 Bad Request — missing serviceId, a body that is not a JSON object, or an invalid tier table.
    • 404 Not Found — no linear service with that id in the company.
    • 409 Conflict — the service was deleted or converted by a concurrent request; reload and retry.

Bulk Update Display Order

Update the displayOrder of many items at once across all four service item types (services, categories, non-linear services, and 2D non-linear services). Intended for reordering: send the full set of items whose order changed in a single request.

  • Method: PUT
  • Path: /prod/services/display-order
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "items": [
        { "type": "service", "id": "uuid", "displayOrder": 0 },
        { "type": "category", "id": "uuid", "displayOrder": 1 },
        { "type": "non-linear", "id": "uuid", "displayOrder": 2 },
        { "type": "2d-non-linear", "id": "uuid", "displayOrder": 3 }
      ]
    }
    
    Note: items is required and must be a non-empty array. For each item, type must be one of service, category, non-linear, 2d-non-linear; id is the item's serviceId (or categoryId for categories); and displayOrder is required and must be an integer. All items are validated before any are written — if any item is structurally invalid, the request is rejected with 400 and nothing is updated. Updates are then applied on a best-effort basis: each item is updated independently, so an item that does not exist (or belongs to another company) is reported as updated: false with reason: "not found" (an unexpected write failure as reason: "error") while the remaining items still succeed.
  • Success Response: 200 OK
    {
      "message": "Display order updated",
      "updatedCount": 3,
      "results": [
        { "id": "uuid", "type": "service", "updated": true },
        { "id": "uuid", "type": "category", "updated": false, "reason": "not found" }
      ]
    }
    

Create Category

  • Method: POST
  • Path: /prod/services/categories
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "name": "Category Name",
      "parentCategoryId": "uuid",
      "displayOrder": 1
    }
    
    Note: displayOrder is optional (defaults to null — an integer used only to determine the order in which the category is shown in the front end; must be an integer when provided).
  • Success Response: 201 Created
    {
      "message": "Category created successfully",
      "categoryId": "uuid"
    }
    

Delete Category

  • Method: DELETE
  • Path: /prod/services/categories/{categoryId}
  • Headers: Authorization: Bearer <token>
  • Success Response: 200 OK
    { "message": "Category and its contents deleted successfully" }
    

Update Category

  • Method: PUT
  • Path: /prod/services/categories/{categoryId}
  • Headers: Authorization: Bearer <token>
  • Request Body: (All fields are optional)
    {
      "name": "Updated Category Name",
      "parentCategoryId": "uuid",
      "displayOrder": 1
    }
    
    Note: displayOrder must be an integer when provided, and may be set to null to clear it. An empty body (no recognized field) is a 400.
  • Success Response: 200 OK
    { "message": "Category updated successfully" }
    
  • Error Responses: 400 Bad Request — validation. 404 Not Found — no category with that id in the company.

Create Non-Linear Service

  • Method: POST
  • Path: /prod/services/services/non-linear
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "name": "Service Name",
      "unit": "vents",
      "prices": {
        "1": 100,
        "5": 400
      },
      "costs": {
        "1": 50,
        "5": 200
      },
      "description": "Description",
      "parentCategoryId": "uuid",
      "allowInitials": true,
      "displayOrder": 1
    }
    
    Note: name and prices are required: prices is an object of { quantity: price } whose keys are non-negative numbers and whose values are finite numbers. unit, description, costs, parentCategoryId, allowInitials, and displayOrder are optional (unit/description default to null and are ignored unless strings). The optional program settings (programPrices, frequency, diyCost, diyTimeMinutes, variants — see "Program settings on services") default to null, as does the optional inventoryItems list (see "Inventory items on services"). costs is the tiered counterpart of a linear service's cost — an object of { quantity: cost } tracking the business's cost for accounting purposes, stored as sent (not validated); defaults to null. A top-level cost is ignored on non-linear services. allowInitials defaults to false. displayOrder defaults to null — an integer used only to determine the order in which the service is shown in the front end; must be an integer when provided.
  • Success Response: 201 Created
    { "message": "Non-linear service created successfully", "serviceId": "uuid" }
    

Delete Non-Linear Service

  • Method: DELETE
  • Path: /prod/services/services/non-linear/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Success Response: 200 OK
    { "message": "Non-linear service deleted successfully" }
    

Update Non-Linear Service

  • Method: PUT
  • Path: /prod/services/services/non-linear/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Request Body: (All fields are optional)
    {
      "name": "Updated Service Name",
      "unit": "vents",
      "prices": {
        "1": 110,
        "5": 450
      },
      "costs": {
        "1": 55,
        "5": 220
      },
      "description": "Updated description",
      "parentCategoryId": "uuid",
      "allowInitials": true,
      "displayOrder": 1
    }
    
    Note: displayOrder must be an integer when provided, and may be set to null to clear it. prices, when present, follows the Create rules; costs is the { quantity: cost } map (stored as sent). Also accepts the optional program settings (programPrices, frequency, diyCost, diyTimeMinutes, variants — see "Program settings on services"); each may be set to null to clear it. Also accepts inventoryItems (see "Inventory items on services"): omitted keeps, null/[] clears.
  • Success Response: 200 OK
    {
      "message": "Non-linear service updated successfully",
      "service": { "companyId": "uuid", "serviceId": "uuid", "name": "Updated Service Name", "pricingModel": "non_linear_1d", "prices": { "1": 110, "5": 450 }, "programPrices": { "1": 90, "5": 380 } },
      "updatedTemplates": []
    }
    
    Same response semantics as Update Service: service is the full stored row, updatedTemplates the rewritten program templates, templateSyncFailed present only when propagation failed.
  • Error Responses: 400 Bad Request — missing body, no recognized field, or a field fails validation. 404 Not Found — no non-linear service with that id in the company.

Create 2D Non-Linear Service

  • Method: POST
  • Path: /prod/services/services/2d-non-linear
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "name": "Service Name",
      "dimXName": "Width",
      "dimYName": "Height",
      "prices": {
        "10": { "10": 100, "20": 150 },
        "20": { "10": 180, "20": 250 }
      },
      "cost": 75.0,
      "description": "Description",
      "parentCategoryId": "uuid",
      "allowInitials": true,
      "displayOrder": 1
    }
    
    Note: description, cost, parentCategoryId, allowInitials, and displayOrder are optional. cost tracks the business's cost for accounting purposes. allowInitials defaults to false. displayOrder defaults to null — an integer used only to determine the order in which the service is shown in the front end; must be an integer when provided.
  • Success Response: 201 Created
    { "message": "2D non-linear service created successfully", "serviceId": "uuid" }
    

Delete 2D Non-Linear Service

  • Method: DELETE
  • Path: /prod/services/services/2d-non-linear/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Success Response: 200 OK
    { "message": "2D non-linear service deleted successfully" }
    

Update 2D Non-Linear Service

  • Method: PUT
  • Path: /prod/services/services/2d-non-linear/{serviceId}
  • Headers: Authorization: Bearer <token>
  • Request Body: (All fields are optional)
    {
      "name": "Updated Service Name",
      "dimXName": "Updated Width",
      "dimYName": "Updated Height",
      "prices": {
        "10": { "10": 110, "20": 160 },
        "20": { "10": 190, "20": 260 }
      },
      "cost": 80.0,
      "description": "Updated description",
      "parentCategoryId": "uuid",
      "allowInitials": true,
      "displayOrder": 1
    }
    
    Note: displayOrder must be an integer when provided, and may be set to null to clear it. An empty body (no recognized field) is a 400.
  • Success Response: 200 OK
    { "message": "2D non-linear service updated successfully" }
    
  • Error Responses: 400 Bad Request — validation. 404 Not Found — no 2D non-linear service with that id in the company.