{
  "openapi": "3.1.0",
  "info": {
    "title": "Trusted Tradies Agent API",
    "version": "0.4.0",
    "description": "Read source-separated marketplace information and prepare customer-controlled job requests. External agents may not choose recipients, approve, submit, unlock contact details, follow up, purchase credits or make payments autonomously."
  },
  "servers": [{ "url": "/", "description": "Same origin as this document" }],
  "paths": {
    "/api/v1/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List approved service categories",
        "description": "Read-only. Availability labels are not a promise of live supply.",
        "responses": {
          "200": {
            "description": "Approved category registry",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CategoryList" } } }
          }
        }
      }
    },
    "/api/v1/providers/search": {
      "get": {
        "operationId": "searchProviders",
        "summary": "Find providers for customer comparison",
        "description": "Read-only. The customer must choose every provider. Sources remain separate and no blended rating is returned.",
        "parameters": [
          { "name": "category", "in": "query", "schema": { "type": "string" } },
          { "name": "suburb", "in": "query", "schema": { "type": "string" } },
          { "name": "accepting", "in": "query", "schema": { "type": "boolean", "default": true } }
        ],
        "responses": {
          "200": {
            "description": "Provider cards for comparison",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProviderSearchResult" } } }
          }
        }
      }
    },
    "/api/v1/providers/{id}/ratings": {
      "get": {
        "operationId": "getProviderRatings",
        "summary": "Get source-separated provider ratings",
        "description": "Read-only. Each source retains its own score, scale, review count, freshness and attribution.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "description": "Opaque provider ID or canonical provider slug", "schema": { "type": "string", "minLength": 1, "maxLength": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Source-separated rating evidence",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RatingResult" } } }
          },
          "404": { "description": "Provider not found" }
        }
      }
    },
    "/api/v1/agent/request-proposals": {
      "post": {
        "operationId": "createExternalAgentRequestProposal",
        "summary": "Prepare a contact-free customer request proposal",
        "description": "Inactive default-off foundation. A future delegated agent may propose job text and businesses but cannot approve recipients, provide contact details or submit a request. Final confirmation remains on Trusted Tradies.",
        "security": [{ "externalAgentBearer": [] }],
        "x-trusted-tradies-required-scopes": ["tt.request.proposal.create"],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentRequestProposalInput" } } }
        },
        "responses": {
          "201": {
            "description": "Proposal stored and a TT-hosted confirmation handoff issued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentRequestProposalResult" } } }
          },
          "400": { "description": "Invalid or contact-bearing proposal" },
          "401": { "description": "Delegated token missing, expired or invalid" },
          "403": { "description": "Client or scope is not permitted" },
          "404": { "description": "External agent foundation is disabled" },
          "503": { "description": "Token verification or proposal persistence is not configured" }
        },
        "x-trusted-tradies-active": false
      }
    },
    "/api/v1/request-drafts": {
      "post": {
        "operationId": "saveRequestDraft",
        "summary": "Save a customer-authenticated request draft",
        "description": "Browser-session preparation only. Provider IDs must come from the customer's explicit selection. External bearer tokens are rejected. This operation does not approve, submit or deliver a request.",
        "security": [{ "customerBrowserSession": [] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RequestDraftInput" } } }
        },
        "responses": {
          "200": { "description": "Draft saved; exact customer review is still required" },
          "400": { "description": "Invalid or incomplete request" },
          "401": { "description": "Authenticated customer session required" },
          "403": { "description": "Customer is not permitted to save this draft" },
          "409": { "description": "Unavailable in demo mode" },
          "503": { "description": "Draft could not be stored safely" }
        }
      }
    },
    "/api/v1/request-drafts/{id}/approve": {
      "post": {
        "operationId": "retiredSeparateApproval",
        "deprecated": true,
        "summary": "Retired separate approval endpoint",
        "description": "Always returns 410. Approval and submission are atomic after the customer reviews the exact request, shared fields and recipient IDs.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": { "410": { "description": "Endpoint retired" } }
      }
    },
    "/api/v1/requests": {
      "post": {
        "operationId": "approveAndSubmitRequest",
        "summary": "Atomically approve and record an exact customer request",
        "description": "TT browser-session confirmation required. External bearer tokens are always rejected. The exact text, shared information and selected provider IDs require a fresh customer confirmation immediately before this call. Recording does not mean external delivery occurred.",
        "security": [{ "customerBrowserSession": [] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitRequestInput" } } }
        },
        "responses": {
          "200": {
            "description": "Request approval recorded idempotently; deliverySent remains false",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitRequestResult" } } }
          },
          "400": { "description": "Approval, draft state or idempotency input invalid" },
          "401": { "description": "Authenticated customer session required" },
          "403": { "description": "Customer is not permitted to submit this draft" },
          "409": { "description": "Unavailable in demo mode" },
          "503": { "description": "Safe completion could not be verified" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "customerBrowserSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "supabase-customer-session",
        "description": "Logical browser-session requirement. Concrete Supabase cookie names are environment-specific. No public external-agent token flow is active."
      },
      "externalAgentBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "OAuth 2.1 access token",
        "description": "Inactive foundation. No external token verifier, OAuth client or Production bearer-token acceptance is configured."
      }
    },
    "schemas": {
      "CategoryList": {
        "type": "object",
        "required": ["categories", "launchMarket", "fictional", "liveSupply"],
        "additionalProperties": false,
        "properties": {
          "categories": { "type": "array", "minItems": 18, "maxItems": 18, "items": { "$ref": "#/components/schemas/Category" } },
          "launchMarket": { "const": "Toowoomba Region" },
          "fictional": { "const": true },
          "liveSupply": { "const": false }
        }
      },
      "Category": {
        "type": "object",
        "required": ["slug", "name", "description", "icon", "availability", "availabilityLabel"],
        "additionalProperties": false,
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "icon": { "type": "string" },
          "availability": { "type": "string", "enum": ["available", "limited", "unclaimed", "unavailable"] },
          "availabilityLabel": { "type": "string" }
        }
      },
      "ProviderSearchResult": {
        "type": "object",
        "required": ["location", "category", "providers", "customerSelectionRequired"],
        "additionalProperties": false,
        "properties": {
          "location": { "type": "string" },
          "category": { "type": "string" },
          "providers": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProvider" } },
          "customerSelectionRequired": { "const": true }
        }
      },
      "PublicProvider": {
        "type": "object",
        "required": ["id", "slug", "businessName", "description", "categories", "suburbs", "ratings", "claimed", "verified", "acceptsRequests", "selectable"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string" },
          "slug": { "type": "string" },
          "businessName": { "type": "string" },
          "description": { "type": "string" },
          "categories": { "type": "array", "items": { "type": "string" } },
          "suburbs": { "type": "array", "items": { "type": "string" } },
          "ratings": { "type": "array", "items": { "$ref": "#/components/schemas/PublicRating" } },
          "claimed": { "type": "boolean" },
          "verified": { "type": "boolean" },
          "acceptsRequests": { "type": "boolean" },
          "selectable": { "type": "boolean" }
        }
      },
      "PublicRating": {
        "type": "object",
        "required": ["source"],
        "additionalProperties": false,
        "properties": {
          "source": { "type": "string" },
          "sourceCode": { "type": "string" },
          "rating": { "type": "number" },
          "recommendationPercent": { "type": "number" },
          "reviewCount": { "type": "integer", "minimum": 0 },
          "profileUrl": { "type": "string", "format": "uri" },
          "lastRefreshed": { "type": "string", "format": "date-time" },
          "refreshDueAt": { "type": "string", "format": "date-time" },
          "attribution": { "type": "string" },
          "confidenceLabel": { "type": "string" },
          "stateMessage": { "type": "string" },
          "status": { "type": "string", "enum": ["available", "pending", "stale", "error", "unavailable"] }
        }
      },
      "RatingResult": {
        "type": "object",
        "required": ["providerId", "businessName", "ratings", "blendedRating", "sourceSeparationRequired"],
        "additionalProperties": false,
        "properties": {
          "providerId": { "type": "string" },
          "businessName": { "type": "string" },
          "ratings": { "type": "array", "items": { "$ref": "#/components/schemas/PublicRating" } },
          "blendedRating": { "type": "null" },
          "sourceSeparationRequired": { "const": true }
        }
      },
      "AgentRequestProposalInput": {
        "type": "object",
        "required": ["categorySlug", "suburb", "postcode", "title", "description", "urgency"],
        "additionalProperties": false,
        "properties": {
          "categorySlug": { "type": "string", "minLength": 2, "maxLength": 120 },
          "suburb": { "type": "string", "minLength": 2, "maxLength": 120 },
          "postcode": { "type": "string", "pattern": "^$|^[0-9]{4}$" },
          "title": { "type": "string", "minLength": 5, "maxLength": 200 },
          "description": { "type": "string", "minLength": 15, "maxLength": 8000 },
          "urgency": { "type": "string", "enum": ["flexible", "soon", "urgent"] },
          "proposedProviderIds": { "type": "array", "maxItems": 12, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 200 } }
        }
      },
      "AgentRequestProposalResult": {
        "type": "object",
        "required": ["proposalId", "handoffPath", "expiresAt", "state", "customerConfirmationRequired", "submissionAvailableToAgent"],
        "additionalProperties": false,
        "properties": {
          "proposalId": { "type": "string", "format": "uuid" },
          "handoffPath": { "type": "string", "pattern": "^/agent-handoff/" },
          "expiresAt": { "type": "string", "format": "date-time" },
          "state": { "const": "proposed" },
          "customerConfirmationRequired": { "const": true },
          "submissionAvailableToAgent": { "const": false }
        }
      },
      "RequestDraftInput": {
        "type": "object",
        "required": ["subcategoryId", "suburb", "postcode", "title", "description", "urgency", "contactMethod", "selectedProviderIds"],
        "properties": {
          "id": { "type": ["string", "null"], "format": "uuid" },
          "subcategoryId": { "type": "string", "format": "uuid" },
          "suburb": { "type": "string", "minLength": 2, "maxLength": 120 },
          "postcode": { "type": "string", "pattern": "^$|^[0-9]{4}$" },
          "title": { "type": "string", "minLength": 5, "maxLength": 200 },
          "description": { "type": "string", "minLength": 15, "maxLength": 8000 },
          "urgency": { "type": "string", "enum": ["flexible", "soon", "urgent"] },
          "contactMethod": { "type": "string", "enum": ["email", "phone", "sms"] },
          "contactValue": { "type": "string", "maxLength": 40 },
          "selectedProviderIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 12,
            "uniqueItems": true,
            "items": { "type": "string", "format": "uuid" },
            "description": "Exact providers selected by the customer; agents may not add recipients independently."
          }
        },
        "additionalProperties": false
      },
      "SubmitRequestInput": {
        "type": "object",
        "required": ["draftId", "idempotencyKey"],
        "properties": {
          "draftId": { "type": "string", "format": "uuid" },
          "idempotencyKey": { "type": "string", "minLength": 8, "maxLength": 200 }
        },
        "additionalProperties": false
      },
      "SubmitRequestResult": {
        "type": "object",
        "required": ["requestId", "approvalVersionId", "status", "deliverySent", "replayed"],
        "properties": {
          "requestId": { "type": "string", "format": "uuid" },
          "approvalVersionId": { "type": "string", "format": "uuid" },
          "status": { "const": "recorded" },
          "deliverySent": { "const": false },
          "replayed": { "type": "boolean" }
        },
        "additionalProperties": false
      }
    }
  },
  "x-trusted-tradies-rules": {
    "externalCustomerOnly": true,
    "internalBusinessAccess": false,
    "customerSelectsEveryRecipient": true,
    "separateRatingSources": true,
    "freshCustomerConfirmationBeforeSubmission": true,
    "agentMayPrepareDraft": true,
    "agentMayNotAutoSelectOrSubmit": true,
    "agentMayNotUnlockContactOrPurchaseCredits": true,
    "externalAgentAuthenticationActive": false,
    "externalAgentProposalActive": false,
    "externalAgentCandidateScopes": ["tt.marketplace.read", "tt.request.proposal.create", "tt.request.proposal.read"],
    "externalAgentMaySubmit": false,
    "liveDeliveryActive": false,
    "paymentsActive": false
  }
}
