{"openapi":"3.1.0","info":{"title":"Every Home ERP API","version":"1.0.0","description":"Multi-tenant home-services ERP REST API with built-in scrypt + HS256 authentication, strict X-Tenant-Id isolation, role-based access control (viewer < employee < admin < owner), work orders, conflict-aware technician scheduling, cent-exact invoicing, and atomic audit logging."},"servers":[{"url":"https://support.myhomesolved.com"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Signed HS256 token returned by /api/auth/register or /api/auth/login"}},"parameters":{"TenantIdHeader":{"name":"X-Tenant-Id","in":"header","required":true,"description":"Positive integer tenant_id of an active tenant membership","schema":{"type":"integer","minimum":1}},"LimitParam":{"name":"limit","in":"query","required":false,"description":"Page size (default 50, max 500)","schema":{"type":"integer","minimum":1,"maximum":500,"default":50}},"OffsetParam":{"name":"offset","in":"query","required":false,"description":"Zero-based row offset","schema":{"type":"integer","minimum":0,"default":0}},"PageParam":{"name":"page","in":"query","required":false,"description":"One-based page number (used when offset is omitted)","schema":{"type":"integer","minimum":1}}},"responses":{"BadRequest":{"description":"Validation or request parameter error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Missing, invalid, or expired bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"Missing tenant membership or insufficient role permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found in active tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Conflict":{"description":"Uniqueness, schedule overlap, or referential integrity conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"schemas":{"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"string"},"code":{"type":"string"},"details":{"type":"object","description":"Duplicate email conflicts include field set to email."}}},"PaginationMeta":{"type":"object","required":["total","limit","offset","page","total_pages","has_more"],"properties":{"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1},"offset":{"type":"integer","minimum":0},"page":{"type":"integer","minimum":1},"total_pages":{"type":"integer","minimum":0},"has_more":{"type":"boolean"}}},"Tenant":{"type":"object","properties":{"tenant_id":{"type":"integer"},"name":{"type":"string"},"stripe_connect_id":{"type":["string","null"]},"created_at":{"type":["string","null"]},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]}}},"User":{"type":"object","properties":{"user_id":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":["string","null"]},"reports_to_id":{"type":["integer","null"]},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]},"membership_id":{"type":"integer"}}},"Membership":{"type":"object","description":"email, phone, and deactivated_by_user_id are returned only to roles with members.manage (owner and admin). Employee, staff, and office receive membership_id, tenant_id, user_id, name, role, active, and deactivated_at. Viewer and helper cannot list or read members.","properties":{"membership_id":{"type":"integer"},"tenant_id":{"type":"integer"},"user_id":{"type":"integer"},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]},"name":{"type":"string"},"active":{"type":"boolean"},"deactivated_at":{"type":["string","null"]},"email":{"type":"string","description":"Included only for members.manage."},"phone":{"type":["string","null"],"description":"Included only for members.manage."},"deactivated_by_user_id":{"type":["integer","null"],"description":"Included only for members.manage."},"deactivated":{"type":"boolean","description":"Present on a deactivate response for members.manage."}}},"Company":{"type":"object","properties":{"company_id":{"type":"integer"},"tenant_id":{"type":"integer"},"legal_name":{"type":"string"},"dba_name":{"type":["string","null"]},"ein":{"type":["string","null"]},"address":{"type":["string","null"]}}},"Brand":{"type":"object","properties":{"brand_id":{"type":"integer"},"tenant_id":{"type":"integer"},"name":{"type":"string"},"primary_color":{"type":["string","null"]},"secondary_color":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"display_name":{"type":"string"},"logo_file_id":{"type":["integer","null"]},"is_default":{"type":"integer"}}},"ServiceTerritory":{"type":"object","properties":{"territory_id":{"type":"integer"},"tenant_id":{"type":"integer"},"name":{"type":"string"},"geojson_boundary":{"type":["string","null"]},"median_income":{"type":["number","null"]},"avg_home_age":{"type":["integer","null"]}}},"StorageLocation":{"type":"object","properties":{"storage_location_id":{"type":"integer"},"tenant_id":{"type":"integer"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"Customer":{"type":"object","properties":{"customer_id":{"type":"integer"},"tenant_id":{"type":"integer"},"name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"latitude":{"type":["number","null"]},"longitude":{"type":["number","null"]},"territory_id":{"type":["integer","null"]},"lead_source":{"type":["string","null"]},"territory_name":{"type":["string","null"]}}},"Asset":{"type":"object","properties":{"asset_id":{"type":"integer"},"tenant_id":{"type":"integer"},"asset_number":{"type":"string"},"model_number":{"type":["string","null"]},"category":{"type":["string","null"]},"image_url":{"type":["string","null"]},"storage_location_id":{"type":["integer","null"]},"storage_location_name":{"type":["string","null"]},"supplier_name":{"type":["string","null"]},"cost_cents":{"type":"integer","minimum":0},"sell_price_cents":{"type":"integer","minimum":0}}},"ClientInstalledAsset":{"type":"object","properties":{"installed_asset_id":{"type":"integer"},"tenant_id":{"type":"integer"},"customer_id":{"type":"integer"},"asset_id":{"type":"integer"},"serial_number":{"type":["string","null"]},"installed_date":{"type":["string","null"]},"warranty_expiration":{"type":["string","null"]},"property_id":{"type":["integer","null"]},"property_label":{"type":["string","null"]},"deleted_at":{"type":["string","null"]}}},"JobCommunication":{"type":"object","properties":{"comm_id":{"type":"integer"},"tenant_id":{"type":"integer"},"customer_id":{"type":"integer"},"user_id":{"type":["integer","null"]},"direction":{"type":"string","enum":["inbound","outbound"]},"message_type":{"type":["string","null"]},"content":{"type":"string"},"timestamp":{"type":["string","null"]}}},"WorkOrder":{"type":"object","properties":{"work_order_id":{"type":"integer"},"tenant_id":{"type":"integer"},"customer_id":{"type":"integer"},"company_id":{"type":["integer","null"]},"brand_id":{"type":["integer","null"]},"installed_asset_id":{"type":["integer","null"]},"assigned_user_id":{"type":["integer","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","scheduled","in_progress","on_hold","completed","canceled"]},"priority":{"type":"string","enum":["low","normal","high","emergency"]},"service_address":{"type":["string","null"]},"scheduled_start":{"type":["string","null"]},"scheduled_end":{"type":["string","null"]},"completed_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}}},"JobSchedule":{"type":"object","properties":{"schedule_id":{"type":"integer"},"tenant_id":{"type":"integer"},"work_order_id":{"type":"integer"},"technician_user_id":{"type":["integer","null"]},"scheduled_start":{"type":"string","format":"date-time"},"scheduled_end":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["scheduled","dispatched","in_progress","completed","canceled"]},"notes":{"type":["string","null"]},"arrival_window_start":{"type":["string","null"],"format":"date-time"},"arrival_window_end":{"type":["string","null"],"format":"date-time"},"spans_multiple_days":{"type":"integer"},"scheduled_start_local":{"type":"string"},"scheduled_end_local":{"type":"string"},"timezone":{"type":"string"},"created_at":{"type":["string","null"]}}},"InvoiceLineItemInput":{"type":"object","required":["description","unit_price_cents"],"properties":{"description":{"type":"string"},"quantity":{"type":"number","minimum":0.0001,"default":1},"unit_price_cents":{"type":"integer","minimum":0},"asset_id":{"type":["integer","null"]}}},"InvoiceLineItem":{"type":"object","properties":{"line_item_id":{"type":"integer"},"tenant_id":{"type":"integer"},"invoice_id":{"type":"integer"},"asset_id":{"type":["integer","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unit_price_cents":{"type":"integer"},"line_total_cents":{"type":"integer"},"line_kind":{"type":"string","enum":["service","hardware","labor","material","discount"]}}},"InvoicePayment":{"type":"object","properties":{"payment_id":{"type":"integer"},"tenant_id":{"type":"integer"},"invoice_id":{"type":"integer"},"recorded_by_user_id":{"type":["integer","null"]},"amount_cents":{"type":"integer"},"payment_method":{"type":"string","enum":["card","ach","cash","check","venmo","other"]},"reference_number":{"type":["string","null"]},"paid_at":{"type":["string","null"]},"notes":{"type":["string","null"]},"voided_at":{"type":["string","null"]},"is_test":{"type":"integer","enum":[0,1]},"recorded_by_name":{"type":["string","null"]}}},"Invoice":{"type":"object","properties":{"invoice_id":{"type":"integer"},"tenant_id":{"type":"integer"},"customer_id":{"type":"integer"},"work_order_id":{"type":["integer","null"]},"company_id":{"type":["integer","null"]},"brand_id":{"type":["integer","null"]},"invoice_number":{"type":"string"},"status":{"type":"string","enum":["draft","sent","partially_paid","paid","void"]},"issue_date":{"type":["string","null"]},"due_date":{"type":["string","null"]},"subtotal_cents":{"type":"integer"},"tax_cents":{"type":"integer"},"discount_cents":{"type":"integer"},"total_cents":{"type":"integer"},"amount_paid_cents":{"type":"integer"},"balance_due_cents":{"type":"integer"},"notes":{"type":["string","null"]},"created_at":{"type":["string","null"]},"pricing_mode":{"type":"string","enum":["tax_added","tax_included"]},"tax_rate_bps":{"type":"integer"},"warranty_terms":{"type":"string"},"payment_terms":{"type":"string"},"terms_days":{"type":"integer"},"review_requested_at":{"type":["string","null"]},"is_test":{"type":"integer","enum":[0,1]},"sent_at":{"type":["string","null"]},"sent_by_user_id":{"type":["integer","null"]},"sent_method":{"type":["string","null"],"enum":["manual","email","text",null]},"customer_name":{"type":["string","null"]},"work_order_title":{"type":["string","null"]},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItem"}},"payments":{"type":"array","items":{"$ref":"#/components/schemas/InvoicePayment"}}}},"EstimateLineItem":{"type":"object","properties":{"estimate_line_item_id":{"type":"integer"},"tenant_id":{"type":"integer"},"estimate_id":{"type":"integer"},"asset_id":{"type":["integer","null"]},"description":{"type":"string"},"quantity":{"type":"number"},"unit_price_cents":{"type":"integer"},"line_total_cents":{"type":"integer"},"line_kind":{"type":"string","enum":["service","hardware","labor","material","discount"]},"unit_cost_cents":{"type":["integer","null"],"description":"Owner and admin only"},"hourly_rate_cents":{"type":["integer","null"],"description":"Owner and admin only"},"markup_bps":{"type":["integer","null"],"description":"Owner and admin only"}}},"Estimate":{"type":"object","properties":{"estimate_id":{"type":"integer"},"tenant_id":{"type":"integer"},"customer_id":{"type":"integer"},"company_id":{"type":["integer","null"]},"brand_id":{"type":["integer","null"]},"installed_asset_id":{"type":["integer","null"]},"prepared_by_user_id":{"type":["integer","null"]},"estimate_number":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","sent","approved","declined","expired","converted"]},"service_address":{"type":["string","null"]},"issue_date":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"subtotal_cents":{"type":"integer"},"tax_cents":{"type":"integer"},"discount_cents":{"type":"integer"},"total_cents":{"type":"integer"},"notes":{"type":["string","null"]},"pricing_mode":{"type":"string","enum":["tax_added","tax_included"]},"tax_rate_bps":{"type":"integer"},"warranty_terms":{"type":"string"},"quote_kind":{"type":"string","enum":["standard","display"]},"quote_price_version":{"type":"integer"},"reviewed_price_version":{"type":["integer","null"]},"approved_at":{"type":["string","null"]},"declined_at":{"type":["string","null"]},"converted_work_order_id":{"type":["integer","null"]},"converted_invoice_id":{"type":["integer","null"]},"converted_at":{"type":["string","null"]},"created_at":{"type":["string","null"]},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/EstimateLineItem"}}}},"InventoryStock":{"type":"object","properties":{"stock_id":{"type":"integer"},"tenant_id":{"type":"integer"},"asset_id":{"type":"integer"},"storage_location_id":{"type":"integer"},"quantity_on_hand":{"type":"number"},"reorder_point":{"type":"number"},"unit_cost_cents":{"type":"integer"},"is_low_stock":{"type":"integer","enum":[0,1]},"updated_at":{"type":["string","null"]}}},"WorkOrderPart":{"type":"object","properties":{"work_order_part_id":{"type":"integer"},"tenant_id":{"type":"integer"},"work_order_id":{"type":"integer"},"asset_id":{"type":["integer","null"]},"storage_location_id":{"type":["integer","null"]},"recorded_by_user_id":{"type":["integer","null"]},"quantity":{"type":"number"},"unit_cost_cents":{"type":"integer"},"unit_price_cents":{"type":"integer"},"line_total_cents":{"type":"integer"},"notes":{"type":["string","null"]},"created_at":{"type":["string","null"]}}},"InventoryTransaction":{"type":"object","properties":{"inventory_tx_id":{"type":"integer"},"tenant_id":{"type":"integer"},"asset_id":{"type":["integer","null"]},"storage_location_id":{"type":["integer","null"]},"related_location_id":{"type":["integer","null"]},"work_order_id":{"type":["integer","null"]},"performed_by_user_id":{"type":["integer","null"]},"tx_type":{"type":"string","enum":["restock","adjust","transfer_out","transfer_in","consume","return"]},"quantity_delta":{"type":"number"},"quantity_after":{"type":"number"},"unit_cost_cents":{"type":"integer"},"reference_note":{"type":["string","null"]},"created_at":{"type":["string","null"]}}},"StripePaymentIntent":{"type":"object","properties":{"payment_intent_row_id":{"type":"integer"},"tenant_id":{"type":"integer"},"invoice_id":{"type":"integer"},"stripe_payment_intent_id":{"type":"string"},"stripe_connect_account_id":{"type":"string"},"amount_cents":{"type":"integer"},"application_fee_cents":{"type":"integer"},"currency":{"type":"string"},"payment_method_type":{"type":"string","enum":["card","ach"]},"status":{"type":"string","enum":["requires_payment_method","processing","succeeded","failed","canceled"]},"client_secret":{"type":["string","null"]},"checkout_url":{"type":["string","null"]},"created_by_user_id":{"type":["integer","null"]},"failure_message":{"type":["string","null"]},"succeeded_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}}},"StripeWebhookEvent":{"type":"object","properties":{"webhook_event_row_id":{"type":"integer"},"stripe_event_id":{"type":"string"},"event_type":{"type":"string"},"tenant_id":{"type":["integer","null"]},"invoice_id":{"type":["integer","null"]},"stripe_payment_intent_id":{"type":["string","null"]},"status":{"type":"string","enum":["processed","ignored"]},"processed_at":{"type":["string","null"]}}},"ServicePriceItem":{"type":"object","properties":{"item_id":{"type":"integer"},"tenant_id":{"type":"integer"},"code":{"type":"string"},"name":{"type":"string"},"public_name":{"type":["string","null"]},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string","enum":["each","hour","sq_ft","linear_ft","job","day"]},"price_cents":{"type":"integer","minimum":0,"description":"Viewer and up. Helpers cannot read this list."},"cost_cents":{"type":["integer","null"],"minimum":0,"description":"Admin and owner only"},"active":{"type":"integer","enum":[0,1]},"admin_only":{"type":"integer","enum":[0,1]},"public_ok":{"type":"integer","enum":[0,1]},"sort_order":{"type":"integer"},"is_test":{"type":"integer","enum":[0,1]},"deleted_at":{"type":["string","null"]}}},"ServicePriceItemWrite":{"type":"object","required":["code","name","unit","price_cents"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"public_name":{"type":["string","null"]},"description":{"type":["string","null"]},"category":{"type":["string","null"]},"unit":{"type":"string","enum":["each","hour","sq_ft","linear_ft","job","day"]},"price_cents":{"type":"integer","minimum":0},"cost_cents":{"type":["integer","null"],"minimum":0},"active":{"type":"boolean"},"admin_only":{"type":"boolean"},"public_ok":{"type":"boolean"},"sort_order":{"type":"integer"},"is_test":{"type":"boolean"}}},"PriceCheckLine":{"type":"object","required":["unit","quantity","unit_price_cents"],"properties":{"unit":{"type":"string","enum":["each","hour","sq_ft","linear_ft","job","day"]},"quantity":{"type":"number","minimum":0},"unit_price_cents":{"type":"integer","minimum":0},"kind":{"type":"string","enum":["charge","discount","minimum_top_up"]}}},"AuditLogEntry":{"type":"object","properties":{"log_id":{"type":"integer"},"tenant_id":{"type":["integer","null"]},"user_id":{"type":["integer","null"]},"action":{"type":"string"},"details":{"type":["string","null"]},"timestamp":{"type":["string","null"]},"actor_name":{"type":["string","null"]},"actor_email":{"type":["string","null"]}}}}},"paths":{"/api/health":{"get":{"tags":["System"],"summary":"Live service & SQLite readiness health check","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string"}}}}}},"503":{"description":"Database unavailable"}}}},"/api/openapi.json":{"get":{"tags":["System"],"summary":"OpenAPI 3.1.0 JSON specification","responses":{"200":{"description":"OpenAPI document"}}}},"/api/docs":{"get":{"tags":["System"],"summary":"Interactive HTML API documentation explorer","responses":{"200":{"description":"HTML documentation page"}}}},"/api/internal/tick":{"post":{"tags":["System"],"summary":"Run due schedules and queued jobs once. Absent (404) unless TICK_SECRET is set. Send that secret as Authorization: Bearer. POST only. Safe to call once a minute. Overlapping calls share one pass.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"One pass finished. data.ran is the number of jobs handled. data.jobs lists job_id, job_type, and status.","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","required":["ran","jobs"],"properties":{"ran":{"type":"integer","minimum":0},"jobs":{"type":"array","items":{"type":"object","required":["job_id","job_type","status"],"properties":{"job_id":{"type":"integer"},"job_type":{"type":"string"},"status":{"type":"string"}}}}}}}}}}},"401":{"description":"TICK_SECRET is set and the bearer token does not match"},"404":{"description":"TICK_SECRET is unset, or the method is not POST"},"429":{"description":"Too many tick calls. A call once a minute stays under the limit."}}}},"/api/auth/register":{"post":{"tags":["Auth"],"summary":"Invite-only registration. Creating a company requires setup_token. Joining a company requires invite_token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","password"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8,"maxLength":256},"phone":{"type":["string","null"]},"tenant_name":{"type":["string","null"]},"setup_token":{"type":"string","description":"Required when tenant_name is set. Matches the server SETUP_TOKEN."},"invite_token":{"type":"string","description":"Required when joining an existing company. Must match the invited email."},"stripe_connect_id":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Registered user and signed bearer token. data.two_factor_enrollment is \"required\" when this person must set up two-step sign-in before using the rest of the app. The token then carries enroll_2fa: true."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"description":"Too many signup attempts"}}}},"/api/auth/login":{"post":{"tags":["Auth"],"summary":"Authenticate with email and password to receive an HS256 bearer token","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"},"totp_code":{"type":"string","description":"Current two-step code. Required when two-step is already on, unless recovery_code is sent. The same code is refused again inside its 30 second window."},"recovery_code":{"type":"string","description":"One-time recovery code from confirm, used instead of totp_code. A code is accepted once. Sending totp_code ignores recovery_code."},"shared_device":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Session token, user, and memberships. data.two_factor_enrollment is \"required\" when this person must enroll, and the token claim enroll_2fa is true. A consumed recovery code adds data.recovery_codes_remaining."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"description":"Sign-in for this email from this client address is paused. Other accounts are not paused."}}}},"/api/auth/invites/accept":{"post":{"tags":["Auth"],"summary":"Accept a company invitation for the signed-in user email. Already a member returns 409 with details.field email.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["invite_token"],"properties":{"invite_token":{"type":"string"}}}}}},"responses":{"200":{"description":"Membership created for the signed-in user"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/auth/me":{"get":{"tags":["Auth"],"summary":"Current authenticated user profile and tenant memberships","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user and memberships. An enroll-limited session may call this so the setup screen can load. data.two_factor_enrollment is \"required\" until two-step sign-in is confirmed."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/auth/password":{"post":{"tags":["Auth"],"summary":"Rotate password for the authenticated user","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["current_password","new_password"],"properties":{"current_password":{"type":"string"},"new_password":{"type":"string","minLength":8,"maxLength":256}}}}}},"responses":{"200":{"description":"Password rotated. The response token replaces every previous token for this person."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/auth/logout":{"post":{"tags":["Auth"],"summary":"Invalidate every bearer token for the signed-in person","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"All existing tokens for this person are no longer accepted"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Set a new password with a one-time reset code. The code is the only lookup key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reset_code","new_password"],"properties":{"reset_code":{"type":"string"},"new_password":{"type":"string","minLength":8,"maxLength":256},"totp_code":{"type":"string","description":"Required when two-step is already on, unless recovery_code is sent."},"recovery_code":{"type":"string","description":"One-time recovery code used instead of totp_code. Single use."}}}}}},"responses":{"200":{"description":"Password updated and a new token issued"},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"description":"Password reset is paused for this client address"}}}},"/api/tenants":{"get":{"tags":["Tenants"],"summary":"List tenants the authenticated user belongs to","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated tenant list","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Tenants"],"summary":"Create a new tenant and assign caller as owner (requires setup_token)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","setup_token"],"properties":{"name":{"type":"string"},"setup_token":{"type":"string"},"stripe_connect_id":{"type":["string","null"]},"timezone":{"type":"string","description":"Optional IANA time zone stored on company settings. Invalid names are rejected."}}}}}},"responses":{"201":{"description":"Created tenant","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Tenant"}}}}}},"429":{"description":"Company setup is paused for this client address"}}}},"/api/tenant":{"get":{"tags":["Tenants"],"summary":"Get active X-Tenant-Id profile (role: viewer+). stripe_connect_id is omitted.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Active tenant details. stripe_connect_id is not included for any role.","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"tenant_id":{"type":"integer"},"name":{"type":"string"},"status":{"type":"string"},"created_at":{"type":["string","null"]},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]},"membership_id":{"type":"integer"}}}}}}}}}},"patch":{"tags":["Tenants"],"summary":"Update active tenant settings (role: owner)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"stripe_connect_id":{"type":["string","null"]},"timezone":{"type":"string","description":"IANA time zone stored on company settings. Invalid names are rejected."}}}}}},"responses":{"200":{"description":"Updated tenant"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/memberships":{"get":{"tags":["Memberships"],"summary":"List tenant memberships. Email and phone only for members.manage (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Membership"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Memberships"],"summary":"Invite an email to the active company (role: admin+). Does not attach or return another company profile. An email that is already a member returns 409 with details.field email.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]}}}}}},"responses":{"201":{"description":"Pending invitation. accept_token is returned once and is not stored in plaintext."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/memberships/invites":{"get":{"tags":["Memberships"],"summary":"List company invitations without accept tokens (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Membership"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/memberships/invites/{inviteId}":{"delete":{"tags":["Memberships"],"summary":"Revoke a pending invitation (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"inviteId","in":"path","required":true,"description":"Invitation ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Invitation revoked"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/memberships/{membershipId}":{"get":{"tags":["Memberships"],"summary":"Read one membership (role: employee+). Email and phone are included only for members.manage.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"membershipId","in":"path","required":true,"description":"Membership ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Membership","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Membership"}}}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Memberships"],"summary":"Update member role (role: admin+; owner required for owner role). Changing your own role returns 409 code SELF_CHANGE_NOT_ALLOWED.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"membershipId","in":"path","required":true,"description":"Membership ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"]}}}}}},"responses":{"200":{"description":"Updated membership"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Memberships"],"summary":"Deactivate a membership. The row stays, with who and when. Requires members.manage. Deactivating yourself returns 409 code SELF_CHANGE_NOT_ALLOWED.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"membershipId","in":"path","required":true,"description":"Membership ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deactivated membership. The row stays."},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/users":{"get":{"tags":["Users"],"summary":"List users belonging to active tenant (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Users"],"summary":"Provision a new user account directly into active tenant (role: admin+). A duplicate email returns 409 with details.field email.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":["string","null"],"minLength":8},"phone":{"type":["string","null"]},"reports_to_id":{"type":["integer","null"]},"role":{"type":"string","enum":["owner","admin","office","staff","employee","helper","viewer"],"default":"employee"}}}}}},"responses":{"201":{"description":"Provisioned user"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/users/{userId}":{"get":{"tags":["Users"],"summary":"Get tenant user by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"userId","in":"path","required":true,"description":"User ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"User record"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Users"],"summary":"Update user profile (self or admin+ for single-tenant users). A duplicate email returns 409 with details.field email.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"userId","in":"path","required":true,"description":"User ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":["string","null"]},"reports_to_id":{"type":["integer","null"]}}}}}},"responses":{"200":{"description":"Updated user"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/users/{userId}/password-reset":{"post":{"tags":["Users"],"summary":"Owner or admin issues a one-time password reset code for a member of this company only. The code is returned once and is not emailed.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"userId","in":"path","required":true,"description":"User ID","schema":{"type":"integer","minimum":1}}],"responses":{"201":{"description":"One-time reset code. Store nothing from this response except to hand the code to the person."},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Password reset is paused for this client address"}}}},"/api/companies":{"get":{"tags":["Companies"],"summary":"List companys (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Company"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Companies"],"summary":"Create company (role: admin+). A tenant that already has a legal company returns 409 with code COMPANY_ALREADY_EXISTS. Set MULTI_COMPANY to 1, true, or yes to allow another. The default is off.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["legal_name"],"properties":{"legal_name":{"type":"string"},"dba_name":{"type":["string","null"]},"ein":{"type":["string","null"]},"address":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Created company","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Company"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"This tenant already has a legal company. The body code is COMPANY_ALREADY_EXISTS unless MULTI_COMPANY is on.","content":{"application/json":{"schema":{"type":"object","required":["status","error","code"],"properties":{"status":{"type":"string","const":"error"},"error":{"type":"string"},"code":{"type":"string","const":"COMPANY_ALREADY_EXISTS"}}}}}}}}},"/api/companies/{companyId}":{"get":{"tags":["Companies"],"summary":"Get company by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"companyId","in":"path","required":true,"description":"company primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"company record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Company"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Companies"],"summary":"Update company (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"companyId","in":"path","required":true,"description":"company primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated company","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Company"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Companies"],"summary":"Delete company (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"companyId","in":"path","required":true,"description":"company primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted company","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/brands":{"get":{"tags":["Brands"],"summary":"List brands (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Brand"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Brands"],"summary":"Create brand (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"primary_color":{"type":["string","null"]},"secondary_color":{"type":["string","null"]},"logo_url":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Created brand","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Brand"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/brands/{brandId}":{"get":{"tags":["Brands"],"summary":"Get brand by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"brandId","in":"path","required":true,"description":"brand primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"brand record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Brand"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Brands"],"summary":"Propose a brand change. An owner or admin approves it before it is published (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"brandId","in":"path","required":true,"description":"brand primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"display_name":{"type":"string"},"primary_color":{"type":["string","null"]},"secondary_color":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"logo_file_id":{"type":["integer","null"]},"sender_name":{"type":"string"},"make_default":{"type":"boolean"}}}}}},"responses":{"202":{"description":"Draft approval. The published brand stays as it is until an owner or admin approves the draft.","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Brand"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Brands"],"summary":"Delete brand (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"brandId","in":"path","required":true,"description":"brand primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted brand","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/branding":{"get":{"tags":["Branding"],"summary":"Published brand, sender name, and customer-facing wording for this company","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Brand presentation. Private and never-publish profile values are omitted."}}}},"/api/branding/wording":{"post":{"tags":["Branding"],"summary":"Propose customer-facing wording. Approval publishes it (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"202":{"description":"Draft approval"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/branding/profile":{"post":{"tags":["Branding"],"summary":"Propose public-profile and company book settings. Approval publishes them (role: admin+). A new draft marks older draft or pending profile approvals for that same legal company as superseded.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"202":{"description":"Draft approval"},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/branding/companies/{companyId}/public-profile":{"get":{"tags":["Branding"],"summary":"Public profile only. Never-publish values are omitted (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"companyId","in":"path","required":true,"description":"Legal company ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Public fields"}}}},"/api/branding/revisions/{revisionId}/rollback":{"post":{"tags":["Branding"],"summary":"Restore a prior approved brand, wording, or profile version (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"revisionId","in":"path","required":true,"description":"Revision ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/documents/estimates/{id}":{"get":{"tags":["Documents"],"summary":"Quote PDF or HTML using that quote's company and brand (role: viewer+). Bearer token and X-Tenant-Id. Content-Disposition attachment. No signed URL.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"PDF by default. Add format=html for HTML."}}}},"/api/documents/invoices/{id}":{"get":{"tags":["Documents"],"summary":"Invoice PDF or HTML using that invoice's company and brand (role: viewer+). Bearer token and X-Tenant-Id. Content-Type and Content-Disposition attachment for a fetch download. No Pay button while online payment is off. No signed URL.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"id","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"PDF by default. Add format=html for HTML."}}}},"/api/territories":{"get":{"tags":["Territories"],"summary":"List service territorys (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceTerritory"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Territories"],"summary":"Create service territory (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"geojson_boundary":{"type":["object","string","null"]},"median_income":{"type":["number","null"],"minimum":0},"avg_home_age":{"type":["integer","null"],"minimum":0}}}}}},"responses":{"201":{"description":"Created service territory","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ServiceTerritory"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/territories/{territoryId}":{"get":{"tags":["Territories"],"summary":"Get service territory by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"territoryId","in":"path","required":true,"description":"service territory primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"service territory record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ServiceTerritory"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Territories"],"summary":"Update service territory (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"territoryId","in":"path","required":true,"description":"service territory primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated service territory","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ServiceTerritory"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Territories"],"summary":"Delete service territory (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"territoryId","in":"path","required":true,"description":"service territory primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted service territory","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/storage-locations":{"get":{"tags":["Storage Locations"],"summary":"List storage locations (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocation"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Storage Locations"],"summary":"Create storage location (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Created storage location","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/StorageLocation"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/storage-locations/{storageLocationId}":{"get":{"tags":["Storage Locations"],"summary":"Get storage location by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"storageLocationId","in":"path","required":true,"description":"storage location primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"storage location record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/StorageLocation"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Storage Locations"],"summary":"Update storage location (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"storageLocationId","in":"path","required":true,"description":"storage location primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated storage location","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/StorageLocation"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Storage Locations"],"summary":"Delete storage location (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"storageLocationId","in":"path","required":true,"description":"storage location primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted storage location","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/customers":{"get":{"tags":["Customers"],"summary":"List customers (role: helper+). A helper sees customers on work orders assigned to them, or on a live visit where they are the technician or extra crew.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"territory_id","in":"query","required":false,"description":"Filter by territory_id","schema":{"type":"integer","minimum":1}},{"name":"search","in":"query","required":false,"description":"Substring search across name, email, phone, address","schema":{"type":"string"}},{"name":"client_kind","in":"query","required":false,"description":"Filter by client kind. Unknown values are rejected.","schema":{"type":"string","enum":["home","business"]}},{"name":"sort","in":"query","required":false,"description":"Sort column. Defaults to customer_id. Unknown values are rejected. Customers have no created_at or updated_at.","schema":{"type":"string","enum":["name","customer_id"]}},{"name":"direction","in":"query","required":false,"description":"Sort direction. Defaults to asc. A name sort breaks ties on customer_id ascending.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"description":"Search, client_kind, sort, direction, and paging totals use that same set. Canceling, deleting, or leaving the visit removes the customer on the next request. An unassigned customer id returns 404."},"post":{"tags":["Customers"],"summary":"Create customer (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"latitude":{"type":["number","null"],"minimum":-90,"maximum":90},"longitude":{"type":["number","null"],"minimum":-180,"maximum":180},"territory_id":{"type":["integer","null"]},"lead_source":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Created customer","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Customer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/customers/{customerId}":{"get":{"tags":["Customers"],"summary":"Get customer by ID (role: helper+). A helper who is not assigned and not booked on a live visit receives 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"customer primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"customer record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Customer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Customers"],"summary":"Update customer (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"customer primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated customer","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Customer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Customers"],"summary":"Delete customer (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"customer primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted customer","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/assets":{"get":{"tags":["Assets"],"summary":"List assets (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"category","in":"query","required":false,"description":"Filter by exact category","schema":{"type":"string"}},{"name":"storage_location_id","in":"query","required":false,"description":"Filter by storage_location_id","schema":{"type":"integer","minimum":1}},{"name":"search","in":"query","required":false,"description":"Substring search across asset_number, model_number, category","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Assets"],"summary":"Create asset (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_number"],"properties":{"asset_number":{"type":"string"},"model_number":{"type":["string","null"]},"category":{"type":["string","null"]},"image_url":{"type":["string","null"]},"storage_location_id":{"type":["integer","null"]},"supplier_name":{"type":["string","null"]},"cost_cents":{"type":"integer","minimum":0},"sell_price_cents":{"type":"integer","minimum":0}}}}}},"responses":{"201":{"description":"Created asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Asset"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/assets/{assetId}":{"get":{"tags":["Assets"],"summary":"Get asset by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"assetId","in":"path","required":true,"description":"asset primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"asset record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Asset"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Assets"],"summary":"Update asset (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"assetId","in":"path","required":true,"description":"asset primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Asset"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Assets"],"summary":"Delete asset (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"assetId","in":"path","required":true,"description":"asset primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/installed-assets":{"get":{"tags":["Installed Assets"],"summary":"List installed assets (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"customer_id","in":"query","required":false,"description":"Filter by customer_id","schema":{"type":"integer","minimum":1}},{"name":"asset_id","in":"query","required":false,"description":"Filter by asset_id","schema":{"type":"integer","minimum":1}},{"name":"property_id","in":"query","required":false,"description":"Filter by property_id","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ClientInstalledAsset"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Installed Assets"],"summary":"Create installed asset (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","asset_id"],"properties":{"customer_id":{"type":"integer","minimum":1},"asset_id":{"type":"integer","minimum":1},"serial_number":{"type":["string","null"]},"installed_date":{"type":["string","null"]},"warranty_expiration":{"type":["string","null"]},"property_id":{"type":["integer","null"],"minimum":1}}}}}},"responses":{"201":{"description":"Created installed asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ClientInstalledAsset"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/installed-assets/{installedAssetId}":{"get":{"tags":["Installed Assets"],"summary":"Get installed asset by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"installedAssetId","in":"path","required":true,"description":"installed asset primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"installed asset record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ClientInstalledAsset"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Installed Assets"],"summary":"Update installed asset (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"installedAssetId","in":"path","required":true,"description":"installed asset primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated installed asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/ClientInstalledAsset"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Installed Assets"],"summary":"Delete installed asset (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"installedAssetId","in":"path","required":true,"description":"installed asset primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted installed asset","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/job-communications":{"get":{"tags":["Job Communications"],"summary":"List job communications (role: helper+). A helper only sees notes on jobs assigned to them, or on a live visit where they are the technician or extra crew. Notes for the same customer on another job are left out.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"customer_id","in":"query","required":false,"description":"Filter by customer_id","schema":{"type":"integer","minimum":1}},{"name":"user_id","in":"query","required":false,"description":"Filter by staff user_id","schema":{"type":"integer","minimum":1}},{"name":"direction","in":"query","required":false,"description":"Filter by direction","schema":{"type":"string","enum":["inbound","outbound"]}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/JobCommunication"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Job Communications"],"summary":"Create a job communication (role: employee+, or a helper on their own job). A helper must send work_order_id. A missing job, another job, or a missing work_order_id returns 404 with the same body.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","content"],"properties":{"customer_id":{"type":"integer","minimum":1},"user_id":{"type":["integer","null"]},"direction":{"type":"string","enum":["inbound","outbound"],"default":"inbound"},"message_type":{"type":["string","null"]},"content":{"type":"string"},"timestamp":{"type":["string","null"]},"work_order_id":{"type":["integer","null"],"minimum":1}}}}}},"responses":{"201":{"description":"Created job communication","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobCommunication"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/job-communications/{commId}":{"get":{"tags":["Job Communications"],"summary":"Get job communication by ID (role: helper+). A note on another job returns 404, the same response as a missing note.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"commId","in":"path","required":true,"description":"job communication primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"job communication record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobCommunication"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Job Communications"],"summary":"Update job communication (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"commId","in":"path","required":true,"description":"job communication primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated job communication","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobCommunication"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Job Communications"],"summary":"Delete job communication (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"commId","in":"path","required":true,"description":"job communication primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted job communication","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/work-orders":{"get":{"tags":["Work Orders"],"summary":"List work orders (role: helper+). A helper sees work orders assigned to them, or jobs where they are the technician or extra crew on a live visit.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"customer_id","in":"query","required":false,"description":"Filter by customer_id","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Filter by status","schema":{"type":"string","enum":["draft","scheduled","in_progress","on_hold","completed","canceled"]}},{"name":"priority","in":"query","required":false,"description":"Filter by priority","schema":{"type":"string","enum":["low","normal","high","emergency"]}},{"name":"assigned_user_id","in":"query","required":false,"description":"Filter by assigned technician","schema":{"type":"integer","minimum":1}},{"name":"search","in":"query","required":false,"description":"Substring search across title, description, address, customer","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkOrder"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"description":"Customer name and service address on an unassigned job are omitted. An unassigned work order id returns 404."},"post":{"tags":["Work Orders"],"summary":"Create work order (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","title"],"properties":{"customer_id":{"type":"integer","minimum":1},"company_id":{"type":["integer","null"]},"brand_id":{"type":["integer","null"]},"installed_asset_id":{"type":["integer","null"]},"assigned_user_id":{"type":["integer","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","scheduled","in_progress","on_hold","completed","canceled"],"default":"draft"},"priority":{"type":"string","enum":["low","normal","high","emergency"],"default":"normal"},"service_address":{"type":["string","null"]},"scheduled_start":{"type":["string","null"],"format":"date-time"},"scheduled_end":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}}}}},"responses":{"201":{"description":"Created work order","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/WorkOrder"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/work-orders/{workOrderId}":{"get":{"tags":["Work Orders"],"summary":"Get work order by ID (role: helper+). A helper who is not the assignee and not booked on a live visit receives 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"work order primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"work order record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/WorkOrder"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Work Orders"],"summary":"Update work order (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"work order primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated work order","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/WorkOrder"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Work Orders"],"summary":"Delete work order (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"work order primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted work order","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/job-schedules":{"get":{"tags":["Job Schedules"],"summary":"List job schedules (role: helper+). A helper sees visits on work orders assigned to them, and visits where they are the technician or extra crew while that visit is live. technician_user_id for someone else returns 403 unless the role is office, admin, or owner.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"work_order_id","in":"query","required":false,"description":"Filter by work_order_id","schema":{"type":"integer","minimum":1}},{"name":"technician_user_id","in":"query","required":false,"description":"Filter by technician_user_id","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Filter by schedule status","schema":{"type":"string","enum":["scheduled","dispatched","in_progress","completed","canceled"]}},{"name":"from","in":"query","required":false,"description":"Include appointments ending on/after timestamp","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Include appointments starting on/before timestamp","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/JobSchedule"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Job Schedules"],"summary":"Create job schedule (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["work_order_id","scheduled_start","scheduled_end"],"properties":{"work_order_id":{"type":"integer","minimum":1},"technician_user_id":{"type":["integer","null"]},"scheduled_start":{"type":"string","format":"date-time"},"scheduled_end":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["scheduled","dispatched","in_progress","completed","canceled"],"default":"scheduled"},"notes":{"type":["string","null"]},"arrival_window_start":{"type":["string","null"],"format":"date-time"},"arrival_window_end":{"type":["string","null"],"format":"date-time"},"additional_user_ids":{"type":"array","items":{"type":"integer","minimum":1}},"allow_overlap":{"type":"boolean","default":false,"description":"Set true to allow overlapping technician appointments"}}}}}},"responses":{"201":{"description":"Created job schedule","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobSchedule"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/job-schedules/{scheduleId}":{"get":{"tags":["Job Schedules"],"summary":"Get job schedule by ID (role: helper+). A visit the helper is not assigned to and not booked on returns 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"scheduleId","in":"path","required":true,"description":"job schedule primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"job schedule record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobSchedule"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Job Schedules"],"summary":"Update job schedule (role: employee+). Moving or rescheduling a canceled or completed visit returns 409 with code VISIT_NOT_EDITABLE.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"scheduleId","in":"path","required":true,"description":"job schedule primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated job schedule","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/JobSchedule"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Job Schedules"],"summary":"Delete job schedule (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"scheduleId","in":"path","required":true,"description":"job schedule primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted job schedule","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/scheduling/unscheduled":{"get":{"tags":["Scheduling"],"summary":"Jobs waiting to be scheduled, oldest urgent work first (role: helper+). A helper sees jobs assigned to them or booked to them on a live visit.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Waiting list"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/scheduling/calendar":{"get":{"tags":["Scheduling"],"summary":"Day or week calendar in the company time zone (role: helper+). A helper sees visits on work orders assigned to them, and visits where they are the technician or extra crew while that visit is not deleted and not canceled. A user_id that is not the caller returns 403 unless the role is office, admin, or owner, on company scope and on staff scope. timezone_missing is true when the company has no time zone; dates still use UTC.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"view","in":"query","required":false,"description":"day or week","schema":{"type":"string","enum":["day","week"]}},{"name":"scope","in":"query","required":false,"description":"company or one person","schema":{"type":"string","enum":["company","staff"]}},{"name":"date","in":"query","required":false,"description":"Local calendar date YYYY-MM-DD","schema":{"type":"string","format":"date"}},{"name":"user_id","in":"query","required":false,"description":"Staff member when scope is staff","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Calendar. timezone is the company zone, or UTC when timezone_missing is true. A missing company time zone does not change the UTC fallback."},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/scheduling/proposals":{"get":{"tags":["Scheduling"],"summary":"Proposed bookings. An owner or admin confirms them in the approval queue (role: helper+). A helper sees proposals for work orders assigned to them or booked to them on a live visit.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Proposals"}}},"post":{"tags":["Scheduling"],"summary":"Propose a visit. It is not on the calendar until an owner or admin approves it (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Draft proposal and approval"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/scheduling/time/current":{"get":{"tags":["Scheduling"],"summary":"The signed-in person's open time entry, or null. This is the caller for every role, including office, admin, and owner. A user_id query is ignored. A viewer is blocked.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Open staff_time_entries row for this caller in this company, or null"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/scheduling/time/clock-in":{"post":{"tags":["Scheduling"],"summary":"Clock in (role: helper, or employee and above). A viewer is blocked. A helper can clock in only for themselves and must be the technician or extra crew on schedule_id, or the response is 404. schedule_id must be a visit on the given work_order_id in this company. Office, admin, and owner can clock someone else in. A caller who is not office, admin, or owner must be the technician or extra crew, or the response is 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer","minimum":1},"work_order_id":{"type":"integer","minimum":1},"schedule_id":{"type":"integer","minimum":1},"clock_in":{"type":"string","format":"date-time"},"notes":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Open time entry"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/scheduling/time/clock-out":{"post":{"tags":["Scheduling"],"summary":"Clock out (role: helper, or employee and above). A viewer is blocked. A helper can close only their own open entry. Office, admin, and owner can clock someone else out.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"entry_id":{"type":"integer","minimum":1},"user_id":{"type":"integer","minimum":1},"clock_out":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Closed time entry"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/scheduling/time/export.csv":{"get":{"tags":["Scheduling"],"summary":"Payroll-ready hours CSV with no pay math (role: office+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"CSV file"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"customer_id","in":"query","required":false,"description":"Filter by customer_id","schema":{"type":"integer","minimum":1}},{"name":"work_order_id","in":"query","required":false,"description":"Filter by work_order_id","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Stored status, or overdue (sent, due date before today, balance above 0)","schema":{"type":"string","enum":["draft","sent","partially_paid","paid","void","overdue"]}},{"name":"client_kind","in":"query","required":false,"description":"Filter by the customer client_kind","schema":{"type":"string","enum":["home","business"]}},{"name":"due_before","in":"query","required":false,"description":"Due on or before this YYYY-MM-DD date","schema":{"type":"string","format":"date"}},{"name":"due_after","in":"query","required":false,"description":"Due on or after this YYYY-MM-DD date","schema":{"type":"string","format":"date"}},{"name":"sort","in":"query","required":false,"description":"Allow-listed sort","schema":{"type":"string","enum":["due_date","issue_date","total"]}},{"name":"direction","in":"query","required":false,"description":"Sort direction","schema":{"type":"string","enum":["asc","desc"]}},{"name":"search","in":"query","required":false,"description":"Substring search across invoice_number and customer name","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"description":"Filtered balance totals cover the whole set, not one page. status=overdue means sent, due before today, and balance above 0. Sort values are due_date, issue_date, and total."},"post":{"tags":["Invoices"],"summary":"Create invoice (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customer_id":{"type":"integer","minimum":1},"work_order_id":{"type":["integer","null"]},"invoice_number":{"type":["string","null"]},"status":{"type":"string","enum":["draft","sent","void"],"default":"draft"},"issue_date":{"type":["string","null"]},"due_date":{"type":["string","null"]},"tax_cents":{"type":"integer","minimum":0,"default":0},"discount_cents":{"type":"integer","minimum":0,"default":0},"notes":{"type":["string","null"]},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}}}}},"responses":{"201":{"description":"Created invoice","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Invoice"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/invoices/{invoiceId}":{"get":{"tags":["Invoices"],"summary":"Get invoice by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"invoice primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"invoice record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Invoice"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Invoices"],"summary":"Update invoice (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"invoice primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated invoice","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Invoice"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Invoices"],"summary":"Delete invoice (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"invoice primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted invoice","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/invoices/{invoiceId}/line-items":{"post":{"tags":["Invoices"],"summary":"Add line item to invoice and recalculate totals (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}},"responses":{"201":{"description":"Created line item and recalculated invoice"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/invoices/{invoiceId}/line-items/{lineItemId}":{"patch":{"tags":["Invoices"],"summary":"Update invoice line item and recalculate totals (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Line Item ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}},"responses":{"200":{"description":"Updated line item and recalculated invoice"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Invoices"],"summary":"Delete invoice line item and recalculate totals (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Line Item ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted line item and recalculated invoice"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/invoices/{invoiceId}/payments":{"get":{"tags":["Invoices"],"summary":"List payments recorded against an invoice (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InvoicePayment"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Invoices"],"summary":"Record a hand payment (capability: payments.record — owner, admin, staff, employee; office refused). Repeat the same Idempotency-Key to get the first result.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional caller key. The same key and body return the first payment instead of recording another.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_cents","payment_method"],"properties":{"amount_cents":{"type":"integer","minimum":1},"payment_method":{"type":"string","enum":["cash","check","venmo","other"],"description":"Required. cash, check, venmo, or other. The company must have that method enabled. Card stays on the Stripe test path."},"reference_number":{"type":["string","null"]},"paid_at":{"type":["string","null"],"format":"date-time"},"notes":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Recorded payment and updated invoice"},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/invoices/{invoiceId}/payments/{paymentId}":{"delete":{"tags":["Invoices"],"summary":"Void a payment and recalculate the balance. The payment row is kept (role: admin+; payments.void). Staff and employee cannot void.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}},{"name":"paymentId","in":"path","required":true,"description":"Payment ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Voided payment and recalculated invoice"}}}},"/api/invoices/{invoiceId}/card-payments":{"post":{"tags":["Invoices"],"summary":"Start a simulated card payment through the fake or Stripe test adapter (capability: payments.record). Card is not accepted on the hand-payment route.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"responses":{"201":{"description":"Pending card payment. No live charge is created."}}}},"/api/invoices/{invoiceId}/mark-sent":{"post":{"tags":["Invoices"],"summary":"Mark an invoice sent by hand (role: employee+, including office, admin, and owner). Sets sent_at, sent_by_user_id, and sent_method manual. Does not queue a message or add a late fee.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sent_method":{"type":"string","enum":["manual"]}}}}}},"responses":{"200":{"description":"Invoice marked sent"},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/payments/webhook":{"post":{"tags":["Invoices"],"summary":"Apply a signed card-payment webhook once. A bad signature is rejected.","responses":{"200":{"description":"Applied or duplicate event"}}}},"/api/estimates":{"get":{"tags":["Estimates"],"summary":"List Estimates (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"customer_id","in":"query","required":false,"description":"Filter by customer_id","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Filter by estimate status","schema":{"type":"string","enum":["draft","sent","approved","declined","expired","converted"]}},{"name":"prepared_by_user_id","in":"query","required":false,"description":"Filter by prepared_by_user_id","schema":{"type":"integer","minimum":1}},{"name":"search","in":"query","required":false,"description":"Case-insensitive search across estimate_number, title, description, or customer name","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Estimate"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Estimates"],"summary":"Create Estimate (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","title"],"properties":{"customer_id":{"type":"integer"},"title":{"type":"string"},"estimate_number":{"type":["string","null"],"description":"Optional; auto-generates EST-000001 per tenant when omitted"},"company_id":{"type":["integer","null"]},"brand_id":{"type":["integer","null"]},"installed_asset_id":{"type":["integer","null"]},"prepared_by_user_id":{"type":["integer","null"]},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","sent","approved","declined","expired"],"default":"draft"},"service_address":{"type":["string","null"]},"issue_date":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"tax_cents":{"type":"integer","minimum":0,"default":0},"discount_cents":{"type":"integer","minimum":0,"default":0,"description":"Must be 0 on a new or edited quote. Use a discount line. Older quotes may still store a header discount. A quote that already has price-list lines returns 409 instead of changing this field."},"notes":{"type":["string","null"]},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}}}}},"responses":{"201":{"description":"Created Estimate","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Estimate"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/estimates/{estimateId}":{"get":{"tags":["Estimates"],"summary":"Get Estimate by ID (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Estimate record","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Estimate"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Estimates"],"summary":"Update Estimate (role: employee+). Sending discount_cents on a quote that already has price-list lines returns 409.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate primary key","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated Estimate","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"$ref":"#/components/schemas/Estimate"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Estimates"],"summary":"Delete Estimate (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate primary key","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted Estimate","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/estimates/{estimateId}/line-items":{"post":{"tags":["Estimates"],"summary":"Add line item to estimate and recalculate totals (role: employee+). A quote that already has price-list lines returns 409.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}},"responses":{"201":{"description":"Created estimate line item and recalculated totals"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/estimates/{estimateId}/line-items/{lineItemId}":{"patch":{"tags":["Estimates"],"summary":"Update estimate line item and recalculate totals (role: employee+). A quote that already has price-list lines returns 409.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Estimate Line Item ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceLineItemInput"}}}},"responses":{"200":{"description":"Updated estimate line item and recalculated totals"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Estimates"],"summary":"Delete estimate line item and recalculate totals (role: employee+). A quote that already has price-list lines returns 409.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Estimate Line Item ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted estimate line item and recalculated totals"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/estimates/{estimateId}/convert":{"post":{"tags":["Estimates"],"summary":"Convert an estimate into a Work Order and/or Invoice after an approved review (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"create_work_order":{"type":"boolean","default":true},"create_invoice":{"type":"boolean","default":true},"priority":{"type":"string","enum":["low","normal","high","emergency"],"default":"normal"},"assigned_user_id":{"type":["integer","null"]},"work_order_status":{"type":"string","enum":["draft","scheduled","in_progress","on_hold"]},"scheduled_start":{"type":["string","null"],"format":"date-time"},"scheduled_end":{"type":["string","null"],"format":"date-time"},"invoice_number":{"type":["string","null"]},"invoice_status":{"type":"string","enum":["draft","sent"],"default":"draft"},"issue_date":{"type":["string","null"]},"due_date":{"type":["string","null"]},"work_order_id":{"type":"integer","description":"Existing job to attach when create_work_order is false"}}}}}},"responses":{"201":{"description":"Converted estimate and created linked Work Order and/or Invoice"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/work-orders/{workOrderId}/parts":{"get":{"tags":["Work Orders"],"summary":"List parts consumed on a work order (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"workOrderId","in":"path","required":true,"description":"Work Order ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkOrderPart"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Work Orders"],"summary":"Consume inventory part onto a work order (role: employee+). When unit_price_cents is omitted, the catalog sell price is used. A catalog item with no sell price returns 400 with code PRICE_REQUIRED. The price is never the stock cost.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"Work Order ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","storage_location_id","quantity"],"properties":{"asset_id":{"type":"integer","minimum":1},"storage_location_id":{"type":"integer","minimum":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit_price_cents":{"type":["integer","null"],"minimum":0},"notes":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Consumed inventory stock and recorded part on work order"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/work-orders/{workOrderId}/parts/{partId}":{"delete":{"tags":["Work Orders"],"summary":"Remove work-order part entry and return stock to inventory (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"Work Order ID","schema":{"type":"integer","minimum":1}},{"name":"partId","in":"path","required":true,"description":"Work Order Part ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Removed part entry and returned stock to storage location"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/inventory/stock":{"get":{"tags":["Inventory"],"summary":"List inventory stock buckets across assets and storage locations (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"asset_id","in":"query","required":false,"description":"Filter by asset_id","schema":{"type":"integer","minimum":1}},{"name":"storage_location_id","in":"query","required":false,"description":"Filter by storage_location_id","schema":{"type":"integer","minimum":1}},{"name":"low_stock","in":"query","required":false,"description":"Filter to items at or below reorder_point (when reorder_point > 0)","schema":{"type":"boolean"}},{"name":"search","in":"query","required":false,"description":"Search asset_number, model_number, category, or storage location name","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryStock"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/inventory/stock/{stockId}":{"patch":{"tags":["Inventory"],"summary":"Update reorder_point or unit_cost_cents on a stock bucket (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"stockId","in":"path","required":true,"description":"Inventory Stock ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reorder_point":{"type":"number","minimum":0},"unit_cost_cents":{"type":"integer","minimum":0}}}}}},"responses":{"200":{"description":"Updated stock bucket metadata"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/inventory/restock":{"post":{"tags":["Inventory"],"summary":"Receive/restock inventory into a storage location (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","storage_location_id","quantity"],"properties":{"asset_id":{"type":"integer","minimum":1},"storage_location_id":{"type":"integer","minimum":1},"quantity":{"type":"number","exclusiveMinimum":0},"unit_cost_cents":{"type":"integer","minimum":0},"reorder_point":{"type":"number","minimum":0},"reference_note":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Restocked inventory and recorded ledger entry"},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/inventory/adjust":{"post":{"tags":["Inventory"],"summary":"Apply signed cycle-count or shrinkage stock adjustment (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","storage_location_id","quantity_delta"],"properties":{"asset_id":{"type":"integer","minimum":1},"storage_location_id":{"type":"integer","minimum":1},"quantity_delta":{"type":"number"},"unit_cost_cents":{"type":"integer","minimum":0},"reorder_point":{"type":"number","minimum":0},"reference_note":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Adjusted stock and recorded ledger entry"},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/inventory/transfer":{"post":{"tags":["Inventory"],"summary":"Atomically transfer stock between two storage locations (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset_id","from_storage_location_id","to_storage_location_id","quantity"],"properties":{"asset_id":{"type":"integer","minimum":1},"from_storage_location_id":{"type":"integer","minimum":1},"to_storage_location_id":{"type":"integer","minimum":1},"quantity":{"type":"number","exclusiveMinimum":0},"reference_note":{"type":["string","null"]}}}}}},"responses":{"201":{"description":"Transferred stock and recorded paired ledger entries"},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/inventory/transactions":{"get":{"tags":["Inventory"],"summary":"List append-only inventory ledger transactions (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"asset_id","in":"query","required":false,"description":"Filter by asset_id","schema":{"type":"integer","minimum":1}},{"name":"storage_location_id","in":"query","required":false,"description":"Filter by storage_location_id","schema":{"type":"integer","minimum":1}},{"name":"work_order_id","in":"query","required":false,"description":"Filter by work_order_id","schema":{"type":"integer","minimum":1}},{"name":"tx_type","in":"query","required":false,"description":"Filter by ledger transaction type","schema":{"type":"string","enum":["restock","adjust","transfer_out","transfer_in","consume","return"]}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InventoryTransaction"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/stripe/connect/status":{"get":{"tags":["Stripe Connect"],"summary":"Simulated Stripe Connect status for the active tenant (role: viewer+). charges_enabled and payouts_enabled stay false.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Tenant Stripe Connect status"}}}},"/api/stripe/connect/onboard":{"post":{"tags":["Stripe Connect"],"summary":"Simulate Stripe Connect onboarding for the active tenant (role: owner). Does not enable real charges or payouts.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"stripe_connect_id":{"type":["string","null"],"description":"Optional existing acct_... identifier; auto-provisions when omitted"}}}}}},"responses":{"201":{"description":"Simulated onboarding URL. Not a live Stripe account."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/invoices/{invoiceId}/checkout":{"post":{"tags":["Stripe Connect"],"summary":"Create a simulated test checkout for an invoice (role: employee+). No real charge is made.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"invoiceId","in":"path","required":true,"description":"Invoice ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"amount_cents":{"type":"integer","minimum":1,"description":"Defaults to invoice.balance_due_cents"},"payment_method_type":{"type":"string","enum":["card","ach"],"default":"card"},"application_fee_cents":{"type":"integer","minimum":0},"application_fee_bps":{"type":"number","minimum":0,"maximum":5000}}}}}},"responses":{"201":{"description":"Simulated test checkout. charges_enabled and payouts_enabled are false."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/stripe/payment-intents":{"get":{"tags":["Stripe Connect"],"summary":"List Stripe PaymentIntents for active tenant. client_secret is omitted. (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"invoice_id","in":"query","required":false,"description":"Filter by invoice_id","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Filter by PaymentIntent status","schema":{"type":"string","enum":["requires_payment_method","processing","succeeded","failed","canceled"]}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripePaymentIntent"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/stripe/webhook-events":{"get":{"tags":["Stripe Connect"],"summary":"List processed Stripe webhook events for active tenant (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"event_type","in":"query","required":false,"description":"Filter by Stripe event type","schema":{"type":"string"}},{"name":"invoice_id","in":"query","required":false,"description":"Filter by invoice_id","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripeWebhookEvent"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/webhooks/stripe":{"post":{"tags":["Stripe Connect"],"summary":"Public Stripe webhook receiver verified via Stripe-Signature HMAC-SHA256 header","parameters":[{"name":"Stripe-Signature","in":"header","required":true,"description":"Stripe HMAC-SHA256 signature header (t=<unix_ts>,v1=<hex_hmac>)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string","example":"evt_123"},"type":{"type":"string","example":"payment_intent.succeeded"},"data":{"type":"object"}}}}}},"responses":{"200":{"description":"Processed or idempotently acknowledged Stripe event"},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/reports/summary":{"get":{"tags":["Reports"],"summary":"KPI dashboard: revenue, receivables, work orders, estimates, inventory and customers in one call (role: admin+)","description":"Aggregated KPIs for the active tenant over the half-open UTC window [from, to).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/revenue":{"get":{"tags":["Reports"],"summary":"Collected vs invoiced revenue time series plus payment-method breakdown (role: admin+)","description":"Collected cents come from Invoice_Payments.paid_at; invoiced cents exclude draft and void invoices.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}},{"name":"group_by","in":"query","required":false,"description":"Series bucket size (weeks start on Monday)","schema":{"type":"string","enum":["day","week","month"],"default":"day"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/ar-aging":{"get":{"tags":["Reports"],"summary":"Accounts-receivable aging buckets (current, 1-30, 31-60, 61-90, 90+) and top debtors (role: admin+)","description":"Covers sent and partially_paid invoices with a positive balance. Invoices without a due_date are treated as current.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"as_of","in":"query","required":false,"description":"Aging reference timestamp (default: now)","schema":{"type":"string","format":"date-time"}},{"name":"top_customers","in":"query","required":false,"description":"Number of top debtors to return","schema":{"type":"integer","minimum":1,"maximum":100,"default":10}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/technician-utilization":{"get":{"tags":["Reports"],"summary":"Per-technician job counts, scheduled/completed hours and utilization against weekday capacity (role: admin+)","description":"Utilization = scheduled (non-canceled) hours / (Monday-Friday days in window x daily_capacity_hours).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}},{"name":"daily_capacity_hours","in":"query","required":false,"description":"Billable hours available per weekday","schema":{"type":"number","minimum":0.5,"maximum":24,"default":8}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/estimate-conversion":{"get":{"tags":["Reports"],"summary":"Estimate pipeline, win rate and conversion rate by status (role: admin+)","description":"Estimates are selected by created_at within the window.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/territory-performance":{"get":{"tags":["Reports"],"summary":"Customers, work orders, invoiced, collected and outstanding cents per service territory (role: admin+)","description":"Includes a synthetic \"Unassigned\" row (territory_id null) for customers without a territory.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"type":"object","additionalProperties":true}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/job-costing":{"get":{"tags":["Reports"],"summary":"Job costing: materials at captured cost, labor hours, and margin (role: admin+)","description":"Requires feature_reports. Labor has no dollar cost. Vendor and bill costs stay 0. Test jobs stay out unless include_test=1.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/job-costing/{workOrderId}":{"get":{"tags":["Reports"],"summary":"Lifetime cost, revenue, and margin for one job (role: admin+)","description":"Requires feature_reports. Returns 404 when the job is outside the current test-record view.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"workOrderId","in":"path","required":true,"description":"Work order ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/revenue-split":{"get":{"tags":["Reports"],"summary":"Invoiced and collected cents by category, brand, and customer type (role: admin+)","description":"Requires feature_reports. Amounts are integer cents.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/revenue-split.csv":{"get":{"tags":["Reports"],"summary":"Revenue split CSV. Dollars are formatted only in this file (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"from","in":"query","required":false,"description":"Inclusive UTC window start (default: 30 days before `to`)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Exclusive UTC window end (default: now); window max 731 days","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"CSV file"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/reports/bookkeeper/recipient":{"get":{"tags":["Reports"],"summary":"Bookkeeper recipient email. Owner or admin only. Delivery stays off.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Recipient email and delivery off"},"403":{"$ref":"#/components/responses/Forbidden"}}},"patch":{"tags":["Reports"],"summary":"Set or clear the bookkeeper recipient email. Each change is audited (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Updated recipient. Delivery remains off."},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/reports/bookkeeper/exports":{"get":{"tags":["Reports"],"summary":"Bookkeeper files stored in the ERP (role: admin+)","description":"Requires feature_reports. The list omits the CSV body. Delivery is off.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/reports/bookkeeper/exports/{exportId}":{"get":{"tags":["Reports"],"summary":"One stored bookkeeper file. format=csv downloads it (role: admin+)","description":"The CSV has no customer name, email, phone, or address.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"exportId","in":"path","required":true,"description":"Bookkeeper export ID","schema":{"type":"integer","minimum":1}},{"name":"format","in":"query","required":false,"description":"Return the file body","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"Report payload","content":{"application/json":{"schema":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/assets/{assetId}/prices":{"get":{"tags":["Assets"],"summary":"Append-only catalog price history (role: viewer+). Cost is omitted for viewers.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"assetId","in":"path","required":true,"description":"Asset ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Price history rows"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/settings":{"get":{"tags":["Settings"],"summary":"Read company timezone, feature switches, tax mode, markup, warranty terms, quote approval, message modes, accepted payment methods, and require_2fa_roles (role: viewer+). require_2fa_roles is not writable on this route.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Company settings. Times in the database stay UTC."}}},"patch":{"tags":["Settings"],"summary":"Update company settings, including quote approval, late fee type, and accepted payment methods (role: owner and admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"payment_methods":{"type":"array","description":"Enabled hand methods. Stored in the order cash, check, venmo, other. The default is cash and check. An empty list means none are enabled.","items":{"type":"string","enum":["cash","check","venmo","other"]}},"venmo_handle":{"type":"string","maxLength":40,"description":"Optional one-line display detail. Printed on the invoice when venmo is enabled."},"check_payable_to":{"type":"string","maxLength":120,"description":"Optional one-line name. Printed on the invoice when check is enabled."}}}}}},"responses":{"200":{"description":"Updated settings"}}}},"/api/approvals":{"get":{"tags":["Approvals"],"summary":"List approval requests. status includes draft, pending, approved, rejected, and superseded. A superseded branding profile cannot be approved. (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Approvals"],"summary":"Create a draft approval (role: employee+). A new profile_change draft marks older draft or pending profile approvals for that same company as superseded.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Draft approval"}}}},"/api/approvals/{approvalId}/submit":{"post":{"tags":["Approvals"],"summary":"Submit a draft approval (role: employee+). Submitting a profile_change marks older draft or pending profile approvals for that same company as superseded.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"approvalId","in":"path","required":true,"description":"Approval ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Pending approval"}}}},"/api/approvals/{approvalId}/decide":{"post":{"tags":["Approvals"],"summary":"Approve or reject a pending approval (role: admin+). Approving an older branding profile after a newer one exists for that company returns 409. Superseded requests cannot be approved.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"approvalId","in":"path","required":true,"description":"Approval ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Decided approval"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/jobs":{"get":{"tags":["Jobs"],"summary":"List background jobs (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Jobs"],"summary":"Enqueue the log-only example job (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Queued job"}}}},"/api/test-records/purge":{"post":{"tags":["Test records"],"summary":"Purge test records (role: owner). Money rows are voided and kept.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Purge counts"}}}},"/api/files":{"get":{"tags":["Files"],"summary":"List company files (role: helper+). Owner-only files are hidden below admin. A helper sees customer and work-order files for jobs assigned to them or booked to them on a live visit. Brand files stay visible.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"File list"}}},"post":{"tags":["Files"],"summary":"Upload a private file (role: helper or employee+). Body is application/octet-stream. A helper upload for an unassigned customer or work order returns 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Stored file metadata. Bytes are not public."}}}},"/api/files/{fileId}/download":{"get":{"tags":["Files"],"summary":"Download a file after a company and role check (role: helper+). Helpers cannot download owner-only files, or customer and work-order files outside their assigned jobs. Those return 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"fileId","in":"path","required":true,"description":"File ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"File bytes"}}}},"/api/files/{fileId}":{"delete":{"tags":["Files"],"summary":"Soft-delete a file (role: admin+). Bytes are kept so the delete can be undone.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"fileId","in":"path","required":true,"description":"File ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Soft-deleted file"}}}},"/api/files/{fileId}/restore":{"post":{"tags":["Files"],"summary":"Restore a soft-deleted file (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"fileId","in":"path","required":true,"description":"File ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored file"}}}},"/api/messages":{"get":{"tags":["Messages"],"summary":"List the outgoing message log (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Outbox rows"}}},"post":{"tags":["Messages"],"summary":"Queue or draft a customer message (role: employee+). Nothing is sent to a real provider.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Outbox row"}}}},"/api/messages/consent":{"get":{"tags":["Messages"],"summary":"Read a customer channel consent (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Consent rows"}}},"post":{"tags":["Messages"],"summary":"Grant or record an opt-out (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Consent row"}}}},"/api/message-templates":{"get":{"tags":["Messages"],"summary":"List message templates (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Templates"}}},"post":{"tags":["Messages"],"summary":"Create a message template (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Template"}}}},"/api/message-templates/{templateId}":{"delete":{"tags":["Messages"],"summary":"Soft-delete a message template (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"templateId","in":"path","required":true,"description":"Template ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deleted template"}}}},"/api/links":{"get":{"tags":["Links"],"summary":"List secure links without their tokens (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Links"}}},"post":{"tags":["Links"],"summary":"Create a secure link. The token is returned once (role: employee+).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Link plus one-time token"}}}},"/api/links/{linkId}/reissue":{"post":{"tags":["Links"],"summary":"Revoke a link and issue a replacement in one step (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"linkId","in":"path","required":true,"description":"Link ID","schema":{"type":"integer","minimum":1}}],"responses":{"201":{"description":"Replacement link and one-time token"}}}},"/api/public/links/{token}":{"get":{"tags":["Links"],"summary":"Resolve a customer link to a minimal read-only placeholder. No login.","parameters":[{"name":"token","in":"path","required":true,"description":"Unguessable link token. Only its hash is stored.","schema":{"type":"string"}}],"responses":{"200":{"description":"Placeholder view"},"404":{"description":"Link is not available"}}}},"/customer-link/{token}":{"get":{"tags":["Links"],"summary":"HTML placeholder for a customer link. No login.","parameters":[{"name":"token","in":"path","required":true,"description":"Unguessable link token. Only its hash is stored.","schema":{"type":"string"}}],"responses":{"200":{"description":"HTML placeholder"}}}},"/api/customers/duplicate-check":{"post":{"tags":["Customers"],"summary":"Find customers with the same phone, email, or address (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Possible duplicates in this company"}}}},"/api/customers/{customerId}/history":{"get":{"tags":["Customers"],"summary":"Customer record with properties, jobs, and the contact log (role: helper+). A helper who is not assigned and not booked on a live visit receives 404. Jobs in the history are the ones that helper can see.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Customer history"}}}},"/api/customers/{customerId}/properties":{"get":{"tags":["Customers"],"summary":"List properties for one customer (role: helper+). A helper who is not assigned and not booked on a live visit for that customer receives 404.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Properties"}}},"post":{"tags":["Customers"],"summary":"Add a property. Rental requires access notes (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}}],"responses":{"201":{"description":"Property and service-area check"}}}},"/api/customers/{customerId}/properties/{propertyId}":{"patch":{"tags":["Customers"],"summary":"Update a property (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}},{"name":"propertyId","in":"path","required":true,"description":"Property ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Updated property"}}},"delete":{"tags":["Customers"],"summary":"Soft-delete a property (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}},{"name":"propertyId","in":"path","required":true,"description":"Property ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Soft-deleted property"}}}},"/api/customers/{customerId}/properties/{propertyId}/restore":{"post":{"tags":["Customers"],"summary":"Undo a soft-deleted property (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}},{"name":"propertyId","in":"path","required":true,"description":"Property ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored property"}}}},"/api/settings/job-types":{"get":{"tags":["Settings"],"summary":"List this company's job types (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Job types"}}},"post":{"tags":["Settings"],"summary":"Add a job type (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Job type"}}}},"/api/settings/lead-sources":{"get":{"tags":["Settings"],"summary":"List this company's lead sources (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Lead sources"}}},"post":{"tags":["Settings"],"summary":"Add a lead source (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Lead source"}}}},"/api/service-area/check":{"post":{"tags":["Territories"],"summary":"Check a latitude and longitude against saved service-area boundaries. Does not geocode (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Inside, outside, or unknown"}}}},"/api/customers/{customerId}/restore":{"post":{"tags":["Customers"],"summary":"Undo a soft-deleted customer (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"customerId","in":"path","required":true,"description":"Customer ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored customer"}}}},"/api/auth/totp/setup":{"post":{"tags":["Auth"],"summary":"Start two-step sign-in. The secret is returned once. An enroll session may call this.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","description":"Required only when two-step is already on."},"totp_code":{"type":"string","description":"Required only when two-step is already on."}}}}}},"responses":{"200":{"description":"data.secret and data.otpauth_url. The secret is shown once and stored only as a pending secret until confirm."},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"Two-step sign-in is off for this company and this role is not required to enroll"}}}},"/api/auth/totp/confirm":{"post":{"tags":["Auth"],"summary":"Confirm a two-step code, turn it on, show recovery codes once, and issue a full session token. An enroll session may call this.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["totp_code"],"properties":{"totp_code":{"type":"string","description":"The same code is refused again inside its 30 second window."}}}}}},"responses":{"200":{"description":"data.totp_enabled is true, data.token is a full session (enroll_2fa is absent; the enroll token is dead), and data.recovery_codes is eight XXXX-XXXX codes shown once."},"401":{"description":"Two-step code is not valid, including a code already used in its 30 second window."},"409":{"description":"Two-step sign-in is already confirmed"},"429":{"description":"Too many wrong two-step codes for this person. The Retry-After header says how many seconds to wait."}}}},"/api/auth/totp/disable":{"post":{"tags":["Auth"],"summary":"Turn two-step sign-in off and issue a new token. Refused when this role must keep two-step on.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password","totp_code"],"properties":{"password":{"type":"string"},"totp_code":{"type":"string"}}}}}},"responses":{"200":{"description":"data.totp_enabled is false and data.token replaces the previous token. Recovery codes are removed."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"code TWO_FACTOR_ENROLL_REQUIRED when this session has not finished enrollment"},"409":{"description":"Two-step sign-in is required for this role"},"429":{"description":"Too many wrong two-step codes for this person. The Retry-After header says how many seconds to wait."}}}},"/api/auth/totp/recovery-codes":{"post":{"tags":["Auth"],"summary":"Issue a new set of recovery codes when none are unused. A full session is required.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password","totp_code"],"properties":{"password":{"type":"string"},"totp_code":{"type":"string"}}}}}},"responses":{"200":{"description":"data.recovery_codes is eight new XXXX-XXXX codes, shown once. Previous codes are removed."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"code TWO_FACTOR_ENROLL_REQUIRED when this session has not finished enrollment"},"409":{"description":"Recovery codes are already set, or two-step sign-in is not on"},"429":{"description":"Too many wrong two-step codes for this person. The Retry-After header says how many seconds to wait."}}}},"/api/memberships/{membershipId}/deactivate":{"post":{"tags":["Memberships"],"summary":"Deactivate a membership. The row stays, with who and when. Requires members.manage. Deactivating yourself returns 409 code SELF_CHANGE_NOT_ALLOWED.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"membershipId","in":"path","required":true,"description":"Membership ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Deactivated membership"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/memberships/{membershipId}/restore":{"post":{"tags":["Memberships"],"summary":"Restore a deactivated membership. Requires members.manage. An owner membership stays with an owner. Restoring yourself returns 409 code SELF_CHANGE_NOT_ALLOWED.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"membershipId","in":"path","required":true,"description":"Membership ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored membership"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/access-keys":{"get":{"tags":["Access keys"],"summary":"List this company's access keys. Hashes and raw keys are omitted (role: owner)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Access keys"}}},"post":{"tags":["Access keys"],"summary":"Issue an access key. The raw key is returned once (role: owner)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Issued access key"}}}},"/api/access-keys/{accessKeyId}/revoke":{"post":{"tags":["Access keys"],"summary":"Revoke an access key (role: owner)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"accessKeyId","in":"path","required":true,"description":"Access key ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Revoked access key"}}}},"/api/export":{"get":{"tags":["Export"],"summary":"Write this company's records to a local file. No cloud upload (role: owner)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Local export file name"}}}},"/api/public/forms":{"post":{"tags":["Public"],"summary":"Check a public form submission. The body is not stored.","responses":{"202":{"description":"Accepted and discarded"}}}},"/api/audit-logs":{"get":{"tags":["Audit Logs"],"summary":"List tenant audit log entries (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"action","in":"query","required":false,"description":"Filter by exact action name (e.g. invoice.create)","schema":{"type":"string"}},{"name":"user_id","in":"query","required":false,"description":"Filter by actor user_id","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/imports/presets":{"get":{"tags":["Imports"],"summary":"List import targets and column presets (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Presets"},"409":{"description":"Imports are turned off"}}}},"/api/imports/preview":{"post":{"tags":["Imports"],"summary":"Preview a CSV import without writing business records (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"201":{"description":"Preview batch"},"409":{"description":"Imports are turned off"}}}},"/api/imports/{batchId}":{"get":{"tags":["Imports"],"summary":"Read one import batch (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"batchId","in":"path","required":true,"description":"Import batch ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Batch"}}}},"/api/imports/{batchId}/confirm":{"post":{"tags":["Imports"],"summary":"Write a previewed import batch (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"batchId","in":"path","required":true,"description":"Import batch ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Confirmed batch"}}}},"/api/imports/{batchId}/undo":{"post":{"tags":["Imports"],"summary":"Soft-close every record a batch created (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"batchId","in":"path","required":true,"description":"Import batch ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Undone batch"}}}},"/api/imports/{batchId}/restore":{"post":{"tags":["Imports"],"summary":"Restore records closed by undo (role: owner or admin)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"batchId","in":"path","required":true,"description":"Import batch ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Restored batch"}}}},"/api/price-list":{"get":{"tags":["Price list"],"summary":"List this company's service prices (role: viewer+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/OffsetParam"},{"$ref":"#/components/parameters/PageParam"},{"name":"search","in":"query","required":false,"description":"Code, name, public name, or category","schema":{"type":"string"}},{"name":"category","in":"query","required":false,"description":"Exact category","schema":{"type":"string"}},{"name":"active","in":"query","required":false,"description":"Admin only: 1, 0, or all","schema":{"type":"string"}},{"name":"include_test","in":"query","required":false,"description":"Owner or admin: 1 includes test rows when the switch is live","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated list response","content":{"application/json":{"schema":{"type":"object","required":["status","data","pagination"],"properties":{"status":{"type":"string","const":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ServicePriceItem"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Price list"],"summary":"Add a service price (role: admin+). Requires feature_price_list.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePriceItemWrite"}}}},"responses":{"201":{"description":"Created service price"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/price-list/rules":{"get":{"tags":["Price list"],"summary":"Read pricing rules (role: viewer+). No cost or margin.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Job minimum and discount cap. hourly_floor_cents is owner and admin only."}}},"patch":{"tags":["Price list"],"summary":"Change pricing rules (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"responses":{"200":{"description":"Saved rules"}}}},"/api/price-list/check":{"post":{"tags":["Price list"],"summary":"Check a draft set of lines and return violations plus a top-up line. Writes nothing (role: viewer+).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lines"],"properties":{"lines":{"type":"array","items":{"$ref":"#/components/schemas/PriceCheckLine"}}}}}}},"responses":{"200":{"description":"Violations and optional top-up line. Nothing is stored."}}}},"/api/price-list/{itemId}":{"get":{"tags":["Price list"],"summary":"Read one service price (role: viewer+). Price and cost follow the role.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"itemId","in":"path","required":true,"description":"Service price ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Service price"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Price list"],"summary":"Change a service price (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"itemId","in":"path","required":true,"description":"Service price ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Updated service price"}}},"delete":{"tags":["Price list"],"summary":"Soft-delete a service price (role: admin+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"itemId","in":"path","required":true,"description":"Service price ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Soft-deleted service price"}}}},"/api/price-list/{itemId}/restore":{"post":{"tags":["Price list"],"summary":"Restore a soft-deleted service price (role: admin+). Optional code when the old code is in use.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"itemId","in":"path","required":true,"description":"Service price ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","description":"New code when a live service already uses this one"}}}}}},"responses":{"200":{"description":"Restored service price"},"409":{"description":"The code is already used by a live service"}}}},"/api/price-list/{itemId}/prices":{"get":{"tags":["Price list"],"summary":"Append-only price history (role: viewer+). Cost is admin and owner only.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"itemId","in":"path","required":true,"description":"Service price ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Price history"}}}},"/api/estimates/{estimateId}/price-lines":{"get":{"tags":["Quotes"],"summary":"List price-list lines on a quote, including the minimum top-up (role: viewer+). The frozen cost is admin and owner only.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Frozen price lines and any minimum top-up","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"estimate_line_item_id":{"type":"integer"},"estimate_id":{"type":"integer"},"price_item_id":{"type":["integer","null"]},"code":{"type":["string","null"]},"name":{"type":"string"},"unit":{"type":"string","enum":["each","hour","sq_ft","linear_ft","job","day"]},"quantity":{"type":"number"},"unit_price_cents":{"type":"integer","minimum":0},"line_total_cents":{"type":"integer","minimum":0},"unit_cost_cents":{"type":["integer","null"],"minimum":0,"description":"Frozen cost snapshot. Returned only for admin and owner."},"line_kind":{"type":"string"},"line_source":{"type":"string","enum":["price_item","minimum_top_up","discount","custom"]}}}}}}}}},"post":{"tags":["Quotes"],"summary":"Add a price-list item, a custom line, or a discount line (role: employee+). Copies a price-list sell price and enforces pricing rules.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"price_item_id":{"type":"integer"},"quantity":{"type":"number"},"kind":{"type":"string","enum":["discount"]},"description":{"type":"string"},"unit":{"type":"string","enum":["each","hour","sq_ft","linear_ft","job","day"]},"unit_price_cents":{"type":"integer","minimum":0},"line_kind":{"type":"string","enum":["service","hardware","labor","material","discount"]}}}}}},"responses":{"201":{"description":"Line added. A minimum top-up is included when the check returns one."},"400":{"description":"A discount cap or hourly floor blocks the change. Nothing is saved. The message has no hourly floor amount unless the reader is an owner or admin."}}}},"/api/estimates/{estimateId}/price-lines/{lineItemId}":{"patch":{"tags":["Quotes"],"summary":"Change quantity, or a discount, and re-check pricing rules (role: employee+). The copied sell price stays frozen.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Estimate line item ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Line updated"},"400":{"description":"A pricing rule blocks the change. Nothing is saved. The hourly floor amount is owner and admin only."}}},"delete":{"tags":["Quotes"],"summary":"Remove a picked line and re-check pricing rules (role: employee+)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TenantIdHeader"},{"name":"estimateId","in":"path","required":true,"description":"Estimate ID","schema":{"type":"integer","minimum":1}},{"name":"lineItemId","in":"path","required":true,"description":"Estimate line item ID","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Line removed"},"400":{"description":"A pricing rule blocks the change. Nothing is saved. The hourly floor amount is owner and admin only."}}}}}}