{"openapi":"3.0.3","info":{"title":"Pooli API","version":"1.0.0","description":"Pooli public API, version 1 alpha. Machine-readable contract for https://api.pooli.app. Human summary: https://api.pooli.app/docs and https://pooli.app/api-documentation/.\n\nCanonical paths use /v1/users/{uid} and /v1/organizations/{orgId}. /users/{uid} and /organizations/{orgId} are legacy aliases of those prefixes and stay supported.\n\nDevelopment host is https://dev-api.pooli.app. The name api-dev.pooli.app does not resolve.\n\nErrors are JSON objects { error, message, status }. List endpoints accept limit, cursor, startAfter, and envelope. The default list body stays a JSON array (null for an empty log page or empty inventory) so existing clients keep working. envelope=1 returns { data, page }.\n\nAPI keys are created in the Pooli app, which calls the Firebase callable apiKeys-generateApiKey (group prefix required). Each call adds a new key. Keys have no enforced expiry. apiKeys.expiresAt is reserved for a later maximum lifetime and is ignored. POST /v1/auth/keys/rotate and apiKeys-rotateApiKey issue a new key while the previous key keeps working until graceEndsAt (default 7 days). The next use after that instant revokes the old key and deletes its access tokens. POST /v1/auth/keys/revoke and apiKeys-revokeApiKey revoke immediately. Keys are scoped to the authenticated user (apiKeys.uid). Organization routes succeed when that user is the organization ownerUserId, an admin (organization.admins, or members/{uid}.role admin), or a technician with status active. Invoices, payments, estimates, service subscriptions, and billing fields on customers require owner or admin. There is no separate organization key. Optional apiKeys.scopes, when the array is non-empty, must include content_management for /v1/content-ingest. Keys without a scopes array still work.\n\nIntentional legacy spellings, kept as stored: field supplimentalSanitizers, and filter value cartrige. Do not rename them.\n\nRate limit: 60 requests per hour per key unless the key user has an active subscription (Pool Club). Subscribers are not limited. Auth header is Authorization: Bearer <API_KEY>. The user or organization id is a path parameter, not a second header.\n\nOrganization read routes never read another organization's collection: customers, routes, stops, invoices, estimates, subscriptions, payments, products, equipment, service visits, and service reports. They are read only. Invoices, payments, estimates, service subscriptions, and billing fields on customers (balances, payment methods, autopay, Stripe customer ids) are limited to the organization owner and admins. Active technicians still receive non-billing customer fields used for service. Card numbers, CVC, expiry, and last4 are removed. Stripe ids are kept for owners and admins.\n\nRoute dates use the organization time zone when organizations/{orgId}.timeZone or timezone is a valid IANA name, or when timeZoneOffset or timezoneOffset is a number of minutes (local minus UTC). The organization record does not require a timezone. Missing or invalid values fall back to UTC (timeZoneSource utc-fallback).\n\nPOST /v1/auth/token exchanges a long-lived API key for a short-lived Bearer access token (default 15 minutes, maximum 1 hour). POST /v1/auth/token/refresh replaces an unexpired access token. POST /v1/auth/keys/regenerate invalidates that key's access tokens without changing the long-lived key string. POST /v1/auth/keys/rotate issues a new key and keeps the old key until the grace period ends. POST /v1/auth/keys/revoke ends that key immediately and deletes its tokens. Expired tokens are rejected.\n\nSend X-Pooli-Act-As: <user id> on an organization route to run the request as that user. Only the organization owner may do this. The target must belong to that organization. A target with a higher role is rejected. The effective role is the acted-as user's role. Billing results follow the effective role. If X-Pooli-Organization is also sent, it must equal the organization id in the path. Personal /v1/users routes reject the header when it names someone else. Content ingest rejects the header. Auth routes ignore it and use the presented credential only.\n\nIdempotency-Key is required on creates that mint a customer, an organization pool, a stop, an inventory item, or a reminder. The value is 8 to 200 characters. The same key and JSON body replay the original status and body. A different body with the same key returns 409. The role check runs before the key is recorded. A failed validation does not store a replay. Organization pool create stays HTTP 200. The other idempotent creates return 201.\n\nWrite roles. Owner or admin: create a customer, create, update, or delete an organization pool, import CSV, and set technician status. Owner, admin, or active technician: create a stop, and create, update, or delete an organization log. Personal user writes require the path uid to be the API key user: user pools, user logs, inventory, and reminders. Those routes reject acting as someone else. Technicians cannot create customers or organization pools.\n\nTechnician status writes accept only active, inactive, or rejected. approved and any other value return 400. active links the technician user the same way processTechnicianApproval does (organizationId, technicianId, organizationStatus active). inactive and rejected clear those three fields when they point at this technician, and leave the organization owner profile unchanged. Stored technician documents may still show pending. Listing with a status query returns pending technicians only."},"servers":[{"url":"https://api.pooli.app","description":"Production"},{"url":"https://dev-api.pooli.app","description":"Development. dev-api.pooli.app is the host that resolves. api-dev.pooli.app does not."}],"tags":[{"name":"Documentation"},{"name":"Pools"},{"name":"Logs"},{"name":"Reminders"},{"name":"Inventory"},{"name":"Technicians"},{"name":"Organization pools"},{"name":"Organization logs"},{"name":"Customers"},{"name":"Routes"},{"name":"Stops"},{"name":"Invoices"},{"name":"Estimates"},{"name":"Subscriptions"},{"name":"Payments"},{"name":"Products"},{"name":"Equipment"},{"name":"Service visits"},{"name":"Service reports"},{"name":"Content ingest"},{"name":"Authentication"}],"paths":{"/v1/users/{uid}/pools":{"get":{"tags":["Pools"],"summary":"List pools for a user","description":"Returns sanitized pools for the user id in the path. The API key must belong to that user. location and weather are omitted. Default body is a JSON array. Empty is [].","operationId":"listUserPools","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Pool"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Pools"],"summary":"Create a user pool","description":"The path uid must be the API key user. Acting as another user is rejected on this route. Body is { pool: PoolCreate }. The server sets userId from the path. supplimentalSanitizers is an intentional legacy field name. Do not rename it. cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","operationId":"createUserPool","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolCreateRequest"}}}}}},"/v1/users/{uid}/pools/{poolId}":{"get":{"tags":["Pools"],"summary":"Get one user pool","description":"Returns the pool document, including fields the list endpoint strips. Missing pools respond 500 with message Unable to fetch pool. (the handler does not map not-found to 404).","operationId":"getUserPool","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Pools"],"summary":"Soft-delete a user pool","description":"The path uid must be the API key user. Sets deleted=true. The document is not removed.","operationId":"deleteUserPool","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"patch":{"tags":["Pools"],"summary":"Update a user pool","description":"The path uid must be the effective user. Allowed fields: name, gallonEstimate, poolType, filterCategory, sanitizingChemical, isIndoor, supplimentalSanitizers. supplimentalSanitizers is the intentional legacy spelling. filterCategory accepts de, sand, and cartridge. Unknown fields are rejected. No Stripe calls.","operationId":"patchUserPool","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolPatch"}}}}}},"/v1/users/{uid}/pools/{poolId}/logs":{"get":{"tags":["Logs"],"summary":"List logs for a user pool","description":"Ordered by createdAt descending. Default limit is 50. cursor and startAfter are a date string (ISO-8601 recommended) of the last item from the previous page. type is applied after the page is read, so a filtered page can be shorter than limit. pool and weather are omitted. An empty page is null unless envelope=1, which returns data: [].","operationId":"listUserLogs","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Log"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Logs"],"summary":"Create a user pool log","description":"The path uid must be the API key user. Query type is required. Body is the log object itself (not wrapped). data in the 201 response is the new log id string, not the stored document.","operationId":"createUserLog","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedLog"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogCreate"}}}}}},"/v1/users/{uid}/pools/{poolId}/logs/{logId}":{"get":{"tags":["Logs"],"summary":"Get one user pool log","description":"404 when the document is missing. pool and weather are omitted.","operationId":"getUserLog","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Logs"],"summary":"Delete a user pool log","description":"The path uid must be the API key user. Hard-deletes the log document.","operationId":"deleteUserLog","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessMessage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"patch":{"tags":["Logs"],"summary":"Update a user pool log","description":"The path uid must be the API key user. Allowed fields: note, creatorName, chemicalReading, equipmentReading. type, userId, and organizationId cannot be changed.","operationId":"patchUserLog","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogPatch"}}}}}},"/v1/users/{uid}/pools/{poolId}/reminders":{"get":{"tags":["Reminders"],"summary":"List reminders for a user pool","description":"The pool field is omitted. Empty is []. Create and update are separate routes and require the path uid to be the API key user.","operationId":"listUserReminders","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Reminder"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Reminders"],"summary":"Create a user pool reminder","description":"reminderType is required. dueDate is an optional date-time and defaults to now. Idempotency-Key is required. This calls the existing scheduleReminder helper, which stores dueDate, notifyDate, and importance for the reminder scheduler. The request itself does not send a notification.","operationId":"createUserReminder","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reminder"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderWrite"}}}}}},"/v1/users/{uid}/inventory":{"get":{"tags":["Inventory"],"summary":"List inventory for a user","description":"An empty inventory is null unless envelope=1, which returns data: []. Create and update are separate routes and require the path uid to be the API key user.","operationId":"listUserInventory","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/InventoryItem"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Inventory"],"summary":"Create a user inventory item","description":"The path uid must be the effective user. name is required. Idempotency-Key is required. No prices or Stripe fields.","operationId":"createUserInventoryItem","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryWrite"}}}}}},"/v1/users/{uid}/inventory/{itemId}":{"patch":{"tags":["Inventory"],"summary":"Update a user inventory item","description":"The path uid must be the API key user. Partial update of the same fields as create. No prices or Stripe fields.","operationId":"patchUserInventoryItem","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"itemId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryWrite"}}}}}},"/v1/users/{uid}/pools/{poolId}/reminders/{reminderId}":{"patch":{"tags":["Reminders"],"summary":"Update a user pool reminder","description":"The path uid must be the API key user. Partial update of the same fields as create.","operationId":"patchUserReminder","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reminderId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reminder"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderWrite"}}}}}},"/users/{uid}/pools":{"get":{"tags":["Pools"],"summary":"List pools for a user","description":"Legacy alias of /v1/users/{uid}/pools. Prefer the canonical path. Returns sanitized pools for the user id in the path. The API key must belong to that user. location and weather are omitted. Default body is a JSON array. Empty is [].","operationId":"listUserPoolsAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Pool"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools","x-legacy-alias":true},"post":{"tags":["Pools"],"summary":"Create a user pool","description":"Legacy alias of /v1/users/{uid}/pools. Prefer the canonical path. The path uid must be the API key user. Acting as another user is rejected on this route. Body is { pool: PoolCreate }. The server sets userId from the path. supplimentalSanitizers is an intentional legacy field name. Do not rename it. cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","operationId":"createUserPoolAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolCreateRequest"}}}},"x-canonical-path":"/v1/users/{uid}/pools","x-legacy-alias":true}},"/users/{uid}/pools/{poolId}":{"get":{"tags":["Pools"],"summary":"Get one user pool","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}. Prefer the canonical path. Returns the pool document, including fields the list endpoint strips. Missing pools respond 500 with message Unable to fetch pool. (the handler does not map not-found to 404).","operationId":"getUserPoolAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}","x-legacy-alias":true},"delete":{"tags":["Pools"],"summary":"Soft-delete a user pool","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}. Prefer the canonical path. The path uid must be the API key user. Sets deleted=true. The document is not removed.","operationId":"deleteUserPoolAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}","x-legacy-alias":true},"patch":{"tags":["Pools"],"summary":"Update a user pool","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}. Prefer the canonical path. The path uid must be the effective user. Allowed fields: name, gallonEstimate, poolType, filterCategory, sanitizingChemical, isIndoor, supplimentalSanitizers. supplimentalSanitizers is the intentional legacy spelling. filterCategory accepts de, sand, and cartridge. Unknown fields are rejected. No Stripe calls.","operationId":"patchUserPoolAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolPatch"}}}},"x-canonical-path":"/v1/users/{uid}/pools/{poolId}","x-legacy-alias":true}},"/users/{uid}/pools/{poolId}/logs":{"get":{"tags":["Logs"],"summary":"List logs for a user pool","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/logs. Prefer the canonical path. Ordered by createdAt descending. Default limit is 50. cursor and startAfter are a date string (ISO-8601 recommended) of the last item from the previous page. type is applied after the page is read, so a filtered page can be shorter than limit. pool and weather are omitted. An empty page is null unless envelope=1, which returns data: [].","operationId":"listUserLogsAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Log"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/logs","x-legacy-alias":true},"post":{"tags":["Logs"],"summary":"Create a user pool log","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/logs. Prefer the canonical path. The path uid must be the API key user. Query type is required. Body is the log object itself (not wrapped). data in the 201 response is the new log id string, not the stored document.","operationId":"createUserLogAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedLog"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogCreate"}}}},"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/logs","x-legacy-alias":true}},"/users/{uid}/pools/{poolId}/logs/{logId}":{"get":{"tags":["Logs"],"summary":"Get one user pool log","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/logs/{logId}. Prefer the canonical path. 404 when the document is missing. pool and weather are omitted.","operationId":"getUserLogAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/logs/{logId}","x-legacy-alias":true},"delete":{"tags":["Logs"],"summary":"Delete a user pool log","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/logs/{logId}. Prefer the canonical path. The path uid must be the API key user. Hard-deletes the log document.","operationId":"deleteUserLogAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessMessage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/logs/{logId}","x-legacy-alias":true},"patch":{"tags":["Logs"],"summary":"Update a user pool log","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/logs/{logId}. Prefer the canonical path. The path uid must be the API key user. Allowed fields: note, creatorName, chemicalReading, equipmentReading. type, userId, and organizationId cannot be changed.","operationId":"patchUserLogAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogPatch"}}}},"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/logs/{logId}","x-legacy-alias":true}},"/users/{uid}/pools/{poolId}/reminders":{"get":{"tags":["Reminders"],"summary":"List reminders for a user pool","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/reminders. Prefer the canonical path. The pool field is omitted. Empty is []. Create and update are separate routes and require the path uid to be the API key user.","operationId":"listUserRemindersAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Reminder"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/reminders","x-legacy-alias":true},"post":{"tags":["Reminders"],"summary":"Create a user pool reminder","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/reminders. Prefer the canonical path. reminderType is required. dueDate is an optional date-time and defaults to now. Idempotency-Key is required. This calls the existing scheduleReminder helper, which stores dueDate, notifyDate, and importance for the reminder scheduler. The request itself does not send a notification.","operationId":"createUserReminderAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reminder"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderWrite"}}}},"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/reminders","x-legacy-alias":true}},"/users/{uid}/inventory":{"get":{"tags":["Inventory"],"summary":"List inventory for a user","description":"Legacy alias of /v1/users/{uid}/inventory. Prefer the canonical path. An empty inventory is null unless envelope=1, which returns data: []. Create and update are separate routes and require the path uid to be the API key user.","operationId":"listUserInventoryAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/InventoryItem"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/users/{uid}/inventory","x-legacy-alias":true},"post":{"tags":["Inventory"],"summary":"Create a user inventory item","description":"Legacy alias of /v1/users/{uid}/inventory. Prefer the canonical path. The path uid must be the effective user. name is required. Idempotency-Key is required. No prices or Stripe fields.","operationId":"createUserInventoryItemAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryWrite"}}}},"x-canonical-path":"/v1/users/{uid}/inventory","x-legacy-alias":true}},"/users/{uid}/inventory/{itemId}":{"patch":{"tags":["Inventory"],"summary":"Update a user inventory item","description":"Legacy alias of /v1/users/{uid}/inventory/{itemId}. Prefer the canonical path. The path uid must be the API key user. Partial update of the same fields as create. No prices or Stripe fields.","operationId":"patchUserInventoryItemAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"itemId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryWrite"}}}},"x-canonical-path":"/v1/users/{uid}/inventory/{itemId}","x-legacy-alias":true}},"/users/{uid}/pools/{poolId}/reminders/{reminderId}":{"patch":{"tags":["Reminders"],"summary":"Update a user pool reminder","description":"Legacy alias of /v1/users/{uid}/pools/{poolId}/reminders/{reminderId}. Prefer the canonical path. The path uid must be the API key user. Partial update of the same fields as create.","operationId":"patchUserReminderAlias","parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reminderId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsRejected"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reminder"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReminderWrite"}}}},"x-canonical-path":"/v1/users/{uid}/pools/{poolId}/reminders/{reminderId}","x-legacy-alias":true}},"/v1/organizations/{orgId}/technicians":{"get":{"tags":["Technicians"],"summary":"List technicians","description":"Any present status query (the value is ignored) returns only technicians whose status is pending. Omit status to return every technician.","operationId":"listTechnicians","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/technicianStatus"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Technician"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/technicians/{techId}/status":{"post":{"tags":["Technicians"],"summary":"Set a technician status","description":"Owners and admins only. status must be active, inactive, or rejected. approved and any other value return 400. active sets the technician user's organizationId, technicianId, and organizationStatus to active, the same fields processTechnicianApproval writes. inactive and rejected clear those three fields when they point at this technician, and leave the organization ownerUserId profile unchanged.","operationId":"updateTechnicianStatus","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"techId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnicianStatusRequest"}}}}}},"/v1/organizations/{orgId}/pools":{"get":{"tags":["Organization pools"],"summary":"List organization pools","description":"Optional assigned query is compared to the isAssigned field as a string, not a boolean. location and weather are omitted.","operationId":"listOrganizationPools","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/assigned"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Pool"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Organization pools"],"summary":"Create an organization pool","description":"Owners and admins only. Active technicians receive 403. Idempotency-Key is required. Clients that used to call this without the header, or as a technician, now receive 400 or 403. Same body as user pool create. The server sets organizationId from the path and userId from the API key's effective user. supplimentalSanitizers is an intentional legacy field name. Do not rename it. cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","operationId":"createOrganizationPool","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolCreateRequest"}}}}}},"/v1/organizations/{orgId}/pools/{poolId}":{"get":{"tags":["Organization pools"],"summary":"Get one organization pool","description":"","operationId":"getOrganizationPool","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Organization pools"],"summary":"Soft-delete an organization pool","description":"Owners and admins only. Active technicians receive 403.","operationId":"deleteOrganizationPool","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"patch":{"tags":["Organization pools"],"summary":"Update an organization pool","description":"Owners and admins only. Same allowed fields as the user pool update. Active technicians receive 403.","operationId":"patchOrganizationPool","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolPatch"}}}}}},"/v1/organizations/{orgId}/pools/{poolId}/logs":{"get":{"tags":["Organization logs"],"summary":"List logs for an organization pool","description":"Same paging rules as user logs. Default limit 50. cursor and startAfter are dates. type is filtered after the page is read. Empty is null unless envelope=1.","operationId":"listOrganizationLogs","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Log"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Organization logs"],"summary":"Create an organization pool log","description":"Owners, admins, and active technicians. Query type is required. Body is the log object. The 201 data field is the new log id.","operationId":"createOrganizationLog","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedLog"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogCreate"}}}}}},"/v1/organizations/{orgId}/pools/{poolId}/logs/{logId}":{"get":{"tags":["Organization logs"],"summary":"Get one organization pool log","description":"","operationId":"getOrganizationLog","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["Organization logs"],"summary":"Delete an organization pool log","description":"Owners, admins, and active technicians.","operationId":"deleteOrganizationLog","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessMessage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"patch":{"tags":["Organization logs"],"summary":"Update an organization pool log","description":"Owners, admins, and active technicians. Allowed fields: note, creatorName, chemicalReading, equipmentReading.","operationId":"patchOrganizationLog","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogPatch"}}}}}},"/v1/organizations/{orgId}/import/csv":{"post":{"tags":["Customers"],"summary":"Import customers and pools from CSV","description":"Owners and admins only. Active technicians receive 403. REST form of the customers-importCsv callable. Send JSON { csvText, mapping, defaults } or a raw text/csv body. filterCategory values are stored as given. The intentional legacy spelling cartrige is valid here and must not be renamed.","operationId":"importOrganizationCustomersCsv","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvImportResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvImportRequest"}},"text/csv":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}}}}},"/v1/organizations/{orgId}/customers":{"get":{"tags":["Customers"],"summary":"List organization customers","description":"Read only. Documents live at organizations/{orgId}/customers. A document whose organizationId field is a different org is omitted. Default limit is 50. Card data and secrets are removed. Owners and admins receive billing fields, including stripeCustomerId, balances, payment methods, and autopay. Active technicians receive the service fields (name, phone, email, address, notes, pools) and those billing fields are omitted.","operationId":"listCustomers","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Customers"],"summary":"Create an organization customer","description":"Owners and admins only. name is required. email, phone, and notes are optional. Billing fields, payment methods, and Stripe ids are rejected. Idempotency-Key is required. No Stripe call is made.","operationId":"createCustomer","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreate"}}}}}},"/v1/organizations/{orgId}/customers/{customerId}":{"get":{"tags":["Customers"],"summary":"Get one organization customer","description":"","operationId":"getCustomer","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"customerId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/routes":{"get":{"tags":["Routes"],"summary":"List routes","description":"Read only. A route is the set of active stops for one technician on one local calendar date, derived from organizations/{orgId}/stops. It is not a separate stored collection. The organization document has no required timezone. date uses organization.timeZone or organization.timezone when that value is an IANA name such as America/Los_Angeles. Otherwise it uses organization.timeZoneOffset or organization.timezoneOffset, in minutes, local minus UTC (the same convention as job.timeZoneOffset; PDT is -420). Abbreviations such as PDT are ignored. If none of those fields are present, or the name is not a valid IANA zone, the date is UTC and timeZoneSource is utc-fallback. id is YYYY-MM-DD:technicianUserId, or YYYY-MM-DD:unassigned. Default limit is 50.","operationId":"listRoutes","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"name":"date","in":"query","required":false,"description":"Local calendar date YYYY-MM-DD in the organization time zone. UTC when the organization has no usable zone.","schema":{"type":"string"}},{"name":"technicianUserId","in":"query","required":false,"description":"Only stops assigned to this technician user id.","schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/RouteDay"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/routes/{routeId}":{"get":{"tags":["Routes"],"summary":"Get one route","description":"routeId is the list id, for example 2026-10-15:technicianUserId.","operationId":"getRoute","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteDay"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/stops":{"get":{"tags":["Stops"],"summary":"List route stops","description":"Read only. organizations/{orgId}/stops. Default limit is 50. Deleted stops are included; clients can filter isDeleted.","operationId":"listStops","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Stops"],"summary":"Create a route stop","description":"Owners, admins, and active technicians. poolId and scheduledDateTime are required. The pool must belong to this organization. The stored stop includes pool, using the app shape (chemicalCategory, indoor, id) plus poolId, and customerId and locationId when the pool has them. technicianUserIds and duration are optional. serviceCost and other billing fields are rejected. Idempotency-Key is required. No routing or Stripe call is made.","operationId":"createStop","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StopCreate"}}}}}},"/v1/organizations/{orgId}/stops/{stopId}":{"get":{"tags":["Stops"],"summary":"Get one route stop","description":"","operationId":"getStop","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stopId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices","description":"Read only. organizations/{orgId}/invoices. Owners and admins only. An active technician receives 403. Default limit is 50. Card data is removed. Stripe ids are kept.","operationId":"listInvoices","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/invoices/{invoiceId}":{"get":{"tags":["Invoices"],"summary":"Get one invoice","description":"","operationId":"getInvoice","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"invoiceId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/estimates":{"get":{"tags":["Estimates"],"summary":"List estimates","description":"Owners and admins only. Active technicians receive 403. Read only. organizations/{orgId}/estimates. Default limit is 50.","operationId":"listEstimates","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/estimates/{estimateId}":{"get":{"tags":["Estimates"],"summary":"Get one estimate","description":"Owners and admins only. Active technicians receive 403.","operationId":"getEstimate","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"estimateId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"List service subscriptions","description":"Owners and admins only. Active technicians receive 403. Read only. organizations/{orgId}/service_subscriptions. These are organization service billing subscriptions, not Pool Club subscriptions. Default limit is 50.","operationId":"listServiceSubscriptions","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/subscriptions/{subscriptionId}":{"get":{"tags":["Subscriptions"],"summary":"Get one service subscription","description":"Owners and admins only. Active technicians receive 403.","operationId":"getServiceSubscription","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/payments":{"get":{"tags":["Payments"],"summary":"List payments","description":"Read only. Owners and admins only. An active technician receives 403. Reads payment_history under this organization's invoices, estimates, and service subscriptions. Optional invoiceId, estimateId, or subscriptionId limits the parent. id is parentType~parentId~paymentId. Stripe ids are kept. Card numbers, CVC, expiry, and last4 are removed. Default limit is 50.","operationId":"listPayments","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"name":"invoiceId","in":"query","required":false,"schema":{"type":"string"}},{"name":"estimateId","in":"query","required":false,"schema":{"type":"string"}},{"name":"subscriptionId","in":"query","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/PaymentRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/payments/{paymentId}":{"get":{"tags":["Payments"],"summary":"Get one payment","description":"paymentId is the composite id returned by the list.","operationId":"getPayment","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/products":{"get":{"tags":["Products"],"summary":"List the organization product catalog","description":"Read only. organizations/{orgId}/products. Default limit is 50. stripeProductId and stripePriceId are kept.","operationId":"listProducts","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/products/{productId}":{"get":{"tags":["Products"],"summary":"Get one catalog product","description":"","operationId":"getProduct","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"productId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/equipment":{"get":{"tags":["Equipment"],"summary":"List installed equipment","description":"Read only. organizations/{orgId}/installed_equipment. Default limit is 50.","operationId":"listEquipment","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/equipment/{equipmentId}":{"get":{"tags":["Equipment"],"summary":"Get one installed equipment item","description":"","operationId":"getEquipment","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"equipmentId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/service-visits":{"get":{"tags":["Service visits"],"summary":"List service visits","description":"Read only. organizations/{orgId}/jobs. Customer signatures are omitted. Default limit is 50.","operationId":"listServiceVisits","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/service-visits/{visitId}":{"get":{"tags":["Service visits"],"summary":"Get one service visit","description":"","operationId":"getServiceVisit","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"visitId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/service-reports":{"get":{"tags":["Service reports"],"summary":"List service reports","description":"Read only projection of service visits that are completed, sending a report, or already have report fields. Default limit is 50.","operationId":"listServiceReports","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ServiceReport"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/organizations/{orgId}/service-reports/{visitId}":{"get":{"tags":["Service reports"],"summary":"Get one service report","description":"","operationId":"getServiceReport","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"visitId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/organizations/{orgId}/technicians":{"get":{"tags":["Technicians"],"summary":"List technicians","description":"Legacy alias of /v1/organizations/{orgId}/technicians. Prefer the canonical path. Any present status query (the value is ignored) returns only technicians whose status is pending. Omit status to return every technician.","operationId":"listTechniciansAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/technicianStatus"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Technician"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/technicians","x-legacy-alias":true}},"/organizations/{orgId}/technicians/{techId}/status":{"post":{"tags":["Technicians"],"summary":"Set a technician status","description":"Legacy alias of /v1/organizations/{orgId}/technicians/{techId}/status. Prefer the canonical path. Owners and admins only. status must be active, inactive, or rejected. approved and any other value return 400. active sets the technician user's organizationId, technicianId, and organizationStatus to active, the same fields processTechnicianApproval writes. inactive and rejected clear those three fields when they point at this technician, and leave the organization ownerUserId profile unchanged.","operationId":"updateTechnicianStatusAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"techId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TechnicianStatusRequest"}}}},"x-canonical-path":"/v1/organizations/{orgId}/technicians/{techId}/status","x-legacy-alias":true}},"/organizations/{orgId}/pools":{"get":{"tags":["Organization pools"],"summary":"List organization pools","description":"Legacy alias of /v1/organizations/{orgId}/pools. Prefer the canonical path. Optional assigned query is compared to the isAssigned field as a string, not a boolean. location and weather are omitted.","operationId":"listOrganizationPoolsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/assigned"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Pool"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools","x-legacy-alias":true},"post":{"tags":["Organization pools"],"summary":"Create an organization pool","description":"Legacy alias of /v1/organizations/{orgId}/pools. Prefer the canonical path. Owners and admins only. Active technicians receive 403. Idempotency-Key is required. Clients that used to call this without the header, or as a technician, now receive 400 or 403. Same body as user pool create. The server sets organizationId from the path and userId from the API key's effective user. supplimentalSanitizers is an intentional legacy field name. Do not rename it. cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","operationId":"createOrganizationPoolAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolCreateRequest"}}}},"x-canonical-path":"/v1/organizations/{orgId}/pools","x-legacy-alias":true}},"/organizations/{orgId}/pools/{poolId}":{"get":{"tags":["Organization pools"],"summary":"Get one organization pool","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}. Prefer the canonical path. ","operationId":"getOrganizationPoolAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}","x-legacy-alias":true},"delete":{"tags":["Organization pools"],"summary":"Soft-delete an organization pool","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}. Prefer the canonical path. Owners and admins only. Active technicians receive 403.","operationId":"deleteOrganizationPoolAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessFlag"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}","x-legacy-alias":true},"patch":{"tags":["Organization pools"],"summary":"Update an organization pool","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}. Prefer the canonical path. Owners and admins only. Same allowed fields as the user pool update. Active technicians receive 403.","operationId":"patchOrganizationPoolAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pool"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolPatch"}}}},"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}","x-legacy-alias":true}},"/organizations/{orgId}/pools/{poolId}/logs":{"get":{"tags":["Organization logs"],"summary":"List logs for an organization pool","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}/logs. Prefer the canonical path. Same paging rules as user logs. Default limit 50. cursor and startAfter are dates. type is filtered after the page is read. Empty is null unless envelope=1.","operationId":"listOrganizationLogsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Log"}},{"type":"null"}]},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}/logs","x-legacy-alias":true},"post":{"tags":["Organization logs"],"summary":"Create an organization pool log","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}/logs. Prefer the canonical path. Owners, admins, and active technicians. Query type is required. Body is the log object. The 201 data field is the new log id.","operationId":"createOrganizationLogAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/logType"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedLog"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogCreate"}}}},"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}/logs","x-legacy-alias":true}},"/organizations/{orgId}/pools/{poolId}/logs/{logId}":{"get":{"tags":["Organization logs"],"summary":"Get one organization pool log","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}. Prefer the canonical path. ","operationId":"getOrganizationLogAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}/logs/{logId}","x-legacy-alias":true},"delete":{"tags":["Organization logs"],"summary":"Delete an organization pool log","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}. Prefer the canonical path. Owners, admins, and active technicians.","operationId":"deleteOrganizationLogAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessMessage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}/logs/{logId}","x-legacy-alias":true},"patch":{"tags":["Organization logs"],"summary":"Update an organization pool log","description":"Legacy alias of /v1/organizations/{orgId}/pools/{poolId}/logs/{logId}. Prefer the canonical path. Owners, admins, and active technicians. Allowed fields: note, creatorName, chemicalReading, equipmentReading.","operationId":"patchOrganizationLogAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"poolId","in":"path","required":true,"schema":{"type":"string"}},{"name":"logId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Log"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogPatch"}}}},"x-canonical-path":"/v1/organizations/{orgId}/pools/{poolId}/logs/{logId}","x-legacy-alias":true}},"/organizations/{orgId}/import/csv":{"post":{"tags":["Customers"],"summary":"Import customers and pools from CSV","description":"Legacy alias of /v1/organizations/{orgId}/import/csv. Prefer the canonical path. Owners and admins only. Active technicians receive 403. REST form of the customers-importCsv callable. Send JSON { csvText, mapping, defaults } or a raw text/csv body. filterCategory values are stored as given. The intentional legacy spelling cartrige is valid here and must not be renamed.","operationId":"importOrganizationCustomersCsvAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvImportResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvImportRequest"}},"text/csv":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}}},"x-canonical-path":"/v1/organizations/{orgId}/import/csv","x-legacy-alias":true}},"/organizations/{orgId}/customers":{"get":{"tags":["Customers"],"summary":"List organization customers","description":"Legacy alias of /v1/organizations/{orgId}/customers. Prefer the canonical path. Read only. Documents live at organizations/{orgId}/customers. A document whose organizationId field is a different org is omitted. Default limit is 50. Card data and secrets are removed. Owners and admins receive billing fields, including stripeCustomerId, balances, payment methods, and autopay. Active technicians receive the service fields (name, phone, email, address, notes, pools) and those billing fields are omitted.","operationId":"listCustomersAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/customers","x-legacy-alias":true},"post":{"tags":["Customers"],"summary":"Create an organization customer","description":"Legacy alias of /v1/organizations/{orgId}/customers. Prefer the canonical path. Owners and admins only. name is required. email, phone, and notes are optional. Billing fields, payment methods, and Stripe ids are rejected. Idempotency-Key is required. No Stripe call is made.","operationId":"createCustomerAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreate"}}}},"x-canonical-path":"/v1/organizations/{orgId}/customers","x-legacy-alias":true}},"/organizations/{orgId}/customers/{customerId}":{"get":{"tags":["Customers"],"summary":"Get one organization customer","description":"Legacy alias of /v1/organizations/{orgId}/customers/{customerId}. Prefer the canonical path. ","operationId":"getCustomerAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"customerId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/customers/{customerId}","x-legacy-alias":true}},"/organizations/{orgId}/routes":{"get":{"tags":["Routes"],"summary":"List routes","description":"Legacy alias of /v1/organizations/{orgId}/routes. Prefer the canonical path. Read only. A route is the set of active stops for one technician on one local calendar date, derived from organizations/{orgId}/stops. It is not a separate stored collection. The organization document has no required timezone. date uses organization.timeZone or organization.timezone when that value is an IANA name such as America/Los_Angeles. Otherwise it uses organization.timeZoneOffset or organization.timezoneOffset, in minutes, local minus UTC (the same convention as job.timeZoneOffset; PDT is -420). Abbreviations such as PDT are ignored. If none of those fields are present, or the name is not a valid IANA zone, the date is UTC and timeZoneSource is utc-fallback. id is YYYY-MM-DD:technicianUserId, or YYYY-MM-DD:unassigned. Default limit is 50.","operationId":"listRoutesAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"name":"date","in":"query","required":false,"description":"Local calendar date YYYY-MM-DD in the organization time zone. UTC when the organization has no usable zone.","schema":{"type":"string"}},{"name":"technicianUserId","in":"query","required":false,"description":"Only stops assigned to this technician user id.","schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/RouteDay"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/routes","x-legacy-alias":true}},"/organizations/{orgId}/routes/{routeId}":{"get":{"tags":["Routes"],"summary":"Get one route","description":"Legacy alias of /v1/organizations/{orgId}/routes/{routeId}. Prefer the canonical path. routeId is the list id, for example 2026-10-15:technicianUserId.","operationId":"getRouteAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteDay"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/routes/{routeId}","x-legacy-alias":true}},"/organizations/{orgId}/stops":{"get":{"tags":["Stops"],"summary":"List route stops","description":"Legacy alias of /v1/organizations/{orgId}/stops. Prefer the canonical path. Read only. organizations/{orgId}/stops. Default limit is 50. Deleted stops are included; clients can filter isDeleted.","operationId":"listStopsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/stops","x-legacy-alias":true},"post":{"tags":["Stops"],"summary":"Create a route stop","description":"Legacy alias of /v1/organizations/{orgId}/stops. Prefer the canonical path. Owners, admins, and active technicians. poolId and scheduledDateTime are required. The pool must belong to this organization. The stored stop includes pool, using the app shape (chemicalCategory, indoor, id) plus poolId, and customerId and locationId when the pool has them. technicianUserIds and duration are optional. serviceCost and other billing fields are rejected. Idempotency-Key is required. No routing or Stripe call is made.","operationId":"createStopAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"},{"$ref":"#/components/parameters/actAs"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StopCreate"}}}},"x-canonical-path":"/v1/organizations/{orgId}/stops","x-legacy-alias":true}},"/organizations/{orgId}/stops/{stopId}":{"get":{"tags":["Stops"],"summary":"Get one route stop","description":"Legacy alias of /v1/organizations/{orgId}/stops/{stopId}. Prefer the canonical path. ","operationId":"getStopAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stopId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/stops/{stopId}","x-legacy-alias":true}},"/organizations/{orgId}/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices","description":"Legacy alias of /v1/organizations/{orgId}/invoices. Prefer the canonical path. Read only. organizations/{orgId}/invoices. Owners and admins only. An active technician receives 403. Default limit is 50. Card data is removed. Stripe ids are kept.","operationId":"listInvoicesAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/invoices","x-legacy-alias":true}},"/organizations/{orgId}/invoices/{invoiceId}":{"get":{"tags":["Invoices"],"summary":"Get one invoice","description":"Legacy alias of /v1/organizations/{orgId}/invoices/{invoiceId}. Prefer the canonical path. ","operationId":"getInvoiceAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"invoiceId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/invoices/{invoiceId}","x-legacy-alias":true}},"/organizations/{orgId}/estimates":{"get":{"tags":["Estimates"],"summary":"List estimates","description":"Legacy alias of /v1/organizations/{orgId}/estimates. Prefer the canonical path. Owners and admins only. Active technicians receive 403. Read only. organizations/{orgId}/estimates. Default limit is 50.","operationId":"listEstimatesAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/estimates","x-legacy-alias":true}},"/organizations/{orgId}/estimates/{estimateId}":{"get":{"tags":["Estimates"],"summary":"Get one estimate","description":"Legacy alias of /v1/organizations/{orgId}/estimates/{estimateId}. Prefer the canonical path. Owners and admins only. Active technicians receive 403.","operationId":"getEstimateAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"estimateId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/estimates/{estimateId}","x-legacy-alias":true}},"/organizations/{orgId}/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"List service subscriptions","description":"Legacy alias of /v1/organizations/{orgId}/subscriptions. Prefer the canonical path. Owners and admins only. Active technicians receive 403. Read only. organizations/{orgId}/service_subscriptions. These are organization service billing subscriptions, not Pool Club subscriptions. Default limit is 50.","operationId":"listServiceSubscriptionsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/subscriptions","x-legacy-alias":true}},"/organizations/{orgId}/subscriptions/{subscriptionId}":{"get":{"tags":["Subscriptions"],"summary":"Get one service subscription","description":"Legacy alias of /v1/organizations/{orgId}/subscriptions/{subscriptionId}. Prefer the canonical path. Owners and admins only. Active technicians receive 403.","operationId":"getServiceSubscriptionAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/subscriptions/{subscriptionId}","x-legacy-alias":true}},"/organizations/{orgId}/payments":{"get":{"tags":["Payments"],"summary":"List payments","description":"Legacy alias of /v1/organizations/{orgId}/payments. Prefer the canonical path. Read only. Owners and admins only. An active technician receives 403. Reads payment_history under this organization's invoices, estimates, and service subscriptions. Optional invoiceId, estimateId, or subscriptionId limits the parent. id is parentType~parentId~paymentId. Stripe ids are kept. Card numbers, CVC, expiry, and last4 are removed. Default limit is 50.","operationId":"listPaymentsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"name":"invoiceId","in":"query","required":false,"schema":{"type":"string"}},{"name":"estimateId","in":"query","required":false,"schema":{"type":"string"}},{"name":"subscriptionId","in":"query","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/PaymentRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/payments","x-legacy-alias":true}},"/organizations/{orgId}/payments/{paymentId}":{"get":{"tags":["Payments"],"summary":"Get one payment","description":"Legacy alias of /v1/organizations/{orgId}/payments/{paymentId}. Prefer the canonical path. paymentId is the composite id returned by the list.","operationId":"getPaymentAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/payments/{paymentId}","x-legacy-alias":true}},"/organizations/{orgId}/products":{"get":{"tags":["Products"],"summary":"List the organization product catalog","description":"Legacy alias of /v1/organizations/{orgId}/products. Prefer the canonical path. Read only. organizations/{orgId}/products. Default limit is 50. stripeProductId and stripePriceId are kept.","operationId":"listProductsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/products","x-legacy-alias":true}},"/organizations/{orgId}/products/{productId}":{"get":{"tags":["Products"],"summary":"Get one catalog product","description":"Legacy alias of /v1/organizations/{orgId}/products/{productId}. Prefer the canonical path. ","operationId":"getProductAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"productId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/products/{productId}","x-legacy-alias":true}},"/organizations/{orgId}/equipment":{"get":{"tags":["Equipment"],"summary":"List installed equipment","description":"Legacy alias of /v1/organizations/{orgId}/equipment. Prefer the canonical path. Read only. organizations/{orgId}/installed_equipment. Default limit is 50.","operationId":"listEquipmentAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/equipment","x-legacy-alias":true}},"/organizations/{orgId}/equipment/{equipmentId}":{"get":{"tags":["Equipment"],"summary":"Get one installed equipment item","description":"Legacy alias of /v1/organizations/{orgId}/equipment/{equipmentId}. Prefer the canonical path. ","operationId":"getEquipmentAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"equipmentId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/equipment/{equipmentId}","x-legacy-alias":true}},"/organizations/{orgId}/service-visits":{"get":{"tags":["Service visits"],"summary":"List service visits","description":"Legacy alias of /v1/organizations/{orgId}/service-visits. Prefer the canonical path. Read only. organizations/{orgId}/jobs. Customer signatures are omitted. Default limit is 50.","operationId":"listServiceVisitsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/OrgRecord"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/service-visits","x-legacy-alias":true}},"/organizations/{orgId}/service-visits/{visitId}":{"get":{"tags":["Service visits"],"summary":"Get one service visit","description":"Legacy alias of /v1/organizations/{orgId}/service-visits/{visitId}. Prefer the canonical path. ","operationId":"getServiceVisitAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"visitId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgRecord"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/service-visits/{visitId}","x-legacy-alias":true}},"/organizations/{orgId}/service-reports":{"get":{"tags":["Service reports"],"summary":"List service reports","description":"Legacy alias of /v1/organizations/{orgId}/service-reports. Prefer the canonical path. Read only projection of service visits that are completed, sending a report, or already have report fields. Default limit is 50.","operationId":"listServiceReportsAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/startAfter"},{"$ref":"#/components/parameters/envelope"},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Default body is the collection JSON (array, or null for empty logs and empty inventory). envelope=1 returns ListEnvelope instead. X-Next-Cursor, X-Has-More, and X-Page-Limit describe the page.","headers":{"X-Next-Cursor":{"schema":{"type":"string"},"description":"Present when another page is available."},"X-Has-More":{"schema":{"type":"string","enum":["true","false"]}},"X-Page-Limit":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ServiceReport"}},{"$ref":"#/components/schemas/ListEnvelope"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/service-reports","x-legacy-alias":true}},"/organizations/{orgId}/service-reports/{visitId}":{"get":{"tags":["Service reports"],"summary":"Get one service report","description":"Legacy alias of /v1/organizations/{orgId}/service-reports/{visitId}. Prefer the canonical path. ","operationId":"getServiceReportAlias","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"visitId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAs"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"x-canonical-path":"/v1/organizations/{orgId}/service-reports/{visitId}","x-legacy-alias":true}},"/v1/content-ingest":{"post":{"tags":["Content ingest"],"summary":"Create a content ingest item","description":"Service route. Bearer API key. If the key document has a non-empty scopes array, it must include content_management. Keys with no scopes array are allowed. Not an organization-scoped route.","operationId":"createContentIngest","parameters":[{"$ref":"#/components/parameters/actAsContent"}],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentIngestResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentIngestCreate"}}}}}},"/v1/content-ingest/{id}":{"patch":{"tags":["Content ingest"],"summary":"Patch a content ingest item","description":"Partial update. Same service key rules as create.","operationId":"patchContentIngest","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/actAsContent"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentIngestResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentIngestPatch"}}}}}},"/v1/auth/token":{"post":{"tags":["Authentication"],"summary":"Exchange a long-lived API key for a short-lived access token","description":"Authorization: Bearer <long-lived API key>. The key is not changed. Default lifetime is 900 seconds. expiresInSeconds may be from 60 to 3600. Send the returned accessToken as Bearer on later calls. Existing clients that send the long-lived key directly keep working.","operationId":"exchangeAccessToken","parameters":[],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenExchangeRequest"}}}}}},"/v1/auth/token/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh a short-lived access token","description":"Authorization: Bearer <unexpired access token>. Issues a new token for the same API key and deletes the presented token. An expired token cannot be refreshed. A revoked or regenerated key cannot refresh.","operationId":"refreshAccessToken","parameters":[],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenExchangeRequest"}}}}}},"/v1/auth/keys/regenerate":{"post":{"tags":["Authentication"],"summary":"Revoke short-lived tokens for this API key","description":"Authorization: Bearer <long-lived API key>. Increments the key generation and deletes apiTokens for that key. The long-lived key string is not changed. Use POST /v1/auth/keys/rotate to issue a new key. Outstanding access tokens fail. Deleting the key with apiKeys-deleteApiKey also revokes its tokens.","operationId":"regenerateApiKeyTokens","parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRegeneration"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}]}},"/v1/auth/keys/rotate":{"post":{"tags":["Authentication"],"summary":"Issue a new API key and start the grace period on this key","description":"Authorization: Bearer <long-lived API key>. Creates a new apiKeys document for the same user and returns its secret once. The presented key, or apiKeyId when that key belongs to the same user, stays valid until graceEndsAt. Default grace is 7 days. gracePeriodDays may be 0 through 90. Zero revokes the old key immediately and deletes its access tokens. A non-zero grace leaves those tokens valid until the old key is revoked. expiresAt on the new key is null and is not enforced. apiKeys-rotateApiKey is the signed-in callable for the same behavior.","operationId":"rotateApiKey","parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRotation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRotationRequest"}}}}}},"/v1/auth/keys/revoke":{"post":{"tags":["Authentication"],"summary":"Revoke an API key before its grace period ends","description":"Authorization: Bearer <long-lived API key>. Revokes that key, or apiKeyId when it belongs to the same user. Sets status revoked, bumps generation, and deletes apiTokens minted from the revoked key. A key past graceEndsAt is revoked the same way the next time it or one of its tokens is used. apiKeys-revokeApiKey is the signed-in callable. apiKeys-deleteApiKey still deletes the key document after its tokens are removed.","operationId":"revokeApiKey","parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRevocation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRevokeRequest"}}}}}},"/openapi.json":{"get":{"tags":["Documentation"],"summary":"OpenAPI document","operationId":"getOpenApi","security":[],"responses":{"200":{"description":"OpenAPI 3.0 document","content":{"application/json":{"schema":{"type":"object","description":"This document."}}}}}}},"/v1/openapi.json":{"get":{"tags":["Documentation"],"summary":"OpenAPI document (versioned alias)","operationId":"getOpenApiV1","security":[],"responses":{"200":{"description":"OpenAPI 3.0 document","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/docs":{"get":{"tags":["Documentation"],"summary":"API documentation page","description":"HTML page that links to /openapi.json and states the canonical paths, development host, error format, pagination, and API key rules.","operationId":"getDocs","security":[],"responses":{"200":{"description":"HTML documentation","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/v1/docs":{"get":{"tags":["Documentation"],"summary":"API documentation page (versioned alias)","operationId":"getDocsV1","security":[],"responses":{"200":{"description":"HTML documentation","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Long-lived API key from apiKeys-generateApiKey, or a short-lived access token from POST /v1/auth/token. Send Authorization: Bearer <key or access token>."}},"parameters":{"actAs":{"name":"X-Pooli-Act-As","in":"header","required":false,"description":"User id to run this organization request as. Only the organization owner may name someone else. The target must belong to this organization. A higher role is rejected. The effective role is the target user's role, including billing visibility. Omit the header, or send the caller's own id, to use the API key user. If X-Pooli-Organization is also sent, it must equal the organization id in the path.","schema":{"type":"string"}},"actAsRejected":{"name":"X-Pooli-Act-As","in":"header","required":false,"description":"Rejected with 403 when this names a user other than the API key user. Personal /v1/users routes do not support acting as another user. The path uid must be the key user.","schema":{"type":"string"}},"actAsContent":{"name":"X-Pooli-Act-As","in":"header","required":false,"description":"Rejected with 403. Content ingest does not support acting as another user.","schema":{"type":"string"}},"idempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"description":"8 to 200 characters. Required when creating a customer, organization pool, stop, inventory item, or reminder. The same key and JSON body replay the original status and body. A different body returns 409. The role check runs before the key is recorded. A failed validation does not store a replay.","schema":{"type":"string","minLength":8,"maxLength":200}},"limit":{"name":"limit","in":"query","required":false,"description":"Page size. Positive integer, maximum 200. Logs and the organization business reads (customers, routes, stops, invoices, estimates, subscriptions, payments, products, equipment, service visits, service reports) default to 50 when omitted. Pools, inventory, reminders, and technicians return the full array when omitted.","schema":{"type":"integer","minimum":1,"maximum":200}},"cursor":{"name":"cursor","in":"query","required":false,"description":"Page token. Logs: date string (same as startAfter). Other lists: id of the last item from the previous page.","schema":{"type":"string"}},"startAfter":{"name":"startAfter","in":"query","required":false,"description":"Alias of cursor. Existing log clients already send this.","schema":{"type":"string"}},"envelope":{"name":"envelope","in":"query","required":false,"description":"Pass 1 or true to wrap the list as { data, page: { limit, nextCursor, hasMore } }. Omit to keep the historical JSON array (or null).","schema":{"type":"string","enum":["1","true"]}},"logType":{"name":"type","in":"query","required":false,"description":"Log type. Required on create. On list, filtered after the page is read. Implemented types: test, product, maintenance, note, water, temperature, pressure, swg, feeder.","schema":{"type":"string","enum":["test","product","maintenance","note","water","temperature","pressure","swg","feeder"]}},"technicianStatus":{"name":"status","in":"query","required":false,"description":"If this parameter is present, only pending technicians are returned. The value is not matched.","schema":{"type":"string"}},"assigned":{"name":"assigned","in":"query","required":false,"description":"Compared as a string to the pool isAssigned field. Example: true matches the string true, not a boolean.","schema":{"type":"string"}}},"responses":{"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or malformed bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Key rejected, uid mismatch, or not allowed for the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Rate limit exceeded for a key without an active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error","message","status"],"description":"Every error response uses this object. message is the human-readable text. error is a short title; for responses that used to be plain text, error and message are the same string.","properties":{"error":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"details":{},"position":{"type":"string"},"requiredScope":{"type":"string"}}},"Page":{"type":"object","required":["hasMore"],"properties":{"limit":{"type":"integer","nullable":true},"nextCursor":{"type":"string","nullable":true},"hasMore":{"type":"boolean"}}},"ListEnvelope":{"type":"object","required":["data","page"],"properties":{"data":{"type":"array","items":{}},"page":{"$ref":"#/components/schemas/Page"}}},"SuccessFlag":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}},"SuccessMessage":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}},"CreatedLog":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"},"data":{"type":"string","description":"New log document id."}}},"Pool":{"type":"object","description":"Pool document. List responses omit location and weather. supplimentalSanitizers is an intentional legacy field name. Do not rename it.","additionalProperties":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"userId":{"type":"string"},"organizationId":{"type":"string"},"poolType":{"type":"string","enum":["vinyl","plaster","fiberglass"]},"filterCategory":{"type":"string","description":"cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge."},"sanitizingChemical":{"type":"string"},"gallonEstimate":{"type":"number"},"isIndoor":{"type":"boolean"},"supplimentalSanitizers":{"type":"array","description":"supplimentalSanitizers is an intentional legacy field name. Do not rename it.","items":{"type":"string","enum":["mineral","ozone","uv","ionize","aop","enzyme","unknown"]}},"deleted":{"type":"boolean"}}},"PoolCreate":{"type":"object","required":["name","gallonEstimate"],"description":"supplimentalSanitizers is an intentional legacy field name. Do not rename it. cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","properties":{"name":{"type":"string"},"poolType":{"type":"string","enum":["vinyl","plaster","fiberglass"],"default":"plaster"},"filterCategory":{"type":"string","description":"cartrige is an intentional legacy spelling used by the app, by CSV import, and as the value stored when filterCategory is omitted on pool create. Do not rename it. When a client sends filterCategory, the create-pool validator currently accepts de, sand, and cartridge.","enum":["de","sand","cartridge"]},"sanitizingChemical":{"type":"string","enum":["chlorine","bromine","swg","biguanides","peroxide","natural","freshwater","mineral"],"default":"chlorine"},"gallonEstimate":{"type":"number"},"isIndoor":{"type":"boolean","default":false},"supplimentalSanitizers":{"type":"array","description":"Intentional legacy spelling of supplemental sanitizers. Do not rename this field.","items":{"type":"string","enum":["mineral","ozone","uv","ionize","aop","enzyme","unknown"]},"default":[]}}},"PoolPatch":{"type":"object","description":"At least one field. supplimentalSanitizers is the intentional legacy spelling. Do not rename it.","properties":{"name":{"type":"string"},"gallonEstimate":{"type":"number"},"poolType":{"type":"string","enum":["vinyl","plaster","fiberglass"]},"filterCategory":{"type":"string","enum":["de","sand","cartridge"],"description":"Sent values accept de, sand, and cartridge. The stored default spelling cartrige is unchanged and must not be renamed."},"sanitizingChemical":{"type":"string"},"isIndoor":{"type":"boolean"},"supplimentalSanitizers":{"type":"array","items":{"type":"string"},"description":"Intentional legacy spelling of supplemental sanitizers. Do not rename this field."}}},"LogPatch":{"type":"object","properties":{"note":{"type":"string"},"creatorName":{"type":"string"},"chemicalReading":{"type":"object","additionalProperties":true},"equipmentReading":{"type":"object","additionalProperties":true}}},"InventoryWrite":{"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"notes":{"type":"string"}}},"ReminderWrite":{"type":"object","required":["reminderType"],"properties":{"reminderType":{"type":"string"},"message":{"type":"string"},"dueDate":{"type":"string","format":"date-time"}}},"CustomerCreate":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"notes":{"type":"string"}}},"StopCreate":{"type":"object","required":["poolId","scheduledDateTime"],"properties":{"poolId":{"type":"string"},"scheduledDateTime":{"type":"string","format":"date-time"},"technicianUserIds":{"type":"array","items":{"type":"string"}},"duration":{"type":"integer"}}},"PoolCreateRequest":{"type":"object","required":["pool"],"properties":{"pool":{"$ref":"#/components/schemas/PoolCreate"}}},"ChemicalReading":{"type":"object","additionalProperties":true,"properties":{"totalChlorine":{"type":"number","nullable":true},"freeChlorine":{"type":"number","nullable":true},"totalBromine":{"type":"number","nullable":true},"peroxide":{"type":"number","nullable":true},"ph":{"type":"number","nullable":true},"temp":{"type":"number","nullable":true},"cyanuricAcid":{"type":"number","nullable":true},"calcium":{"type":"number","nullable":true},"cch":{"type":"number","nullable":true},"salt":{"type":"number","nullable":true},"alkalinity":{"type":"number","nullable":true},"phosphates":{"type":"number","nullable":true},"totalDissolvedSolids":{"type":"number","nullable":true},"combinedChlorine":{"type":"number","nullable":true},"copper":{"type":"number","nullable":true},"borates":{"type":"number","nullable":true},"iron":{"type":"number","nullable":true},"orp":{"type":"number","nullable":true},"biguanide":{"type":"number","nullable":true},"swgProduction":{"type":"number","nullable":true},"testMethod":{"type":"string","nullable":true}}},"EquipmentReading":{"type":"object","description":"Optional readings nested on a test log. Not a standalone equipment resource.","properties":{"otherMethod":{"type":"string","nullable":true},"swgPercent":{"type":"number","nullable":true},"observation":{"type":"string","nullable":true},"filterPressure":{"type":"number","nullable":true},"testMethod":{"type":"string","nullable":true}}},"Log":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"type":{"type":"string"},"creatorName":{"type":"string"},"createdAt":{},"updatedAt":{},"chemicalReading":{"$ref":"#/components/schemas/ChemicalReading"},"equipmentReading":{"$ref":"#/components/schemas/EquipmentReading"}}},"LogCreate":{"type":"object","description":"Shape depends on the type query. Common optional fields: creatorName, createdAt, updatedAt, includeReminders.","additionalProperties":true,"properties":{"creatorName":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"includeReminders":{"type":"boolean"},"chemicalReading":{"$ref":"#/components/schemas/ChemicalReading"},"equipmentReading":{"$ref":"#/components/schemas/EquipmentReading"},"scan":{"type":"object","nullable":true},"targets":{"type":"object"},"gallonEstimate":{"type":"number"},"amount":{"type":"number"},"productName":{"type":"string"},"unitType":{"type":"string"},"form":{"type":"string"},"productType":{"type":"string"},"solution":{"type":"object"},"maintenances":{"type":"array","items":{"type":"object"}},"note":{"type":"string"},"attachments":{"type":"object","nullable":true},"temperature":{"type":"number","nullable":true},"gallons":{"type":"number","nullable":true},"gallonsAddition":{"type":"number","nullable":true},"pressure":{"type":"number","nullable":true},"rpm":{"type":"number","nullable":true},"flowRate":{"type":"number","nullable":true},"trackingUnit":{"type":"string","nullable":true},"swgPercentage":{"type":"number","nullable":true},"startPercentage":{"type":"number","nullable":true},"duration":{"type":"number","nullable":true},"feederConfigId":{"type":"string"},"feederName":{"type":"string"},"setting":{"type":"number"},"previousSetting":{"type":"number","nullable":true}}},"Reminder":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"poolId":{"type":"string"}}},"InventoryItem":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"}}},"Technician":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"userId":{"type":"string"},"status":{"type":"string","description":"App values include pending, active, rejected, inactive."}}},"TechnicianStatusRequest":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["active","inactive","rejected"],"description":"Write values are active, inactive, and rejected. approved and any other value return 400. Stored technician documents may still show pending."},"message":{"type":"string"}}},"CsvImportRequest":{"type":"object","required":["csvText"],"properties":{"csvText":{"type":"string"},"mapping":{"type":"object","additionalProperties":true},"defaults":{"type":"object","description":"Optional defaults. filterCategory may be the legacy spelling cartrige.","additionalProperties":true}}},"CsvImportResult":{"type":"object","properties":{"headers":{"type":"array","items":{"type":"string"}},"counts":{"type":"object","additionalProperties":true},"sampleErrors":{"type":"array","items":{}},"jobId":{"type":"string"}}},"ContentIngestCreate":{"type":"object","additionalProperties":true,"description":"Same payload as the ingestContent callable."},"ContentIngestPatch":{"type":"object","additionalProperties":true},"TokenExchangeRequest":{"type":"object","description":"Optional. Omit expiresInSeconds for a 900 second token. The body may be an empty object.","properties":{"expiresInSeconds":{"type":"integer","minimum":60,"maximum":3600}}},"AccessToken":{"type":"object","required":["accessToken","tokenType","expiresAt","expiresInSeconds"],"properties":{"accessToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresAt":{"type":"string","format":"date-time"},"expiresInSeconds":{"type":"integer"}}},"KeyRegeneration":{"type":"object","required":["generation","tokensRevoked","keyRotated"],"properties":{"generation":{"type":"integer"},"tokensRevoked":{"type":"integer"},"keyRotated":{"type":"boolean","description":"Always false. Regenerate does not issue a new key string. POST /keys/rotate does."}}},"KeyRotationRequest":{"type":"object","description":"Optional. Omit gracePeriodDays for 7 days. apiKeyId defaults to the Bearer key and must belong to that same user.","properties":{"gracePeriodDays":{"type":"integer","minimum":0,"maximum":90},"apiKeyId":{"type":"string"}}},"KeyRotation":{"type":"object","required":["apiKey","previousKeyId","gracePeriodDays","graceEndsAt","status"],"properties":{"apiKey":{"type":"string","description":"New long-lived key secret. Store it. The response is the only place the new secret is required."},"previousKeyId":{"type":"string"},"gracePeriodDays":{"type":"integer"},"graceEndsAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["rotating","revoked"]},"tokensRevoked":{"type":"integer"}}},"KeyRevokeRequest":{"type":"object","description":"Optional. apiKeyId defaults to the Bearer key and must belong to that same user.","properties":{"apiKeyId":{"type":"string"}}},"KeyRevocation":{"type":"object","required":["apiKeyId","status"],"properties":{"apiKeyId":{"type":"string"},"status":{"type":"string","enum":["revoked"]},"tokensRevoked":{"type":"integer"},"generation":{"type":"integer"},"alreadyRevoked":{"type":"boolean"}}},"ContentIngestResult":{"type":"object","properties":{"id":{"type":"string"},"item":{"type":"object","additionalProperties":true}}},"OrgRecord":{"type":"object","additionalProperties":true,"description":"Organization document after redaction. Card numbers, CVC, expiry, last4, customer signatures, and secret fields are removed. stripe*Id fields are kept. organizationId is the organization in the path. A stored organizationId for a different organization is omitted entirely.","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"stripeCustomerId":{"type":"string"},"stripeProductId":{"type":"string"},"stripePriceId":{"type":"string"}}},"RouteDay":{"type":"object","additionalProperties":false,"description":"Active stops for one technician on one local calendar date, derived from stops. Not a stored collection. id is YYYY-MM-DD:technicianUserId, or YYYY-MM-DD:unassigned. date follows the organization time zone, or UTC when that zone is missing.","required":["id","organizationId","date","stopIds","timeZoneSource"],"properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"date":{"type":"string","description":"Calendar date YYYY-MM-DD in the organization time zone, or UTC when timeZoneSource is utc-fallback."},"technicianUserId":{"type":"string","nullable":true},"scheduleId":{"type":"string","nullable":true},"stopIds":{"type":"array","items":{"type":"string"}},"timeZone":{"type":"string","nullable":true,"description":"IANA zone from organization.timeZone or organization.timezone. UTC when the fallback is used. Omitted when a numeric offset is used instead."},"timeZoneOffsetMinutes":{"type":"integer","nullable":true,"description":"Present when the organization has timeZoneOffset or timezoneOffset and no IANA zone. Local minus UTC. PDT is -420."},"timeZoneSource":{"type":"string","enum":["organization.timeZone","organization.timezone","organization.timeZoneOffset","organization.timezoneOffset","utc-fallback"]}}},"PaymentRecord":{"type":"object","additionalProperties":true,"description":"payment_history row. id is parentType~parentId~paymentId. Amount, status, notes, paymentMethod, and stripe*Id fields are kept. Card numbers, CVC, expiry, brand, and last4 are removed.","properties":{"id":{"type":"string"},"paymentId":{"type":"string"},"parentType":{"type":"string","enum":["invoice","estimate","subscription"]},"parentId":{"type":"string"},"organizationId":{"type":"string"},"amount":{"type":"number"},"status":{"type":"string"},"paymentMethod":{"type":"string"},"notes":{"type":"string"},"stripePaymentIntentId":{"type":"string"},"stripeChargeId":{"type":"string"}}},"ServiceReport":{"type":"object","additionalProperties":true,"description":"Projection of a service visit that is completed, sending a report, or already has report fields. customerSignature is omitted. Photo arrays are included. photoCount is their combined length.","properties":{"id":{"type":"string"},"visitId":{"type":"string"},"organizationId":{"type":"string"},"poolId":{"type":"string"},"status":{"type":"string"},"notes":{"type":"string"},"reportLinkUrl":{"type":"string"},"reportMessageId":{"type":"string"},"reportGeneratedAt":{},"photoCount":{"type":"integer"},"technicianUserIds":{"type":"array","items":{"type":"string"}},"beforePhotos":{"type":"array","items":{}},"afterPhotos":{"type":"array","items":{}},"gatePhotos":{"type":"array","items":{}}}}}},"x-pooli-api-keys":{"creation":"Created in the Pooli app via Firebase callable apiKeys-generateApiKey while the user is signed in. The callable writes apiKeys/{token} with uid, token, counters, generation 0, status active, and expiresAt null. expiresAt is reserved for a future maximum lifetime and is not enforced. Web dashboard organization key management is not this API.","scoping":"The key is the user in apiKeys.uid. User routes require the path uid to equal that uid. Organization routes resolve access as owner (ownerUserId), admin (organization.admins or members/{uid}.role admin), or a technicians document for that userId with status active. Billing reads, including invoices, payments, estimates, and service subscriptions, require owner or admin. No separate organization credential is minted. Fine-grained scopes are not applied on user or organization routes. content_management is consulted only by /v1/content-ingest, and only when scopes is a non-empty array.","regenerateOnRequest":"POST /v1/auth/keys/regenerate and a later generate call do not replace the key string. Regenerate only bumps generation and deletes that key's access tokens. That is not rotation.","rotate":"POST /v1/auth/keys/rotate and callable apiKeys-rotateApiKey issue a new key. The previous key stays active until graceEndsAt (default 7 days, body gracePeriodDays 0 through 90). gracePeriodDays 0 revokes the previous key immediately. After graceEndsAt, the next use of the old key or of a token minted from it sets status revoked and deletes those tokens.","revoke":"POST /v1/auth/keys/revoke and callable apiKeys-revokeApiKey revoke one key that belongs to the caller and delete its access tokens. apiKeys-deleteApiKey deletes the key document after removing its tokens.","list":"apiKeys-fetchApiKeys returns the signed-in user's keys, including status, graceEndsAt, successorKeyId, and expiresAt when those fields are set."},"x-legacy-spellings":{"supplimentalSanitizers":"Intentional field name on pool create and stored pool documents. Do not rename.","cartrige":"Intentional filterCategory spelling in the app, CSV import, and the create-pool default when the field is omitted. Do not rename."},"x-development-host":"https://dev-api.pooli.app","x-pooli-roles":{"reads":"Organization reads allow the owner, an admin, or an active technician. Billing reads (invoices, payments, estimates, service subscriptions, and billing fields on customers) are owner or admin.","structuralWrites":"Owner or admin: create a customer, create, update, or delete an organization pool, import CSV, and set technician status. Active technicians receive 403.","operationalWrites":"Owner, admin, or active technician: create a stop, and create, update, or delete an organization log.","userWrites":"The path uid must be the API key user. X-Pooli-Act-As naming someone else is rejected. Covers user pool create, update, and delete, user log create, update, and delete, inventory create and update, and reminder create and update.","technicianStatus":"POST /v1/organizations/{orgId}/technicians/{techId}/status accepts active, inactive, or rejected. approved and any other value return 400. Owners and admins only."}}