Tradesmen Technologies Tradesmen Technologies

API reference Accounts

Accounts 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>

When token authentication fails, every authenticated endpoint returns 401 Unauthorized with a body of this shape:

{
  "message": "Unauthorized: Token revoked",
  "reason": "SUPERSEDED"
}

The reason field is machine-readable and lets clients explain to the user why their session ended:

  • MISSING_TOKEN — no Authorization header, or it is not a Bearer token.
  • INVALID_TOKEN — the token is not recognized (never issued, or its record has already been cleaned up).
  • SUPERSEDED — the token was revoked because the same employee logged in again (e.g. on another device). Only one active session is allowed per employee.
  • EXPIRED — the token's lifetime elapsed.
  • PASSWORD_CHANGED — the token was revoked because the employee changed their password from another session (Change Password below). Added after SUPERSEDED/EXPIRED; older clients treat it as unrecognized.

If a token is both revoked and past its expiry, the reason reflects whichever happened first. The reason field was added after the initial release; clients should treat it as optional and fall back to a generic "session expired" handling when it is absent or unrecognized.

Idempotent Retries (Idempotency-Key)

Every mutating request (POST, PUT, PATCH, DELETE) on every endpoint may carry an Idempotency-Key header: a client-generated unique string of 16–128 characters from A-Z, a-z, 0-9, _, ., : and - (a UUID is ideal). The server remembers the first answer it gave for a key for 30 days and, for any later request with the same key, returns that answer again (same status and body, plus an Idempotent-Replay: true header) without acting a second time. This makes a retry safe when the first attempt may have reached the server but its answer was lost — a timeout, a dropped connection, a device that went offline and replays its queue later. Use a fresh key for every logical request and the same key for every attempt of it.

Rules:

  • The header is optional; a request without it behaves as before. A malformed key is ignored, not rejected.
  • Only answers that decided the request are remembered: every 2xx, 3xx and 4xx except 401, 403, 408 and 429. A 5xx, a 401/403 (the session was wrong, not the request) and 408/429 forget the key, so the next attempt runs for real.
  • While the first attempt is still running, a second one with the same key gets 503 with Retry-After: 5 and {"message": "...", "code": "IDEMPOTENT_REQUEST_IN_PROGRESS"} — retry it shortly.
  • A key reused for a different method or path is treated as a new request.
  • A stored answer larger than 300 KB is replayed as its original status with {"message": "Request already processed", "idempotentReplay": true} instead of the original body.

The timesheet endpoints additionally accept their own in-body keys (timesheetId on create, idempotencyKey on edit) from before this header existed; both mechanisms work together.

Base Path

/prod/accounts


Enums

TradeType (used for jobType)

  • APPLIANCE_REPAIR
  • AUTO_REPAIR
  • CARPENTRY
  • CARPET_CLEANING
  • CONCRETE
  • DRYWALL
  • ELECTRICAL
  • ESTHETICIAN
  • FENCING
  • FLOORING
  • GARAGE_DOOR
  • GENERAL_CONTRACTING
  • GLASS_AND_GLAZING
  • GUTTERS
  • HANDYMAN
  • HVAC
  • INSULATION
  • JUNK_REMOVAL
  • LANDSCAPING
  • LOCKSMITH
  • MASONRY
  • MOVING
  • PAINTING
  • PEST_CONTROL
  • PET_GROOMING
  • PLASTERING
  • PLUMBING
  • POOL_AND_SPA
  • POWER_WASHING
  • ROOFING
  • SEPTIC
  • SIDING
  • SOLAR
  • TREE_SERVICE
  • WELDING
  • WINDOW_AND_DOOR
  • WINDOW_CLEANING
  • OTHER

CompanyStatus

  • OPERATIONAL
  • DISABLED
  • UNPAID

EmployeeType

  • ADMIN
  • LEAD_TECHNICIAN
  • TECHNICIAN
  • CREW_TECHNICIAN — a TECHNICIAN who may also clock other employees in and out (crew clock-in/out, GET /timesheets/open); for every other permission it is treated exactly as TECHNICIAN (since 2026-10-22).

Endpoints

Create Company

Create a new company and its first admin employee.

  • Method: POST

  • Path: /prod/accounts/companies

  • Request Body:

    {
      "name": "Company Name",
      "companyEmail": "email@company.com",
      "companyPhoneNumber": "1234567890",
      "address": {
        "streetAddress": "123 Street",
        "city": "City",
        "state": "State",
        "zipCode": "12345"
      },
      "timeZone": "America/Chicago",
      "firstName": "John",
      "middleName": "D",
      "lastName": "Doe",
      "employeeEmail": "john@company.com",
      "employeePhoneNumber": "0987654321",
      "employeeType": "ADMIN",
      "password": "securepassword123",
      "brand": "thermacrm"
    }
    
  • Success Response: 201 Created

    {
      "message": "Company and Employee created successfully",
      "companyId": "uuid",
      "employeeId": "uuid",
      "freeTrialEndDate": "2026-09-25"
    }
    

    Note: employeePhoneNumber must be a valid US phone number (10 digits, or 11 digits starting with 1). It is normalized to E.164 (e.g. +10987654321) before storage. companyPhoneNumber is stored as provided. timeZone is optional (an IANA time zone string, stored verbatim and returned by Login as companyTimeZone); when omitted the company starts on America/New_York. companyEmail and employeeEmail are trimmed and lowercased before the uniqueness check and storage; every other string field is trimmed.

    brand is optional: the app flavor the company signed up through — "tradesmen" (Tradesmen Tech) or "thermacrm" (ThermaCRM). It is stored on the company record as brand and names the app in the internal sign-up notification email. Absent or unrecognized values resolve to "tradesmen", so app versions that never send it are unaffected. The stored brand decides one more thing: brands without a free tier start on an automatic free trial — a "thermacrm" sign-up gets a 14-day trial (freeTrialEndDate = the sign-up's UTC date + 14 days; the login response's subscription.freeTrialEndDate reflects it from the first login), while "tradesmen" gets none (its free tier applies). The response echoes the granted freeTrialEndDate (null when no trial was granted). The stored brand also picks the sender address of every email sent for the company: "thermacrm" companies' email (login codes, password resets, documents, reports, reminders, campaigns, receipts, notices) comes from noreply@thermacrm.com, everything else from noreply@tradesmentechnologies.com; a company created before brand existed has none and sends from the Tradesmen address. Nothing else on the backend reads the brand — the app build, not the stored brand, decides the user-facing behavior.

  • Error Responses:

    • 400 Bad Request: Missing body, password missing/too short, or invalid employeePhoneNumber.
    • 409 Conflict: Email already in use by an existing company or employee.
    • 500 Internal Server Error: Unexpected server error.

Create Employee

Create a new employee within the authenticated admin's company.

  • Method: POST

  • Path: /prod/accounts/employees

  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.

  • Request Body:

    {
      "firstName": "Jane",
      "middleName": "A",
      "lastName": "Smith",
      "email": "jane@company.com",
      "phoneNumber": "5551234567",
      "employeeType": "LEAD_TECHNICIAN",
      "password": "securepassword123",
      "hasAppAccess": true,
      "hourlyRate": 2550,
      "clockInTypeRates": {
        "clock-in-type-uuid-work": 2550,
        "clock-in-type-uuid-travel": 1200
      }
    }
    

    Note: firstName is the only field every employee needs (since 2026-10-22; lastName and phoneNumber used to be required too). middleName, lastName and phoneNumber are optional strings — a blank or omitted value is stored as null, and names are trimmed. When phoneNumber is non-blank it must be a valid US phone number (10 digits, or 11 digits starting with 1) and is normalized to E.164 (e.g. +15551234567) before storage. When provided, employeeType must be one of: ADMIN, LEAD_TECHNICIAN, TECHNICIAN, CREW_TECHNICIAN.

    hasAppAccess is optional and defaults to true. When true, the employee can log in and counts as a billable per-seat user, so email and password are required as well (password must be at least 8 characters); employeeType defaults to TECHNICIAN when omitted. When false, the employee is a no-access record used only for assignment/tracking: they cannot log in and are excluded from per-seat billing, so only firstName is required — email, password, and employeeType (role) are all optional (any password sent is ignored, and the role is omitted from the record when not provided). Email uniqueness is still enforced when an email is provided.

    hourlyRate is optional for all employees (with or without app access) and is the employee's pay rate as a non-negative integer number of cents per hour (e.g. 2550 = $25.50/hr). When omitted, the employee has no rate on record.

    clockInTypeRates is optional: the employee's rate per clock-in type, an object mapping a clockInTypeId to a non-negative integer number of cents per hour (a null value leaves that type without a rate; ids are not checked against the company's list). Omitted or null = no per-type rates. How an hour of each type is paid from these two fields is described under clock-in types; the app keeps hourlyRate equal to the default type's rate.

    Welcome email: after an employee with app access is created, the server emails the new employee at the login email (subject Your {app} account is ready, sent from the company's brand address — see brand under Create Company): it names the company and the app, tells them to sign in with that email address, and that their administrator set their password (with "Forgot password?" on the sign-in screen as the way to choose their own), with a prominent button to the brand's website. The email never contains the password — the administrator hands it over. It is best-effort and server-side only: a delivery problem is logged and does not change the response, no email is sent for a no-access employee (hasAppAccess: false), and @example.com addresses are never emailed.

  • Success Response: 201 Created

    {
      "message": "Employee created successfully",
      "employeeId": "uuid"
    }
    
  • Error Responses:

    • 400 Bad Request: Missing body, missing or invalid required fields, invalid email format, invalid employeeType, invalid phone number, password too short, non-boolean hasAppAccess, invalid hourlyRate (must be a non-negative integer), or invalid clockInTypeRates (must be an object of non-negative integer cents per clock-in type id).
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 409 Conflict: Email already in use by an existing company or employee.
    • 500 Internal Server Error: Unexpected server error.

Login

Authenticate an employee and receive a token and company data.

  • Method: POST
  • Path: /prod/accounts/login
  • Request Body:
    {
      "employeeEmail": "john@company.com",
      "password": "securepassword123",
      "mfaDeviceToken": "base64url_device_token"
    }
    
    mfaDeviceToken is optional: it is the device-trust token returned by the Verify MFA Code endpoint after a previous successful code verification on this device. When it is present, valid, and within its 30-day trust window, the login completes directly; otherwise the endpoint responds with an MFA challenge (see below).
  • MFA challenge response: 200 OK. When MFA applies and the request does not carry a valid mfaDeviceToken, the credentials are still verified, but instead of a session the endpoint emails a 6-digit code to the employee's login email and returns:
    {
      "mfaRequired": true,
      "mfaToken": "base64url_challenge_token",
      "mfaExpiresAt": 1770973140,
      "deliveryMethod": "EMAIL",
      "emailMasked": "j***@company.com",
      "phoneNumberMasked": "***-***-7890"
    }
    
    The client must then collect the code and call Verify MFA Code with mfaToken and the code to complete the login. mfaExpiresAt (epoch seconds) is when the challenge and code expire (10 minutes). phoneNumberMasked is present only when the phone number on file can receive SMS — it signals that the client may offer "Text me the code instead" via the Resend MFA Code endpoint (the escape hatch for a user who lost email access but still has their phone); when absent, do not offer the SMS option. Employees with multiple devices can hold a valid device-trust token on each; verifying a new device does not invalidate other devices' tokens.
  • Note: Only one active session is allowed per employee. Logging in revokes any existing tokens for the employee, so a previous session on another device will stop working; subsequent requests from that device receive 401 with reason: "SUPERSEDED" (see Authentication above). The session token expires 12 hours after it is issued (expiresAt, epoch seconds); after that every authenticated call answers 401 with reason: "EXPIRED" and the employee must log in again. For non-ADMIN employees (LEAD_TECHNICIAN, TECHNICIAN, CREW_TECHNICIAN), the jobs array only contains jobs assigned to them (where their employeeId is in the job's employeeIds). ADMIN employees receive all jobs for the company. The documents array contains every estimate, invoice and custom document of the company — attached to a job or standalone — in the item shape of GET /jobs/documents (see "Get All Documents" and "Standalone Documents" in jobs-endpoints.md); ADMIN employees receive all of them, other employees the documents of the jobs in their jobs array plus the standalone documents they created. It is [] when the documents could not be read (the login still succeeds; clients refetch via GET /jobs/documents), and absent from responses of backend versions before 2026-10-02. The merchantOnboardingStatus field is null if the company has not started merchant onboarding, or one of: PENDING, IN_REVIEW, APPROVED, REJECTED. The talusPayPublicKey field is only present when merchantOnboardingStatus is APPROVED. The stripeAccount field is the company's Stripe "Pay over time" account (the account object in stripe-endpoints.md: status, payLaterAvailable, payLaterMethods, payLaterCapabilities, requirementsDue, approvedAt, updatedAt) or null when the company never started Pay over time onboarding. The reviewLinks field is null if the company has not configured review links, or an object containing the configured links (e.g., googleLink). The logoUrl field is null if the company has not uploaded a logo. The companyTimeZone field is null if the company has not set a time zone, or a valid IANA time zone string (e.g., "America/New_York"). The customFields array contains the company's custom field definitions for jobs (each with fieldId, entityType: "JOB", name, type, and createdAt). The customerCustomFields array contains the company's custom field definitions for customers, with the same shape but entityType: "CUSTOMER". The customTexts array contains the company's saved free-form text snippets for documents (each with customTextId, name, text, documentType of ESTIMATE or INVOICE, and createdAt); the front end uses these to populate the customText field when creating or updating an estimate or invoice. The customDocumentTemplates array contains the company's saved templates for CUSTOM_DOCUMENT documents (each with templateId, title, text, requireSignature, and createdAt); the front end clones these into a new document via the Create Document endpoint with type: "CUSTOM_DOCUMENT". The subscription object contains the company's SaaS subscription details: status is one of NEVER_SUBSCRIBED, ACTIVE, PAYMENT_FAILED, CANCELLED; tradesmenTechPublicKey is the TalusPay public key for tokenizing SaaS payment methods (distinct from talusPayPublicKey which is for collecting customer payments); freeTrialEndDate is the trial end date (YYYY-MM-DD) or null; hasPaymentMethod indicates whether the company has a card on file; paymentMethodLast4 is the last 4 digits of the card/bank account on file or null if unknown (e.g. subscriptions created before this field existed); billingDayOfMonth is the day of month for recurring billing or null; maxEmployees is the subscribed employee limit or null; lastPaymentDate and lastPaymentAmount (in cents) reflect the most recent charge or null; cancelAtPeriodEnd (boolean) and cancelledAt (timestamp or null) describe a pending cancellation; pricing contains basePrice and additionalUserPrice (in cents) and isCustomPricing (whether the company has a custom rate). A CANCELLED subscription is reported as ACTIVE while a free trial is still running (today is before freeTrialEndDate), so the company keeps access until the trial ends. The additionalCharges array contains the company's configured document charges (each with id, type of TAX/CONVENIENCE/CUSTOM, label, and percentage); these are applied as a percentage of an invoice's line-items total (before deposit) when creating an invoice. The tipPercentages array contains the company's configured tip options (e.g. [10, 15, 20]) that the customer can choose from when paying an invoice on the doc viewer. Both default to []. The programTemplates and customerPrograms arrays contain the company's maintenance-program templates and per-client programs (shapes in programs-endpoints.md); programSavingsThresholdPercent is the company's display threshold for the program DIY-savings line (percent, defaults to 50 when never set); programVisitHorizonDays is the rolling number of days of maintenance-program visit jobs kept materialized per ACTIVE program (integer, defaults to 90 when never set — see programs-endpoints.md). The customerEquipment array contains all of the company's equipment records — equipment left at customers' properties (record shape in equipment-endpoints.md). The customerSubscriptions array contains every recurring charge the company bills its customers (shape in customer-subscriptions-endpoints.md). The clockInTypes array is the company's list of clock-in types with their pay rates (shape in timesheets-endpoints.md, "Clock-in types"); [] when the company uses none. The equipmentCatalog array is the company's list of often-installed equipment (each {catalogItemId, name, brand, model, notes, attributes}; see "Equipment Catalog" in equipment-endpoints.md), used by the app to prefill a new equipment record; [] when never configured. The inventoryItems array is every inventory item of the company (item shape in inventory-endpoints.md), inventoryTrucks is the company's truck list and inventoryCategories its item-category list (each {categoryId, name}; both [] when never configured) — the same three lists GET /inventory returns. Each entry in the customers array includes a server-generated createdAt timestamp (UTC ISO 8601) recording when the customer record was created; customers created before this field existed may lack it, so consumers should treat it as optional. Each entry in the employees array includes a hasAppAccess boolean (defaults to true for employees created before this field existed); no-access employees (false) are listed for assignment/tracking but cannot log in. Each entry also includes an hourlyRate field (pay rate in cents per hour, or null when no rate is set) and a clockInTypeRates object (the employee's cents-per-hour rate per clock-in type, {} when none are set — see timesheets-endpoints.md); both are compensation data, so ADMIN callers see every employee's values while non-ADMIN callers see only their own (null for everyone else). A login attempt by an employee with hasAppAccess: false returns 403 Forbidden.
  • Success Response: 200 OK
    {
      "token": "base64url_token",
      "tokenType": "Bearer",
      "expiresAt": 1770973140,
      "employeeId": "uuid",
      "companyId": "uuid",
      "companyName": "Company Name",
      "companyEmail": "email@company.com",
      "companyPhoneNumber": "1234567890",
      "companyAddress": {
        "streetAddress": "123 Street",
        "city": "City",
        "state": "State",
        "zipCode": "12345"
      },
      "logoUrl": "https://s3.amazonaws.com/bucket/company-logo.png",
      "companyTimeZone": "America/New_York",
      "firstName": "John",
      "middleName": "D",
      "lastName": "Doe",
      "email": "john@company.com",
      "phoneNumber": "0987654321",
      "employeeType": "ADMIN",
      "maxEmployees": 1,
      "additionalCharges": [
        { "id": "uuid", "type": "TAX", "label": "Sales Tax", "percentage": 8.25, "appliesTo": "INVOICE" },
        { "id": "uuid", "type": "CONVENIENCE", "label": "Card Processing Fee", "percentage": 3, "appliesTo": "PAYMENT", "waivedForAch": true }
      ],
      "tipPercentages": [10, 15, 20],
      "categories": [
        {
          "companyId": "uuid",
          "categoryId": "uuid",
          "name": "Category Name",
          "parentCategoryId": null,
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "services": [
        {
          "companyId": "uuid",
          "serviceId": "uuid",
          "name": "Service Name",
          "description": "Optional description",
          "unit": "sqft",
          "price": 5.0,
          "cost": 3.0,
          "parentCategoryId": "uuid",
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "nonLinearServices": [
        {
          "companyId": "uuid",
          "serviceId": "uuid",
          "name": "Non-Linear Service Name",
          "description": "Optional description",
          "cost": 50.0,
          "parentCategoryId": "uuid",
          "pricingModel": "non_linear_1d",
          "prices": {
            "1": 100,
            "5": 400
          },
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "twoDNonLinearServices": [
        {
          "companyId": "uuid",
          "serviceId": "uuid",
          "name": "2D Non-Linear Service Name",
          "description": "Optional description",
          "cost": 75.0,
          "parentCategoryId": "uuid",
          "pricingModel": "non_linear_2d",
          "dimXName": "Width",
          "dimYName": "Height",
          "prices": {
            "10": { "10": 100, "20": 150 },
            "20": { "10": 180, "20": 250 }
          },
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "jobs": [
        {
          "companyId": "uuid",
          "jobId": "uuid",
          "displayId": 1,
          "customerId": "uuid",
          "jobType": "MOVING",
          "description": "Sample job description",
          "status": "SCHEDULED",
          "startDateTime": "2024-01-01T10:00:00.000Z",
          "endDateTime": "2024-01-01T14:00:00.000Z",
          "createdAt": "2024-01-01T00:00:00.000Z",
          "photos": [
            {
              "photoId": "uuid",
              "fileName": "site-photo.jpg",
              "contentType": "image/jpeg",
              "size": 12345,
              "s3Key": "companyId/jobId/uuid.jpg"
            }
          ]
        }
      ],
      "documents": [
        {
          "companyId": "uuid",
          "documentId": "uuid",
          "documentCode": "secure-code",
          "displayId": "1",
          "jobId": "uuid",
          "type": "ESTIMATE",
          "status": "SENT",
          "hours": 0,
          "depositPrice": 0,
          "requireSignature": true,
          "services": [],
          "oneDPrices": [],
          "twoDPrices": [],
          "createdBy": "employee-uuid",
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customers": [
        {
          "companyId": "uuid",
          "customerId": "uuid",
          "firstName": "Jane",
          "lastName": "Smith",
          "email": "jane@example.com",
          "phoneNumber": "1112223333",
          "additionalEmails": ["extra@example.com"],
          "address": {
            "streetAddress": "456 Avenue",
            "city": "City",
            "state": "State",
            "zipCode": "12345"
          },
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "employees": [
        {
          "companyId": "uuid",
          "employeeId": "uuid",
          "firstName": "John",
          "middleName": "D",
          "lastName": "Doe",
          "email": "john@company.com",
          "phoneNumber": "0987654321",
          "employeeType": "ADMIN",
          "hasAppAccess": true,
          "hourlyRate": 2550,
          "clockInTypeRates": {
            "clock-in-type-uuid-work": 2550,
            "clock-in-type-uuid-travel": 1200
          },
          "description": "Expert plumber with 10 years of experience.",
          "photoUrl": "https://s3.amazonaws.com/bucket/employee-photos/companyId/employeeId/photo.png"
        }
      ],
      "webhooks": [
        {
          "code": "ABCDEFghijklmnopQRSTUVwxyz012345",
          "source": "Zapier"
        }
      ],
      "merchantOnboardingStatus": "APPROVED",
      "talusPayPublicKey": "pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "stripeAccount": null,
      "flags": {
        "newJobFeature": true,
        "betaTesting": false
      },
      "reviewLinks": {
        "googleLink": "https://g.page/r/your-business/review",
        "otherLink": "https://yelp.com/biz/your-business",
        "updatedAt": "2026-01-01T00:00:00.000Z"
      },
      "pipelines": [
        {
          "companyId": "uuid",
          "pipelineId": "uuid",
          "name": "Sales Pipeline",
          "statuses": ["LEAD", "CONTACTED", "QUOTED", "WON", "LOST"],
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customFields": [
        {
          "companyId": "uuid",
          "fieldId": "uuid",
          "entityType": "JOB",
          "name": "Project Number",
          "type": "STRING",
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customerCustomFields": [
        {
          "companyId": "uuid",
          "fieldId": "uuid",
          "entityType": "CUSTOMER",
          "name": "Birthday",
          "type": "DATE",
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customTexts": [
        {
          "companyId": "uuid",
          "customTextId": "uuid",
          "name": "Default Estimate Terms",
          "text": "This estimate is valid for 30 days.",
          "documentType": "ESTIMATE",
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customDocumentTemplates": [
        {
          "companyId": "uuid",
          "templateId": "uuid",
          "title": "Liability Waiver",
          "text": "I waive all liability...",
          "requireSignature": true,
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "customerCategories": [
        {
          "companyId": "uuid",
          "categoryId": "uuid",
          "name": "Residential",
          "parentCategoryId": null,
          "createdAt": "2024-01-01T00:00:00.000Z"
        }
      ],
      "programTemplates": [],
      "customerPrograms": [],
      "customerEquipment": [],
      "customerSubscriptions": [],
      "equipmentCatalog": [],
      "inventoryItems": [],
      "inventoryTrucks": [],
      "inventoryCategories": [],
      "clockInTypes": [],
      "programSavingsThresholdPercent": 50,
      "programVisitHorizonDays": 90,
      "subscription": {
        "status": "ACTIVE",
        "tradesmenTechPublicKey": "pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "freeTrialEndDate": "2026-05-01",
        "hasPaymentMethod": true,
        "paymentMethodLast4": "4242",
        "billingDayOfMonth": 15,
        "maxEmployees": 5,
        "lastPaymentDate": "2026-04-01T09:00:00.000Z",
        "lastPaymentAmount": 11500,
        "cancelAtPeriodEnd": false,
        "cancelledAt": null,
        "pricing": {
          "basePrice": 4500,
          "additionalUserPrice": 3000,
          "isCustomPricing": false
        }
      }
    }
    
  • Note: Each entry in jobs carries a displayId — a human-friendly, company-scoped sequential job number (an integer starting at 1; see the Create Job endpoint in jobs-endpoints.md). It is absent on jobs created before the feature was added. A job with attached videos also carries a videos array (each {videoId, fileName, contentType, size, s3Key, uploadedAt} plus thumbnailS3Key, durationMs, width, height when known — see "Job Videos" in jobs-endpoints.md). The embedded jobs do not include their documents, so document displayIds are not part of the login payload.
  • Error Responses:
    • 400 Bad Request: Missing body, or missing employeeEmail/password.
    • 401 Unauthorized: Invalid credentials (the message is intentionally generic and does not reveal whether the email exists).
    • 403 Forbidden: The employee exists but has hasAppAccess: false.
    • 429 Too Many Requests: The account is temporarily locked after 5 consecutive failed login attempts. The lockout lasts 15 minutes; a Retry-After header gives the remaining seconds. A successful login resets the counter.
    • 500 Internal Server Error: Unexpected server error.

Verify MFA Code

Complete an MFA login by submitting the 6-digit code emailed to the employee (or texted, after a Resend MFA Code with method: "SMS"), together with the mfaToken from the Login endpoint's MFA challenge response. On success, the response is the full Login success payload (token, company data, etc. — identical shape to Login) plus two extra fields the device should persist:

  • Method: POST
  • Path: /prod/accounts/login/mfa/verify
  • Request Body:
    {
      "mfaToken": "base64url_challenge_token",
      "code": "123456"
    }
    
  • Success Response: 200 OK — the Login success payload with these additional fields:
    {
      "mfaDeviceToken": "base64url_device_token",
      "mfaDeviceTokenExpiresAt": 1773565140
    }
    
    The client should store mfaDeviceToken securely on the device and send it in future Login requests to skip MFA until mfaDeviceTokenExpiresAt (epoch seconds, 30 days after verification). The trust window is fixed at verification time; it does not extend on later logins. Each device gets its own token, so trusting one device never shortens or revokes another's window.
  • Note: The code allows up to 5 attempts before the challenge is invalidated; the challenge is single-use and expires 10 minutes after issuance. After an invalidated or expired challenge, restart from the Login endpoint to get a new code.
  • Error Responses:
    • 400 Bad Request: Missing body, or missing mfaToken/code.
    • 401 Unauthorized: Invalid or expired challenge token, wrong code, or attempts exhausted (the message is intentionally generic and does not distinguish these cases).
    • 403 Forbidden: The employee's hasAppAccess was set to false after the challenge was issued.
    • 500 Internal Server Error: Unexpected server error.

Resend MFA Code

Re-deliver the verification code for a pending MFA challenge, by email again or by SMS instead (for a user who no longer has access to their inbox but still has their phone). A new code is generated each time — previous codes stop working — and the 10-minute expiry restarts. The same mfaToken remains valid for Verify MFA Code.

  • Method: POST
  • Path: /prod/accounts/login/mfa/resend
  • Request Body:
    {
      "mfaToken": "base64url_challenge_token",
      "method": "SMS"
    }
    
    method must be EMAIL or SMS. Only offer SMS when the Login challenge response included phoneNumberMasked.
  • Success Response: 200 OK
    {
      "deliveryMethod": "SMS",
      "mfaExpiresAt": 1770973740,
      "destinationMasked": "***-***-7890"
    }
    
  • Note: At most 3 re-sends are allowed per challenge (the initial email does not count); the 5-attempt guess limit is shared across all codes for the challenge and does not reset on re-send. After the cap, restart from the Login endpoint.
  • Error Responses:
    • 400 Bad Request: Missing body, missing mfaToken, invalid method, or method: "SMS" when the phone number on file cannot receive text messages (the emailed code stays valid in that case).
    • 401 Unauthorized: Invalid or expired challenge token.
    • 429 Too Many Requests: The re-send cap for this challenge is exhausted.
    • 500 Internal Server Error: Unexpected server error.

Validate Token

Check whether the caller's auth token is still valid. Useful for the front end to detect expired or invalidated sessions without performing a heavier authenticated request.

  • Method: GET
  • Path: /prod/accounts/validate-token
  • Authentication: Required (Bearer Token).
  • Request Body: None.
  • Success Response: 200 OK
    {
      "valid": true,
      "companyId": "uuid",
      "employeeId": "uuid"
    }
    
  • Error Responses:
    • 401 Unauthorized: Missing, invalid, revoked, or expired token. The body includes the machine-readable reason field described under Authentication. The front end should treat any 401 as a signal to redirect the user to the login screen, and may use reason to tell the user why (e.g. SUPERSEDED → "signed in on another device").
    • 500 Internal Server Error: Unexpected server error.

Forgot Password

Request a password reset code. A 6-digit code is emailed to the employee's address if an account exists. The endpoint always returns 200 to avoid leaking whether an email is registered.

  • Method: POST
  • Path: /prod/accounts/forgot-password
  • Request Body:
    {
      "employeeEmail": "john@company.com"
    }
    
  • Success Response: 200 OK
    {
      "message": "If an account exists for that email, a password reset code has been sent."
    }
    
    Note: The reset code expires after 60 minutes. Only one active reset code exists per employee at a time; requesting a new code invalidates any previous code. Addresses ending in @example.com are never emailed (the domain cannot receive mail); the endpoint still answers 200.
  • Error Responses:
    • 400 Bad Request: Missing body or missing employeeEmail.
    • 500 Internal Server Error: Unexpected server error.

Reset Password

Complete a password reset using the 6-digit code emailed by the Forgot Password endpoint. On success, the employee's password is updated and any existing login sessions are invalidated.

  • Method: POST
  • Path: /prod/accounts/reset-password
  • Request Body:
    {
      "employeeEmail": "john@company.com",
      "code": "123456",
      "newPassword": "newSecurePassword123"
    }
    
    Note: newPassword must be at least 8 characters. The code allows up to 5 attempts before it is invalidated.
  • Success Response: 200 OK
    {
      "message": "Password reset successfully"
    }
    
  • Error Responses:
    • 400 Bad Request: Missing body, missing fields, newPassword too short, or invalid/expired reset code.
    • 500 Internal Server Error: Unexpected server error.

Change Password

Change the signed-in employee's password. The caller proves they know the current password; on success the new password is stored, every other session of the employee is revoked (those devices receive 401 with reason: "PASSWORD_CHANGED"), any pending password-reset code is invalidated, and a "your password was changed" notice is emailed to the employee's address (sent from the company brand's sender; best-effort — an email failure never fails the change; never sent to @example.com). The calling session stays valid, so no new token is issued.

  • Method: POST
  • Path: /prod/accounts/change-password
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "currentPassword": "oldSecurePassword123",
      "newPassword": "newSecurePassword456"
    }
    
    Note: both fields are required strings. newPassword must be at least 8 characters and different from currentPassword. Checking the current password counts against the same per-email brute-force limit as Login (5 consecutive failures lock the email for 15 minutes, for both endpoints).
  • Success Response: 200 OK
    {
      "message": "Password changed successfully",
      "otherSessionsSignedOut": 1
    }
    
    otherSessionsSignedOut is the number of other sessions that were revoked.
  • Error Responses: 4xx bodies carry message plus a machine-readable code. A wrong current password is deliberately not a 401/403 — clients treat those as "session ended".
    • 400 Bad Request, code:
      • INVALID_REQUEST — missing body, or currentPassword / newPassword missing or not a string.
      • PASSWORD_TOO_SHORT — newPassword is shorter than 8 characters.
      • SAME_PASSWORD — newPassword equals currentPassword.
      • INVALID_CURRENT_PASSWORD — the current password is wrong.
      • IMPERSONATED_SESSION — the caller is a support session acting on the employee's behalf, which may not change the employee's password.
    • 401 Unauthorized: Missing, invalid, revoked, or expired token (see Authentication).
    • 404 Not Found: The employee no longer exists.
    • 429 Too Many Requests, code: "TOO_MANY_ATTEMPTS": Too many failed attempts for this email; a Retry-After header gives the seconds remaining.
    • 500 Internal Server Error: Unexpected server error.

Upload a company logo as a base64 string.

  • Method: POST
  • Path: /prod/accounts/companies/logo
  • Headers: Authorization: Bearer <token>
  • Request Body:
    {
      "logoBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
      "contentType": "image/png"
    }
    
  • Success Response: 200 OK
    {
      "message": "Logo uploaded successfully",
      "logoUrl": "https://s3.amazonaws.com/bucket/logo.png"
    }
    
  • Error Responses:
    • 400 Bad Request: Missing body or missing logoBase64.
    • 401 Unauthorized: Missing or invalid token.
    • 500 Internal Server Error: Unexpected server error.

Update Employee Profile

Update the authenticated employee's email or phone number.

  • Method: PATCH
  • Path: /prod/accounts/employees/profile
  • Authentication: Required (Bearer Token)
  • Request Body:
    {
      "email": "new-email@example.com",
      "phoneNumber": "1234567890"
    }
    
    Note: At least one of email or phoneNumber must be provided. When provided, phoneNumber must be a valid US phone number (10 digits, or 11 digits starting with 1) and is normalized to E.164 (e.g. +11234567890) before storage.
  • Success Response: 200 OK
    {
      "message": "Profile updated successfully"
    }
    
  • Error Responses:
    • 400 Bad Request: Missing body, invalid email format, invalid phone number, or no fields provided.
    • 401 Unauthorized: Missing or invalid token.
    • 409 Conflict: Email already in use by an existing company or employee.
    • 500 Internal Server Error: Unexpected server error.

Update Employee

Update an employee's details. Only ADMIN employees can update other employees.

  • Method: PUT
  • Path: /prod/accounts/employees/{employeeId}
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Path Parameters:
    • employeeId (required): The ID of the employee to update.
  • Request Body:
    {
      "firstName": "Jane",
      "middleName": "A",
      "lastName": "Smith",
      "email": "jane@company.com",
      "phoneNumber": "5551234567",
      "employeeType": "LEAD_TECHNICIAN",
      "hasAppAccess": false,
      "hourlyRate": 2550,
      "clockInTypeRates": {
        "clock-in-type-uuid-work": 2550,
        "clock-in-type-uuid-travel": 1200
      }
    }
    
    Note: All fields are optional. At least one field must be provided (an empty string counts as provided). firstName may be changed but never blanked — it is the only field every employee needs. middleName, lastName and phoneNumber can be cleared with an empty string or null (stored as null; names are trimmed). employeeType must be one of: ADMIN, LEAD_TECHNICIAN, TECHNICIAN, CREW_TECHNICIAN. When phoneNumber is non-blank it must be a valid US phone number (10 digits, or 11 digits starting with 1) and is normalized to E.164 (e.g. +15551234567) before storage. An empty-string or null email removes the stored address, but only for an employee without app access (or one the same request sets hasAppAccess: false on) — an employee who can log in signs in with it, so blanking it answers 400 (email is required for an employee with app access). Email uniqueness is enforced across employees and companies. hasAppAccess (boolean) toggles whether the employee can log in and is counted as a billable per-seat user; setting it to false revokes app access and removes them from per-seat billing. Note that flipping it back to true does not by itself create login credentials — the employee must have (or set, via the forgot/reset-password flow) an email and password to actually sign in. hourlyRate is the employee's pay rate as a non-negative integer number of cents per hour (e.g. 2550 = $25.50/hr); set it to null to clear the rate. clockInTypeRates (the per-clock-in-type rates described under Create Employee) replaces the whole map — a type left out of the object loses its rate — and null clears it.
  • Success Response: 200 OK
    {
      "message": "Employee updated successfully",
      "employeeId": "uuid"
    }
    
  • Error Responses:
    • 400 Bad Request: Missing body, missing employeeId, no valid fields provided, blank firstName, invalid email format, blank email on an employee with app access, invalid phone number, invalid employeeType, non-boolean hasAppAccess, invalid hourlyRate (must be a non-negative integer or null), or invalid clockInTypeRates (must be an object of non-negative integer cents per clock-in type id, or null).
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 404 Not Found: Target employee not found in the caller's company.
    • 409 Conflict: Email already in use by an existing company or employee.
    • 500 Internal Server Error: Unexpected server error.

List Employees

Returns all employees for the authenticated user's company, along with the company's max employee limit.

  • Method: GET
  • Path: /prod/accounts/employees
  • Authentication: Required (Bearer Token).
  • Success Response: 200 OK
    {
      "employees": [
        {
          "companyId": "uuid",
          "employeeId": "uuid",
          "firstName": "John",
          "middleName": "D",
          "lastName": "Doe",
          "email": "john@company.com",
          "phoneNumber": "0987654321",
          "employeeType": "ADMIN",
          "hasAppAccess": true,
          "hourlyRate": 2550,
          "clockInTypeRates": {
            "clock-in-type-uuid-work": 2550,
            "clock-in-type-uuid-travel": 1200
          },
          "description": "Expert plumber with 10 years of experience.",
          "photoUrl": "https://s3.amazonaws.com/bucket/employee-photos/companyId/employeeId/photo.png"
        }
      ],
      "maxEmployees": 5
    }
    
Field Type Description
employees array List of employee objects for the company
maxEmployees number Maximum number of employees allowed for the company (defaults to 1)

Note: hourlyRate is the employee's pay rate in cents per hour (e.g. 2550 = $25.50/hr), or null when no rate is set, and clockInTypeRates is their rate per clock-in type ({} when none are set; see timesheets-endpoints.md). Both are compensation data: ADMIN callers see every employee's values; non-ADMIN callers see only their own (both are null for everyone else regardless of what is set). The same visibility rule applies to the employees array in the Login response.

  • Error Responses:
    • 401 Unauthorized: Missing or invalid token.
    • 500 Internal Server Error: Unexpected server error.

Delete Employee

Delete an employee from the authenticated admin's company.

  • Method: DELETE
  • Path: /prod/accounts/employees/{employeeId}
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Path Parameters:
    • employeeId (required): The ID of the employee to delete.
  • Notes: The caller cannot delete themselves.
  • Success Response: 200 OK
    {
      "message": "Employee deleted successfully",
      "employeeId": "uuid"
    }
    
  • Error Responses:
    • 400 Bad Request: Missing employeeId in path.
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee, or caller attempted to delete themselves.
    • 404 Not Found: Employee not found in the caller's company.
    • 500 Internal Server Error: Unexpected server error.

Update Company

Update the authenticated admin's company details.

  • Method: PATCH

  • Path: /prod/accounts/companies

  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.

  • Request Body:

    {
      "name": "Updated Company Name",
      "email": "new-email@company.com",
      "phoneNumber": "5551234567",
      "address": {
        "streetAddress": "789 New St",
        "city": "New City",
        "state": "NC",
        "zipCode": "99999"
      },
      "timeZone": "America/Chicago",
      "additionalCharges": [
        { "type": "TAX", "label": "Sales Tax", "percentage": 8.25 },
        { "id": "existing-charge-id", "type": "CUSTOM", "label": "Disposal Fee", "percentage": 2 },
        { "type": "CONVENIENCE", "label": "Card Processing Fee", "percentage": 3, "appliesTo": "PAYMENT", "waivedForAch": true }
      ],
      "tipPercentages": [10, 15, 20]
    }
    

    Note: All fields are optional, but at least one must be provided (unknown fields are ignored; a body with no allowed field answers 400 with the allowedFields list in the body). Allowed fields: name, email, phoneNumber, address, timeZone, additionalCharges, tipPercentages, programSavingsThresholdPercent, programVisitHorizonDays. String fields are trimmed; email is also lowercased before the uniqueness check and storage. When providing address, all subfields (streetAddress, city, state, zipCode) are required.

    additionalCharges replaces the company's full charge list. Each entry requires type (one of TAX, CONVENIENCE, CUSTOM), a non-empty label (max 100 chars), and a percentage in (0, 100]. An id is optional — supply it to preserve an existing charge's id, otherwise one is generated. appliesTo (optional, one of INVOICE, PAYMENT; default INVOICE) says when the charge is applied: INVOICE charges are snapshotted onto every new invoice (see Create Document in jobs-endpoints.md); a PAYMENT charge is a payment-time fee — it never appears on the invoice itself and is added only when the customer pays electronically (a keyed or tapped card, a saved payment method, the emailed document page, or "Pay over time"), as a percentage of the amount being paid (before any tip). waivedForAch (optional boolean, stored on PAYMENT charges only, default false) skips that fee when the payment is made from a bank account (ACH). A cancelled or declined payment leaves no fee behind; once a payment with the fee is approved, the fee is recorded on the paid document (paymentFeeAmount / paymentFees, see payments-endpoints.md) and stays on it. The stored list always carries appliesTo (and waivedForAch for PAYMENT entries), so the login response and company reads return them. tipPercentages replaces the company's tip options; each entry must be a number in (0, 100]. Send [] to clear either list. programSavingsThresholdPercent is a number in [0, 100] — the percent spread required before the maintenance-program DIY-savings line is shown (defaults to 50 when never set; see programs-endpoints.md). programVisitHorizonDays is an integer in [1, 730] — how many days ahead of today maintenance-program visit jobs are kept materialized (defaults to 90 when never set; shrinking it does not delete already-created visit jobs).

  • Success Response: 200 OK

    {
      "message": "Company updated successfully"
    }
    
  • Error Responses:

    • 400 Bad Request: Missing body, no valid fields provided, a string field that is not a string, invalid email format, an address missing a subfield, an invalid additionalCharges or tipPercentages entry, programSavingsThresholdPercent outside [0, 100], or programVisitHorizonDays not an integer in [1, 730].
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 409 Conflict: Email already in use by an existing company or employee.
    • 500 Internal Server Error: Unexpected server error.

Delete Company

Schedule deletion of the authenticated admin's company and all associated business data. This is the back-end of the in-app Delete Account flow required by the App Store.

Deletion is not immediate: the company is placed on a 7-day hold. The response includes deletionScheduledFor (the UTC timestamp after which the purge runs). During the hold the account remains fully usable — employees stay signed in and all data is intact — so the deletion can be cancelled (see Cancel Company Deletion). Once the hold elapses, a scheduled job permanently deletes every record scoped to the companyId across the company's tables (employees, services, customers, jobs, documents, payments, calendar connections and event mappings, pipelines, custom fields/texts/documents, email campaigns, customer categories, employee profiles, equipment, customer subscriptions, inventory items and movements, merchant accounts, customer vault, review links, reminder configs, SaaS subscription, free trial, and the Stripe account record), removing the company record itself last.

Calling this endpoint again while a deletion is already pending is a no-op: it returns the existing deletionScheduledFor without extending the hold.

When a deletion is newly scheduled in production, an internal notification email is sent to the Tradesmen support inbox naming the company and the employee who requested it. A second internal email is sent to the same inbox when the scheduled purge actually runs (subject Company permanently deleted: {name}), or if the purge fails and will be retried on the next run (subject Company purge FAILED (will retry): {name}). These are server-side only and do not affect the API response.

  • Method: DELETE
  • Path: /prod/accounts/companies
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Request Body: None
  • Success Response: 200 OK
    {
      "message": "Company deletion scheduled. The account remains active until the hold period ends, after which all data is permanently deleted. Cancel any time before then to keep the account.",
      "companyId": "uuid",
      "deletionScheduledFor": "2026-06-11T12:00:00.000Z"
    }
    
    When a deletion is already pending, the message is "Company deletion is already scheduled" and deletionScheduledFor reflects the original schedule.
  • Error Responses:
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 404 Not Found: Company not found.
    • 500 Internal Server Error: Unexpected server error.

Cancel Company Deletion

Cancel a pending company-deletion hold scheduled via Delete Company. Must be called before the hold period elapses; once the purge runs, the company and its data no longer exist.

  • Method: POST
  • Path: /prod/accounts/companies/cancel-deletion
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Request Body: None
  • Success Response: 200 OK
    {
      "message": "Company deletion cancelled",
      "companyId": "uuid"
    }
    
  • Error Responses:
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 404 Not Found: Company not found.
    • 409 Conflict: No pending deletion to cancel.
    • 500 Internal Server Error: Unexpected server error.

Update Feature Flags

Update feature flags for the authenticated employee's company or their own account.

  • Method: PUT
  • Path: /prod/accounts/feature-flags/{tier}
  • Authentication: Required (Bearer Token). For company tier, caller must be an ADMIN employee. For account tier, any authenticated employee can update their own flags.
  • Path Parameters:
    • tier (required): The tier to update. Must be one of: company, account.
  • Request Body:
    {
      "flags": {
        "darkMode": true,
        "betaFeature": false
      }
    }
    
    Note: flags must be an object. Its contents replace any existing flags for the target.
  • Success Response: 200 OK
    {
      "message": "Feature flags updated successfully",
      "targetId": "COMPANY#uuid"
    }
    
  • Error Responses:
    • 400 Bad Request: Invalid tier, missing body, or missing/invalid flags object.
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee (company tier only).
    • 500 Internal Server Error: Unexpected server error.

Update Employee Page

Set or update an employee's public-facing profile page (description and/or photo). This data is displayed to customers.

  • Method: PUT
  • Path: /prod/accounts/employees/{employeeId}/page
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Path Parameters:
    • employeeId (required): The ID of the employee whose page to update.
  • Request Body:
    {
      "description": "Expert plumber with 10 years of experience in residential and commercial plumbing.",
      "photoBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
      "contentType": "image/png"
    }
    
    Note: At least one of description or photoBase64 must be provided. Both are optional individually — omitted fields are preserved from the existing profile. Set description to null to clear it. contentType is optional and defaults to image/png if not provided.
  • Success Response: 200 OK
    {
      "message": "Employee page updated successfully",
      "photoUrl": "https://s3.amazonaws.com/bucket/employee-photos/companyId/employeeId/photo.png"
    }
    
    Note: photoUrl is null if no photo has been uploaded.
  • Error Responses:
    • 400 Bad Request: Missing body, missing employeeId, neither description nor photo provided, or description is neither a string nor null.
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 404 Not Found: Target employee not found in the caller's company.
    • 500 Internal Server Error: Unexpected server error.

Update Max Employees

Update the maximum number of employees allowed for the company.

  • Method: PATCH
  • Path: /prod/accounts/companies/max-employees
  • Authentication: Required (Bearer Token). Caller must be an ADMIN employee.
  • Request Body:
    {
      "maxEmployees": 10
    }
    
    Note: maxEmployees must be a positive integer.
  • Success Response: 200 OK
    {
      "message": "Max employees updated successfully",
      "maxEmployees": 10
    }
    
  • Error Responses:
    • 400 Bad Request: Missing body, missing maxEmployees, or invalid value (must be a positive integer).
    • 401 Unauthorized: Missing or invalid token.
    • 403 Forbidden: Caller is not an ADMIN employee.
    • 500 Internal Server Error: Unexpected server error.