{
  "openapi": "3.1.0",
  "info": {
    "title": "Novel Systems API",
    "version": "1.0.0",
    "summary": "Multi-tenant CPQ and field-service dispatch for specialty-trade contractors.",
    "description": "Generated from the Zod schemas the server validates against — see\n`backend/src/schemas/` and `backend/scripts/generate-openapi.ts`. Do not\nhand-edit this file; `npm run openapi:check` will fail on it in CI.\n\nEvery route below except `/api/health` requires a bearer token from\n`POST /api/v1/auth/login`. Tokens are scoped to one organization and one\nrole; the row-level security policies on the database enforce the same\nboundary independently, so a token cannot read another tenant's data even\nif a handler forgets to filter.\n\nMoney is always an integer count of cents. Exact decimals — margins, rate\nmultipliers — are strings, never JSON numbers, so that a client cannot\nsilently parse them into a float.",
    "contact": {
      "name": "Novel Systems support",
      "url": "https://novelsystems.ca/contact"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Novel-Systems-Commercial"
    }
  },
  "servers": [
    {
      "url": "https://novel-systems-backend.vercel.app",
      "description": "Production. Canadian data residency. This is the host that answers today; api.novelsystems.ca is not yet provisioned and is deliberately not listed."
    }
  ],
  "tags": [
    {
      "name": "Infrastructure",
      "description": "Unauthenticated liveness."
    },
    {
      "name": "Authentication",
      "description": "Tokens and identity."
    },
    {
      "name": "Quotes",
      "description": "Server-side pricing. ADMIN and SALES only."
    },
    {
      "name": "Work orders",
      "description": "Dispatch and field status. TECHNICIAN-readable."
    },
    {
      "name": "Integrations",
      "description": "Stripe, QuickBooks and Salesforce. ADMIN only, except the two endpoints the providers themselves call."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": [
          "Infrastructure"
        ],
        "operationId": "getHealth",
        "summary": "Liveness and database reachability.",
        "description": "Unauthenticated and outside /v1 on purpose: it is infrastructure, not API surface, and the load balancer that calls it has no credentials. Returns 503 rather than 200-with-a-status-field, because a green check on a broken service is worse than no check at all.",
        "security": [],
        "responses": {
          "200": {
            "description": "The process can reach its database.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "ok"
                          ]
                        },
                        "database": {
                          "type": "string",
                          "enum": [
                            "reachable"
                          ]
                        },
                        "latency_ms": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "build": {
                          "type": "object",
                          "properties": {
                            "commit": {
                              "type": "string",
                              "nullable": true
                            },
                            "ref": {
                              "type": "string",
                              "nullable": true
                            },
                            "environment": {
                              "type": "string",
                              "nullable": true
                            }
                          },
                          "required": [
                            "commit",
                            "ref",
                            "environment"
                          ],
                          "additionalProperties": false,
                          "description": "Which commit, branch and environment this deployment was built from."
                        },
                        "timestamp": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "status",
                        "database",
                        "latency_ms",
                        "build",
                        "timestamp"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "degraded"
                          ]
                        },
                        "database": {
                          "type": "string",
                          "enum": [
                            "unreachable"
                          ]
                        },
                        "build": {
                          "type": "object",
                          "properties": {
                            "commit": {
                              "type": "string",
                              "nullable": true
                            },
                            "ref": {
                              "type": "string",
                              "nullable": true
                            },
                            "environment": {
                              "type": "string",
                              "nullable": true
                            }
                          },
                          "required": [
                            "commit",
                            "ref",
                            "environment"
                          ],
                          "additionalProperties": false,
                          "description": "Which commit, branch and environment this deployment was built from."
                        },
                        "timestamp": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "status",
                        "database",
                        "build",
                        "timestamp"
                      ],
                      "additionalProperties": false
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "The process cannot reach its database.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "ok"
                          ]
                        },
                        "database": {
                          "type": "string",
                          "enum": [
                            "reachable"
                          ]
                        },
                        "latency_ms": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "build": {
                          "type": "object",
                          "properties": {
                            "commit": {
                              "type": "string",
                              "nullable": true
                            },
                            "ref": {
                              "type": "string",
                              "nullable": true
                            },
                            "environment": {
                              "type": "string",
                              "nullable": true
                            }
                          },
                          "required": [
                            "commit",
                            "ref",
                            "environment"
                          ],
                          "additionalProperties": false,
                          "description": "Which commit, branch and environment this deployment was built from."
                        },
                        "timestamp": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "status",
                        "database",
                        "latency_ms",
                        "build",
                        "timestamp"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "degraded"
                          ]
                        },
                        "database": {
                          "type": "string",
                          "enum": [
                            "unreachable"
                          ]
                        },
                        "build": {
                          "type": "object",
                          "properties": {
                            "commit": {
                              "type": "string",
                              "nullable": true
                            },
                            "ref": {
                              "type": "string",
                              "nullable": true
                            },
                            "environment": {
                              "type": "string",
                              "nullable": true
                            }
                          },
                          "required": [
                            "commit",
                            "ref",
                            "environment"
                          ],
                          "additionalProperties": false,
                          "description": "Which commit, branch and environment this deployment was built from."
                        },
                        "timestamp": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "status",
                        "database",
                        "build",
                        "timestamp"
                      ],
                      "additionalProperties": false
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/routes": {
      "get": {
        "tags": [
          "Infrastructure"
        ],
        "operationId": "getRoutes",
        "summary": "The route table of the process answering this request.",
        "description": "Exists so that a contract check can fail. The obvious way to ask a live\ndeployment whether it serves a path is to call it unauthenticated and read the\nstatus, and that does not work here: the guards are mounted on the routers\nrather than on each route, so authentication runs before Express matches a\npath within the router and a real path and an invented one both answer 401\nwith the same envelope. Under `/api/v1` that test cannot fail, which is a\nworse state than having no test.\n\nSo the process says what it serves, walked from the router stack it actually\ndispatches against — not parsed from source, because the deployed artefact is\nbuilt from a different repository and reading source in CI cannot describe it.\n`scripts/check-deployed-api.mjs` diffs this against the document you are\nreading now.\n\nUnauthenticated, and it discloses nothing this document does not already\npublish: methods and paths, no handlers, no schemas, no configuration. The\nauthorisation on each route is the control, and it is unchanged.",
        "security": [],
        "responses": {
          "200": {
            "description": "Every method and path this deployment will dispatch, sorted by path then method so two deployments compare as text.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "routes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "method": {
                            "type": "string",
                            "enum": [
                              "GET",
                              "POST",
                              "PATCH",
                              "PUT",
                              "DELETE"
                            ]
                          },
                          "path": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "method",
                          "path"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "count",
                    "routes"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        }
      }
    },
    "/api/connectors": {
      "get": {
        "tags": [
          "Infrastructure"
        ],
        "operationId": "getConnectors",
        "summary": "Which of the three integrations this deployment can actually begin.",
        "description": "Unauthenticated, for the audience that cannot authenticate: someone\nevaluating whether a connector they read about on the marketing site is a\nswitch they can flip. The field mappings, the system-of-record rules and the\ndeployment path are all published as prose. This is the endpoint behind them.\n\nThree states, derived from the adapters rather than typed anywhere:\n\n- `operator_credentials_missing` — this deployment holds no application\n  credentials for the provider. There is no consent screen to send anyone to,\n  and no tenant can begin. The connector is built; it is not connectable here.\n- `awaiting_tenant_consent` — the platform side is complete. Each organisation\n  authorises its own account and nothing syncs until an administrator does.\n- `platform_account_live` — configured, and the account is ours rather than the\n  tenant's. Stripe is the only provider shaped this way.\n\nIt reports no tenant rows and does not name the environment variables behind a\ngap. Those stay behind the ADMIN guard on `GET /api/v1/integrations`, which\nanswers the operator's question — *what do I set* — rather than the buyer's.\nWhether a Connect button would work is a fact anyone can establish by pressing\nit; the name of the variable holding a client secret is not.",
        "security": [],
        "responses": {
          "200": {
            "description": "Every provider this build ships an adapter for, in registry order, with the state each is actually in right now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "connectors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string",
                            "enum": [
                              "stripe",
                              "quickbooks",
                              "salesforce"
                            ]
                          },
                          "display_name": {
                            "type": "string"
                          },
                          "platform_ready": {
                            "type": "boolean"
                          },
                          "readiness": {
                            "type": "string",
                            "enum": [
                              "operator_credentials_missing",
                              "awaiting_tenant_consent",
                              "platform_account_live"
                            ]
                          },
                          "summary": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "slug",
                          "display_name",
                          "platform_ready",
                          "readiness",
                          "summary"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "count",
                    "connectors"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "login",
        "summary": "Exchange workspace, email and password for a bearer token.",
        "description": "The workspace subdomain is part of the credential, not something discovered from the email: addresses are unique per organization, not globally. Wrong password, unknown email, unknown workspace and disabled account all return the same 401 with the same timing.",
        "security": [],
        "requestBody": {
          "description": "Workspace, email and password.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subdomain": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 63,
                    "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 320
                  },
                  "password": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  }
                },
                "required": [
                  "subdomain",
                  "email",
                  "password"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A token and the caller's identity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expires_in": {
                      "type": "integer",
                      "exclusiveMinimum": true,
                      "minimum": 0
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "ADMIN",
                            "SALES",
                            "TECHNICIAN"
                          ]
                        },
                        "organization_id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      },
                      "required": [
                        "id",
                        "email",
                        "role",
                        "organization_id"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "access_token",
                    "token_type",
                    "expires_in",
                    "user"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Credentials rejected. The body does not say which part was wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Ten attempts per fifteen minutes against one account. Keyed on the workspace and email being attempted, not on the caller's IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "operationId": "getCurrentUser",
        "summary": "The caller's own profile and organization.",
        "description": "Exists so a client can recover its identity after a reload without decoding the token itself. The role comes from the database, not from the token, so a role changed after issue takes effect on the next call.",
        "responses": {
          "200": {
            "description": "The caller's profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "full_name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "ADMIN",
                        "SALES",
                        "TECHNICIAN"
                      ]
                    },
                    "last_login_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "organization": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "subdomain": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "subdomain"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "id",
                    "email",
                    "full_name",
                    "role",
                    "last_login_at",
                    "organization"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes": {
      "get": {
        "tags": [
          "Quotes"
        ],
        "operationId": "listQuotes",
        "summary": "The tenant's quotes, newest first.",
        "description": "Cursor paginated, not offset: this table grows while you read it, and offsets both skip and repeat rows when a colleague saves a quote mid-scroll. Pass the `next_cursor` from the previous page as `cursor`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "DRAFT",
                "SENT",
                "APPROVED",
                "REJECTED",
                "EXPIRED"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of quotes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "quote_number": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "DRAFT",
                              "SENT",
                              "APPROVED",
                              "REJECTED",
                              "EXPIRED"
                            ]
                          },
                          "total_amount_cents": {
                            "type": "integer"
                          },
                          "margin_percent": {
                            "type": "string",
                            "description": "An exact decimal, as a string. Do not parse it into a float."
                          },
                          "customer_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          "created_by": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string",
                                "format": "email"
                              }
                            },
                            "required": [
                              "id",
                              "name",
                              "email"
                            ],
                            "additionalProperties": false,
                            "nullable": true
                          }
                        },
                        "required": [
                          "id",
                          "quote_number",
                          "status",
                          "total_amount_cents",
                          "margin_percent",
                          "customer_name",
                          "created_at",
                          "created_by"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    }
                  },
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token. This resource carries cost and margin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes/calculate": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "operationId": "calculateQuote",
        "summary": "Price a configuration server-side.",
        "description": "The client sends dimensions and SKU codes and nothing else. It does not send\ncosts, and if it did they would be ignored: every figure in the response is\nresolved from the tenant's own rate card inside the transaction.\n\n`targetMargin` is a fraction — 0.50, never 50. Omitting it prices at the\nfloor. A value above the floor is accepted without ceremony; a value below it\nrequires `marginOverride.reason` and an ADMIN token, and the reason is stored\non the quote. See D-001 in DECISIONS.md.\n\n`persist: false` prices and discards. `persist: true` writes the quote and\nreturns its id and per-tenant sequential number.",
        "requestBody": {
          "description": "Labour tier, line items, margin intent.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "laborTierCode": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 32
                  },
                  "lineItems": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "reference": {
                          "type": "string",
                          "maxLength": 120
                        },
                        "widthInches": {
                          "type": "number",
                          "exclusiveMinimum": true,
                          "minimum": 0
                        },
                        "dropInches": {
                          "type": "number",
                          "exclusiveMinimum": true,
                          "minimum": 0
                        },
                        "quantity": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 500
                        },
                        "fabricSkuCode": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        },
                        "hardwareSkuCode": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        },
                        "motorSkuCode": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        },
                        "trimSkuCode": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        }
                      },
                      "required": [
                        "widthInches",
                        "dropInches",
                        "quantity",
                        "fabricSkuCode",
                        "hardwareSkuCode"
                      ],
                      "additionalProperties": false
                    },
                    "minItems": 1,
                    "maxItems": 200
                  },
                  "targetMargin": {
                    "type": "number",
                    "minimum": 0,
                    "exclusiveMaximum": true,
                    "maximum": 1
                  },
                  "marginOverride": {
                    "type": "object",
                    "properties": {
                      "reason": {
                        "type": "string",
                        "minLength": 10,
                        "maxLength": 500
                      }
                    },
                    "required": [
                      "reason"
                    ],
                    "additionalProperties": false
                  },
                  "customer": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200
                      },
                      "email": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 320
                      }
                    },
                    "required": [
                      "name"
                    ],
                    "additionalProperties": false
                  },
                  "persist": {
                    "type": "boolean",
                    "default": false
                  }
                },
                "required": [
                  "laborTierCode",
                  "lineItems"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The priced quote. Cut sizes are per unit; totals are for the whole order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currency": {
                      "type": "string",
                      "enum": [
                        "CAD"
                      ]
                    },
                    "labor_tier": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "hourly_rate_cents": {
                          "type": "integer"
                        },
                        "multiplier": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        }
                      },
                      "required": [
                        "code",
                        "name",
                        "hourly_rate_cents",
                        "multiplier"
                      ],
                      "additionalProperties": false
                    },
                    "margin_floor": {
                      "type": "string",
                      "description": "An exact decimal, as a string. Do not parse it into a float."
                    },
                    "applied_margin": {
                      "type": "string",
                      "description": "An exact decimal, as a string. Do not parse it into a float."
                    },
                    "floor_overridden": {
                      "type": "boolean"
                    },
                    "target_margin": {
                      "type": "string",
                      "description": "Deprecated. An alias of applied_margin."
                    },
                    "line_items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "reference": {
                            "type": "string",
                            "nullable": true
                          },
                          "width_inches": {
                            "type": "number"
                          },
                          "drop_inches": {
                            "type": "number"
                          },
                          "quantity": {
                            "type": "integer"
                          },
                          "opening_square_feet": {
                            "type": "string",
                            "description": "An exact decimal, as a string. Do not parse it into a float."
                          },
                          "fabric_square_feet_with_waste": {
                            "type": "string",
                            "description": "An exact decimal, as a string. Do not parse it into a float."
                          },
                          "labour_hours": {
                            "type": "string",
                            "description": "An exact decimal, as a string. Do not parse it into a float."
                          },
                          "cut_list": {
                            "type": "object",
                            "properties": {
                              "tube_length_inches": {
                                "type": "string",
                                "description": "An exact decimal, as a string. Do not parse it into a float."
                              },
                              "hembar_length_inches": {
                                "type": "string",
                                "description": "An exact decimal, as a string. Do not parse it into a float."
                              },
                              "fabric_width_inches": {
                                "type": "string",
                                "description": "An exact decimal, as a string. Do not parse it into a float."
                              },
                              "fabric_drop_inches": {
                                "type": "string",
                                "description": "An exact decimal, as a string. Do not parse it into a float."
                              },
                              "fabric_square_feet": {
                                "type": "string",
                                "description": "An exact decimal, as a string. Do not parse it into a float."
                              }
                            },
                            "required": [
                              "tube_length_inches",
                              "hembar_length_inches",
                              "fabric_width_inches",
                              "fabric_drop_inches",
                              "fabric_square_feet"
                            ],
                            "additionalProperties": false,
                            "description": "Per-unit cut sizes in inches. Identical to what the browser sandbox shows."
                          },
                          "components": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "component": {
                                  "type": "string",
                                  "enum": [
                                    "fabric",
                                    "hardware",
                                    "motor",
                                    "trim",
                                    "labour"
                                  ]
                                },
                                "sku_code": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "description": {
                                  "type": "string"
                                },
                                "quantity": {
                                  "type": "string",
                                  "description": "An exact decimal, as a string. Do not parse it into a float."
                                },
                                "unit": {
                                  "type": "string"
                                },
                                "unit_cost_cents": {
                                  "type": "string",
                                  "description": "An exact decimal, as a string. Do not parse it into a float."
                                },
                                "extended_cost_cents": {
                                  "type": "integer"
                                }
                              },
                              "required": [
                                "component",
                                "sku_code",
                                "description",
                                "quantity",
                                "unit",
                                "unit_cost_cents",
                                "extended_cost_cents"
                              ],
                              "additionalProperties": false
                            }
                          },
                          "cost_cents": {
                            "type": "integer"
                          },
                          "price_cents": {
                            "type": "integer"
                          }
                        },
                        "required": [
                          "reference",
                          "width_inches",
                          "drop_inches",
                          "quantity",
                          "opening_square_feet",
                          "fabric_square_feet_with_waste",
                          "labour_hours",
                          "cut_list",
                          "components",
                          "cost_cents",
                          "price_cents"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "totals": {
                      "type": "object",
                      "properties": {
                        "cost_cents": {
                          "type": "integer"
                        },
                        "amount_cents": {
                          "type": "integer"
                        },
                        "profit_cents": {
                          "type": "integer"
                        },
                        "margin_percent": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        }
                      },
                      "required": [
                        "cost_cents",
                        "amount_cents",
                        "profit_cents",
                        "margin_percent"
                      ],
                      "additionalProperties": false
                    },
                    "assumptions": {
                      "type": "object",
                      "properties": {
                        "fabric_waste_factor": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        },
                        "labour_base_hours_per_unit": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        },
                        "labour_hours_per_square_foot": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        },
                        "labour_motorisation_hours": {
                          "type": "string",
                          "description": "An exact decimal, as a string. Do not parse it into a float."
                        }
                      },
                      "required": [
                        "fabric_waste_factor",
                        "labour_base_hours_per_unit",
                        "labour_hours_per_square_foot",
                        "labour_motorisation_hours"
                      ],
                      "additionalProperties": false
                    },
                    "quote_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "quote_number": {
                      "type": "integer",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "DRAFT",
                        "SENT",
                        "APPROVED",
                        "REJECTED",
                        "EXPIRED"
                      ],
                      "nullable": true
                    },
                    "persisted": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "currency",
                    "labor_tier",
                    "margin_floor",
                    "applied_margin",
                    "floor_overridden",
                    "target_margin",
                    "line_items",
                    "totals",
                    "assumptions",
                    "quote_id",
                    "quote_number",
                    "status",
                    "persisted"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "A SKU code, labour tier, or margin the engine refused — including a margin below the floor without an override.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token, or an override attempted by a non-ADMIN.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes/{id}/approve": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "operationId": "approveQuote",
        "summary": "Approve a quote and fire `quote.approved`.",
        "description": "Its own endpoint rather than a PATCH on status, because approval is the moment a quote stops being a working document and becomes a commitment. It has side effects — webhooks to the tenant's accounting system — that a general-purpose field update should not be able to trigger by accident.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The approved quote.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "quote_number": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "DRAFT",
                        "SENT",
                        "APPROVED",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    },
                    "approved_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    }
                  },
                  "required": [
                    "id",
                    "quote_number",
                    "status",
                    "approved_at"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No quote with that id in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The quote is not in a state that can be approved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes/{id}/checkout-session": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "operationId": "createQuoteCheckoutSession",
        "summary": "Mint a hosted Stripe payment page for an approved quote.",
        "description": "The *pay now* path. Approval already fires an invoice with net-14 terms\nthrough the integration fan-out, which is what a commercial buyer expects;\nthis endpoint is the other option — a hosted card page, returned as a URL\nthe product can put behind a button. Neither is the default, because which\none a job wants is a commercial question.\n\nAmounts are the quote's own per-line integers, sent with `quantity: 1` so\nStripe cannot recompute a line and land a cent away from the document that\nwas approved. HST is an explicit thirteen-percent line, not Stripe Tax,\nwhich computes zero until a registration is configured and does so\nsilently.\n\n`return_url` is optional and must be an origin already in\n`CORS_ALLOWED_ORIGINS`. A caller-supplied redirect echoed into a page\nhosted on `checkout.stripe.com` is an open redirect on an origin both a\nphishing filter and a human trust, so the allowlist is checked rather than\nthe string merely being parsed. Omitting it returns the payer to the first\nconfigured origin.\n\nCalling twice returns the still-open session with `reused: true` rather\nthan minting a second live payment link. Once the quote is paid the\nendpoint returns 409 — a second payable page for a settled job is how a\ncustomer pays twice.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "Optional return target. Send `{}` to use the deployment's default origin.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "return_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2000
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session created by an earlier call, still open. `reused` is true and no second payment link was minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "quote_number": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true
                    },
                    "payment_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "amount_total_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "currency": {
                      "type": "string",
                      "nullable": true
                    },
                    "expires_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "reused": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "session_id",
                    "quote_number",
                    "url",
                    "status",
                    "payment_status",
                    "amount_total_cents",
                    "currency",
                    "expires_at",
                    "reused"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "201": {
            "description": "A newly created payable page. `url` is where to send the payer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "quote_number": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true
                    },
                    "payment_status": {
                      "type": "string",
                      "nullable": true
                    },
                    "amount_total_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "currency": {
                      "type": "string",
                      "nullable": true
                    },
                    "expires_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "reused": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "session_id",
                    "quote_number",
                    "url",
                    "status",
                    "payment_status",
                    "amount_total_cents",
                    "currency",
                    "expires_at",
                    "reused"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The quote is not approved, or `return_url` is not an origin this deployment will send a payer to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No quote with that id in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The quote is already paid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`STRIPE_SECRET_KEY` is not set on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quotes/{id}/invoice": {
      "get": {
        "tags": [
          "Quotes"
        ],
        "operationId": "getQuoteInvoice",
        "summary": "The Stripe invoice written when this quote was approved.",
        "description": "Approval fans out to the configured connectors, and Stripe's adapter\nresponds by resolving a customer, posting one invoice item per quote line\nplus an explicit HST line, creating the invoice and finalising it. This\nendpoint returns that document. It is a read — nothing at Stripe is created\nor changed by calling it.\n\nIt is the companion to `POST /quotes/{id}/checkout-session`, not a\nduplicate of it. That endpoint mints a hosted card page for paying now;\nthis one returns the net-14 invoice that already exists, with\n`hosted_invoice_url` and `invoice_pdf_url` pointing at Stripe's own copies.\n\n`mode` is derived from the `livemode` flag Stripe stamps on the object, not\nfrom which key the deployment holds — so a caller asking whether it is\nlooking at test data gets an answer that travelled with the artifact.\n\n`tax_cents` is nullable and the nullability is deliberate. This platform\ndoes not use Stripe Tax, which computes zero until a registration is\nconfigured on the account and does so silently; HST is posted as an\nordinary line and identified on the way back by the description we wrote.\nNull means no line carried that label. Zero would mean the invoice has no\ntax on it, which on a Canadian total is a different claim.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The finalised invoice, with its line items and Stripe's hosted copies.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quote_number": {
                      "type": "integer"
                    },
                    "invoice_id": {
                      "type": "string"
                    },
                    "number": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "test",
                        "live",
                        "unknown"
                      ]
                    },
                    "currency": {
                      "type": "string"
                    },
                    "subtotal_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "tax_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "total_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "amount_due_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "hosted_invoice_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "invoice_pdf_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "lines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "description": {
                            "type": "string"
                          },
                          "amount_cents": {
                            "type": "integer"
                          },
                          "is_tax": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "description",
                          "amount_cents",
                          "is_tax"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "created": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "quote_number",
                    "invoice_id",
                    "number",
                    "status",
                    "mode",
                    "currency",
                    "subtotal_cents",
                    "tax_cents",
                    "total_cents",
                    "amount_due_cents",
                    "created_at",
                    "hosted_invoice_url",
                    "invoice_pdf_url",
                    "lines",
                    "created"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No quote with that id in this tenant, or no invoice was recorded for it — a quote produces one when it is approved. `POST` to this same path writes the missing one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`STRIPE_SECRET_KEY` is not set on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Quotes"
        ],
        "operationId": "writeQuoteInvoice",
        "summary": "Make sure an approved quote has an invoice at Stripe.",
        "description": "The repair path for the `GET` above, and the only way an already-approved\nquote can acquire an invoice.\n\nApproval fires the connector fan-out exactly once and `POST /approve`\nanswers 409 on a second attempt — deliberately, because two approvals\nwould mean two invoices for one job. The consequence is that a quote whose\nStripe leg failed, or one approved before the connector existed, had no\nsecond chance. This is it.\n\nIdempotent, and by three independent mechanisms rather than one. A\nrecorded successful sync short-circuits before any call to Stripe and the\nexisting invoice is returned unchanged. Every mutating call the adapter\nmakes carries an `Idempotency-Key` derived from the quote id, so a retry\nafter a timeout re-reads Stripe's stored response rather than creating a\nsecond document. And the sync table's unique key on\n`(organization, provider, entity type, quote)` makes the record itself\nsingle. So the accurate description is not \"this creates an invoice\" but\n\"this ensures exactly one exists\" — `created` tells you which case you\nwere in, and `201` versus `200` says the same thing.\n\nIt repairs one connector for one quote. It does not re-fire the tenant's\n`quote.approved` webhook or the QuickBooks and Salesforce legs: a missing\nStripe invoice is not a reason to tell everyone else the quote was\napproved a second time.\n\nAmounts come from the calculation stored at approval, never from a\nrecalculation. On a path that may run an arbitrary time after the quote\nwas agreed, today's SKU costs would bill a different document from the one\nthe customer accepted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An invoice already existed for this quote; it is returned untouched, with `created: false`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quote_number": {
                      "type": "integer"
                    },
                    "invoice_id": {
                      "type": "string"
                    },
                    "number": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "test",
                        "live",
                        "unknown"
                      ]
                    },
                    "currency": {
                      "type": "string"
                    },
                    "subtotal_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "tax_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "total_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "amount_due_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "hosted_invoice_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "invoice_pdf_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "lines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "description": {
                            "type": "string"
                          },
                          "amount_cents": {
                            "type": "integer"
                          },
                          "is_tax": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "description",
                          "amount_cents",
                          "is_tax"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "created": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "quote_number",
                    "invoice_id",
                    "number",
                    "status",
                    "mode",
                    "currency",
                    "subtotal_cents",
                    "tax_cents",
                    "total_cents",
                    "amount_due_cents",
                    "created_at",
                    "hosted_invoice_url",
                    "invoice_pdf_url",
                    "lines",
                    "created"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "201": {
            "description": "The invoice was written and finalised by this call. `created: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quote_number": {
                      "type": "integer"
                    },
                    "invoice_id": {
                      "type": "string"
                    },
                    "number": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "nullable": true
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "test",
                        "live",
                        "unknown"
                      ]
                    },
                    "currency": {
                      "type": "string"
                    },
                    "subtotal_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "tax_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "total_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "amount_due_cents": {
                      "type": "integer",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "hosted_invoice_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "invoice_pdf_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "lines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "description": {
                            "type": "string"
                          },
                          "amount_cents": {
                            "type": "integer"
                          },
                          "is_tax": {
                            "type": "boolean"
                          }
                        },
                        "required": [
                          "description",
                          "amount_cents",
                          "is_tax"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "created": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "quote_number",
                    "invoice_id",
                    "number",
                    "status",
                    "mode",
                    "currency",
                    "subtotal_cents",
                    "tax_cents",
                    "total_cents",
                    "amount_due_cents",
                    "created_at",
                    "hosted_invoice_url",
                    "invoice_pdf_url",
                    "lines",
                    "created"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The quote is not approved. An invoice belongs to an approved quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A TECHNICIAN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No quote with that id in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Stripe refused. The attempt is recorded against the quote with its attempt count incremented, and the call can be repeated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`STRIPE_SECRET_KEY` is not set on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/work-orders": {
      "get": {
        "tags": [
          "Work orders"
        ],
        "operationId": "listWorkOrders",
        "summary": "Scheduled work, soonest first.",
        "description": "A TECHNICIAN sees only their own jobs — the mobile app renders this list as the day's route. Note what the response does not carry: no cost, no margin, no quote total.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "DISPATCHED",
                "COMPLETED",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "scheduledOn",
            "in": "query",
            "required": false,
            "description": "Calendar date, YYYY-MM-DD.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching work orders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "PENDING",
                              "DISPATCHED",
                              "COMPLETED",
                              "CANCELLED"
                            ]
                          },
                          "scheduled_date": {
                            "type": "string",
                            "nullable": true,
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          "site_address": {
                            "type": "string",
                            "nullable": true
                          },
                          "notes": {
                            "type": "string",
                            "nullable": true
                          },
                          "assigned_technician_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "quote_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "dispatched_at": {
                            "type": "string",
                            "nullable": true,
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          "completed_at": {
                            "type": "string",
                            "nullable": true,
                            "description": "ISO-8601 timestamp, UTC."
                          }
                        },
                        "required": [
                          "id",
                          "status",
                          "scheduled_date",
                          "site_address",
                          "notes",
                          "assigned_technician_id",
                          "quote_id",
                          "dispatched_at",
                          "completed_at"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Work orders"
        ],
        "operationId": "createWorkOrder",
        "summary": "Dispatch a job. ADMIN only.",
        "description": "Creating work is a dispatch decision; quoting it is not. Both references are verified inside the tenant transaction before the insert, so a cross-tenant id returns a 404 naming which reference was wrong rather than an opaque conflict.",
        "requestBody": {
          "description": "Schedule, site, and assignment.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quoteId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "assignedTechnicianId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "scheduledDate": {
                    "type": "string",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                    "description": "Calendar date, YYYY-MM-DD."
                  },
                  "siteAddress": {
                    "type": "string",
                    "maxLength": 400
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 2000
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created work order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "PENDING",
                        "DISPATCHED",
                        "COMPLETED",
                        "CANCELLED"
                      ]
                    },
                    "scheduled_date": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "assigned_technician_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    }
                  },
                  "required": [
                    "id",
                    "status",
                    "scheduled_date",
                    "assigned_technician_id"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The assignee is not a TECHNICIAN, or their account is deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an ADMIN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The quote or technician id does not exist in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/work-orders/{id}/status": {
      "patch": {
        "tags": [
          "Work orders"
        ],
        "operationId": "updateWorkOrderStatus",
        "summary": "Move a work order through the dispatch state machine.",
        "description": "PENDING → DISPATCHED or CANCELLED. DISPATCHED → COMPLETED, PENDING or\nCANCELLED. COMPLETED and CANCELLED are terminal: reopening a completed job\nwould destroy the timestamp billing runs off, and a cancelled job that comes\nback is a new work order.\n\nIdempotent. Sending the status a record already has returns 200 with\n`unchanged: true` rather than a conflict, because a technician on\nintermittent signal will send the same completion twice.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "description": "The target status and optional notes.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "PENDING",
                      "DISPATCHED",
                      "COMPLETED",
                      "CANCELLED"
                    ]
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 2000
                  }
                },
                "required": [
                  "status"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The work order's new state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "PENDING",
                        "DISPATCHED",
                        "COMPLETED",
                        "CANCELLED"
                      ]
                    },
                    "unchanged": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "id",
                    "status",
                    "unchanged"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A technician who is not the one assigned to this job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No work order with that id in this tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The transition is not permitted from the current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations": {
      "get": {
        "tags": [
          "Integrations"
        ],
        "operationId": "listIntegrations",
        "summary": "Every provider this deployment knows about, and the caller's own connection.",
        "description": "Two different questions, answered separately on purpose. `configured` is about\nthe platform: whether this deployment holds the client credentials the provider\nneeds at all. `connection` is about the tenant: whether this organization has\nconsented. A UI that conflates them tells an administrator to reconnect when the\nthing that is actually missing is an environment variable only we can set, and\n`configuration.summary` exists so the wrong instruction is never the one shown.\n\nNo token, ciphertext or secret appears in this response. The only thing said\nabout credentials is when they expire.",
        "responses": {
          "200": {
            "description": "One entry per provider, connected or not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "provider": {
                            "type": "string",
                            "enum": [
                              "STRIPE",
                              "QUICKBOOKS",
                              "SALESFORCE"
                            ]
                          },
                          "slug": {
                            "type": "string",
                            "enum": [
                              "stripe",
                              "quickbooks",
                              "salesforce"
                            ]
                          },
                          "display_name": {
                            "type": "string"
                          },
                          "configured": {
                            "type": "boolean"
                          },
                          "configuration": {
                            "type": "object",
                            "properties": {
                              "missing_environment": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "awaiting_tenant_consent": {
                                "type": "boolean"
                              },
                              "summary": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "missing_environment",
                              "awaiting_tenant_consent",
                              "summary"
                            ],
                            "additionalProperties": false
                          },
                          "connection": {
                            "type": "object",
                            "properties": {
                              "status": {
                                "type": "string",
                                "enum": [
                                  "DISCONNECTED",
                                  "CONNECTED",
                                  "EXPIRED",
                                  "ERROR"
                                ]
                              },
                              "external_account_id": {
                                "type": "string",
                                "nullable": true
                              },
                              "instance_url": {
                                "type": "string",
                                "nullable": true
                              },
                              "scopes": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "connected_at": {
                                "type": "string",
                                "nullable": true,
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              "last_sync_at": {
                                "type": "string",
                                "nullable": true,
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              "last_error": {
                                "type": "string",
                                "nullable": true
                              },
                              "access_token_expires_at": {
                                "type": "string",
                                "nullable": true,
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              "refresh_token_expires_at": {
                                "type": "string",
                                "nullable": true,
                                "description": "ISO-8601 timestamp, UTC."
                              }
                            },
                            "required": [
                              "status",
                              "external_account_id",
                              "instance_url",
                              "scopes",
                              "connected_at",
                              "last_sync_at",
                              "last_error",
                              "access_token_expires_at",
                              "refresh_token_expires_at"
                            ],
                            "additionalProperties": false,
                            "nullable": true
                          }
                        },
                        "required": [
                          "provider",
                          "slug",
                          "display_name",
                          "configured",
                          "configuration",
                          "connection"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "description": "Every provider this deployment knows about, with the caller's own connection state."
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an ADMIN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/syncs": {
      "get": {
        "tags": [
          "Integrations"
        ],
        "operationId": "listIntegrationSyncs",
        "summary": "What each provider did with each approved quote.",
        "description": "The answer to \"did the invoice go out\". A SKIPPED row is a real answer — it means the provider was not connected when the quote was approved, so nothing was attempted — and it is recorded rather than left as silence, because silence is indistinguishable from a bug.",
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "stripe",
                "quickbooks",
                "salesforce"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The most recent sync attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "provider": {
                            "type": "string",
                            "enum": [
                              "STRIPE",
                              "QUICKBOOKS",
                              "SALESFORCE"
                            ]
                          },
                          "entity_type": {
                            "type": "string"
                          },
                          "local_id": {
                            "type": "string"
                          },
                          "external_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "PENDING",
                              "SUCCEEDED",
                              "FAILED",
                              "SKIPPED"
                            ]
                          },
                          "attempts": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "amount_cents": {
                            "type": "integer",
                            "nullable": true
                          },
                          "last_error": {
                            "type": "string",
                            "nullable": true
                          },
                          "synced_at": {
                            "type": "string",
                            "nullable": true,
                            "description": "ISO-8601 timestamp, UTC."
                          }
                        },
                        "required": [
                          "id",
                          "provider",
                          "entity_type",
                          "local_id",
                          "external_id",
                          "status",
                          "attempts",
                          "amount_cents",
                          "last_error",
                          "synced_at"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "description": "What each provider did with each approved quote. The answer to 'did the invoice go out'."
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an ADMIN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/{provider}/connect": {
      "post": {
        "tags": [
          "Integrations"
        ],
        "operationId": "connectIntegration",
        "summary": "Begin an OAuth consent flow.",
        "description": "Returns the provider's consent URL rather than redirecting to it. A 302 from an\nXHR is followed by the browser transparently and lands the provider's HTML\nconsent page inside a fetch the client cannot render, so the client has to move\nthe top-level window itself.\n\n`stripe` is rejected with a 400. Stripe here is the platform's own account, not\na per-tenant grant, so there is nothing for a tenant to consent to — and a\nconnect flow that silently succeeds while doing nothing is worse than a refusal\nthat says why.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stripe",
                "quickbooks",
                "salesforce"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Where to send the browser.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string",
                      "enum": [
                        "stripe",
                        "quickbooks",
                        "salesforce"
                      ]
                    },
                    "authorize_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in_seconds": {
                      "type": "integer",
                      "exclusiveMinimum": true,
                      "minimum": 0
                    }
                  },
                  "required": [
                    "provider",
                    "authorize_url",
                    "expires_in_seconds"
                  ],
                  "additionalProperties": false,
                  "description": "Where to send the browser to start an OAuth consent flow."
                }
              }
            }
          },
          "400": {
            "description": "`stripe`, which has no per-tenant flow; or the provider's client credentials are not configured on this deployment; or INTEGRATIONS_ENCRYPTION_KEY is unset, so no credential could be stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an ADMIN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/{provider}": {
      "delete": {
        "tags": [
          "Integrations"
        ],
        "operationId": "disconnectIntegration",
        "summary": "Forget this organization's credentials for a provider.",
        "description": "Idempotent, and destructive on purpose: the stored ciphertext is cleared rather than superseded. A history of retired refresh tokens is a history of live ones. Sync rows are kept — they are the tenant's audit trail of invoices that already exist in someone else's system, and deleting them would not unsend anything.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stripe",
                "quickbooks",
                "salesforce"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The provider is now disconnected.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string",
                      "enum": [
                        "stripe",
                        "quickbooks",
                        "salesforce"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "DISCONNECTED",
                        "CONNECTED",
                        "EXPIRED",
                        "ERROR"
                      ]
                    }
                  },
                  "required": [
                    "provider",
                    "status"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "`stripe`, which is configured platform-wide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No token, a malformed token, or a token this server did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not an ADMIN token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation. `error.details` names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Retry after the window in the `RateLimit-*` headers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error. Quote `error.request_id` in a support ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/{provider}/callback": {
      "get": {
        "tags": [
          "Integrations"
        ],
        "operationId": "oauthCallback",
        "summary": "The provider's redirect at the end of consent.",
        "description": "Public by necessity. This is a top-level browser navigation from the provider's\nown domain: it carries no cookie of ours and no bearer token, and nothing about\nthe request is under our control except the signed `state` we put into the\nauthorize URL ten minutes earlier. That signature is the authentication.\n\nNot for programmatic use, and the only endpoint in this API that answers with\nHTML — a human is looking at it. Every outcome including a refusal renders a\npage rather than an error envelope; pressing Deny is a completed flow with a no\nin it, not a validation failure.",
        "security": [],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stripe",
                "quickbooks",
                "salesforce"
              ]
            }
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2000
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2000
            }
          },
          {
            "name": "realmId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            }
          },
          {
            "name": "error",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "error_description",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consent completed, or was declined. The page says which; the status does not.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The `state` parameter was missing, expired, tampered with, or replayed.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "The provider rejected the code exchange.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/stripe/webhook": {
      "post": {
        "tags": [
          "Integrations"
        ],
        "operationId": "stripeWebhook",
        "summary": "Stripe's event delivery endpoint.",
        "description": "Public, and authenticated by an HMAC over the exact bytes Stripe sent — it has\nno credential of ours to present. The body is read raw for that reason: parsing\nand re-serialising JSON is not byte-identical to its input, so a handler working\nfrom a parsed object cannot verify anything.\n\nReturns 200 for events it acts on and events it does not, distinguished by\n`handled`. Stripe retries any non-2xx with backoff for three days, so a 500 on\nan event type we have no handler for buys three days of retrying something that\nwill never succeed. A failed signature is the one case where retrying is right,\nand the one case that answers 4xx.\n\n`invoice.paid` sets `paid_at` on the originating quote, guarded on that column\nbeing null so a replayed delivery cannot rewrite the date a tenant was paid.",
        "security": [],
        "parameters": [
          {
            "name": "stripe-signature",
            "in": "header",
            "required": true,
            "description": "Stripe's `t=` timestamp and `v1=` HMAC, as sent.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "A Stripe event object, delivered as raw bytes.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "received": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "handled": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "received",
                    "handled"
                  ],
                  "additionalProperties": false,
                  "description": "Acknowledgement. Always 200 once the signature verifies — an unrecognised event type is handled:false rather than an error, because Stripe retries anything non-2xx and there is nothing to retry."
                }
              }
            }
          },
          "400": {
            "description": "Signature verification failed, or the body is not JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "STRIPE_WEBHOOK_SECRET is not set on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The `access_token` from POST /api/v1/auth/login. Carries the organization and role; both are re-checked against the database on every request."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "bad_request",
                  "validation_failed",
                  "unauthenticated",
                  "invalid_credentials",
                  "forbidden",
                  "not_found",
                  "conflict",
                  "rate_limited",
                  "provider_error",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "properties": {
                  "fields": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "fields"
                ],
                "additionalProperties": false,
                "description": "Field path → the first thing wrong with it.",
                "nullable": true
              },
              "request_id": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "code",
              "message",
              "request_id"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "The shape every non-2xx response takes, without exception."
      },
      "ValidationIssues": {
        "type": "object",
        "properties": {
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "fields"
        ],
        "additionalProperties": false,
        "description": "Field path → the first thing wrong with it."
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "x-generated-by": "backend/scripts/generate-openapi.ts",
  "x-generated-from": "backend/src/schemas/*.schema.ts"
}
