{
  "openapi": "3.1.0",
  "info": {
    "title": "TenderLead Public API",
    "version": "2026-10-23",
    "description": "TenderLead's authenticated daily-brief read API and legacy public form, sign-in and signed-link endpoints. GET /v1/briefs/daily requires an account-scoped bearer token with briefs:read and an active plan or trial. Form submissions, feedback and unsubscribe links can change state; they are not part of the read-only integration. This is not the separate OAuth-protected MCP specification or a complete CLI API specification. See https://tenderlead.co.uk/api-docs for daily-brief setup and https://tenderlead.co.uk/.well-known/auth.md for capability boundaries."
  },
  "externalDocs": {
    "description": "Daily-brief API setup, response fields and limits",
    "url": "https://tenderlead.co.uk/api-docs"
  },
  "servers": [
    {
      "url": "https://tenderlead.co.uk"
    }
  ],
  "security": [],
  "components": {
    "securitySchemes": {
      "apiToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Use a read-only token from Settings > API access in the Authorization: Bearer header. The daily brief requires briefs:read. The current narrowest token option grants briefs:read and tenders:read. Tokens are account-scoped, revocable and expire after 90 days. Never put a token in a URL or public example."
      }
    },
    "schemas": {
      "DailyBrief": {
        "type": "object",
        "required": ["generated_at", "tier", "opportunities"],
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Response time, not evidence that source notices were refreshed."
          },
          "tier": {
            "type": "string",
            "enum": ["starter", "pro", "team"],
            "description": "Effective account entitlement. Pro/Team return account-matched recommendations; legacy Starter returns public-feed fields."
          },
          "opportunities": {
            "type": "array",
            "description": "May be empty. For ordinary matched accounts, includes scores from the previous 24 hours. Missing source or assessment fields remain absent or null; inspect the original notice before acting.",
            "items": {
              "type": "object",
              "properties": {
                "contract_id": { "type": ["string", "null"], "description": "Stable identifier for destination deduplication when supplied." },
                "title": { "type": ["string", "null"] },
                "buyer": { "type": ["string", "null"] },
                "source": { "type": ["string", "null"] },
                "source_url": { "type": ["string", "null"] },
                "estimated_value": { "type": ["number", "string", "null"], "description": "Source value, including decimal strings where supplied. Missing is not zero." },
                "currency": { "type": ["string", "null"] },
                "deadline": { "type": ["string", "null"], "description": "Source deadline where supplied. Missing does not mean open-ended." },
                "score": { "type": ["number", "null"], "description": "Account qualification signal when available, not a winning probability." },
                "recommendation": { "type": ["string", "null"] },
                "tender_summary": { "type": ["string", "object", "null"], "description": "May be plain text or labelled sections." }
              },
              "additionalProperties": true
            }
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "requiresUpgrade": { "type": "boolean" },
          "requiredTier": { "type": "string" }
        },
        "additionalProperties": true
      }
    }
  },
  "paths": {
    "/v1/briefs/daily": {
      "get": {
        "summary": "Read the authorised account's daily tender brief",
        "operationId": "getDailyTenderBrief",
        "description": "Requires briefs:read and an active plan or trial. No request body, query parameters or pagination. Reads stored results without generating scores or spending AI credits; it is not an event trigger. Limited to 120 reads/hour/token and 240 reads/hour/IP. Honour Retry-After on 429; resolve 401/402/403 before retrying. Source-specific reuse conditions still apply: https://tenderlead.co.uk/sources.",
        "security": [{ "apiToken": [] }],
        "responses": {
          "200": {
            "description": "Current account brief; an empty opportunities array is a valid successful response.",
            "headers": {
              "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DailyBrief" },
                "examples": {
                  "fictionalEmptyBrief": {
                    "summary": "Fictional response shape, not customer or live-feed evidence",
                    "value": { "generated_at": "2026-10-23T09:00:00Z", "tier": "pro", "opportunities": [] }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid, expired or revoked token. Replace or reconnect it.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "example": { "error": "Unauthorised" } } }
          },
          "402": {
            "description": "Inactive account entitlement. Restore authorised account access; do not bypass the check.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "example": { "error": "Payment required", "requiresUpgrade": true, "requiredTier": "starter" } } }
          },
          "403": {
            "description": "Token lacks briefs:read. Use the required read scope, not write access.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "example": { "error": "Token requires briefs:read scope" } } }
          },
          "429": {
            "description": "Token or IP limit reached. Honour Retry-After before retrying.",
            "headers": {
              "Retry-After": { "description": "Delay in seconds; currently 3600.", "schema": { "type": "string" }, "example": "3600" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" }, "example": { "error": "Too many requests" } } }
          }
        }
      }
    },
    "/api/waitlist": {
      "post": {
        "summary": "Join the TenderLead waitlist",
        "operationId": "joinWaitlist",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "company_name": {
                    "type": "string"
                  },
                  "sector": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Waitlist request accepted"
          },
          "400": {
            "description": "Invalid request"
          }
        }
      }
    },
    "/api/access-request": {
      "post": {
        "summary": "Request paid or pilot access",
        "operationId": "requestAccess",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "email", "company_name", "sector"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "company_name": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "sector": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 2000
                  },
                  "website": {
                    "type": "string",
                    "format": "uri"
                  },
                  "plan": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access request accepted"
          },
          "400": {
            "description": "Invalid request"
          }
        }
      }
    },
    "/api/auth/google/waitlist": {
      "get": {
        "summary": "Start Google waitlist signup",
        "operationId": "startGoogleWaitlist",
        "responses": {
          "302": {
            "description": "Redirects to Google OAuth"
          }
        }
      }
    },
    "/api/auth/google": {
      "get": {
        "summary": "Start Google sign-in",
        "operationId": "startGoogleSignIn",
        "responses": {
          "302": {
            "description": "Redirects to Google OAuth"
          }
        }
      }
    },
    "/api/feedback": {
      "get": {
        "summary": "Record feedback from an email link",
        "operationId": "recordEmailFeedback",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "response",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": ["positive", "negative"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback recorded or already recorded"
          },
          "400": {
            "description": "Invalid link"
          }
        }
      }
    },
    "/api/unsubscribe": {
      "get": {
        "summary": "Unsubscribe from TenderLead email using a signed link",
        "operationId": "unsubscribe",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unsubscribe page or confirmation"
          },
          "400": {
            "description": "Invalid link"
          }
        }
      }
    }
  }
}
