{
  "openapi": "3.1.0",
  "info": {
    "title": "BirdSift API",
    "version": "1.0.0",
    "description": "BirdSift API v1 — list saved leads, run bounded local-business searches, and draft outreach. Human-readable reference: https://www.birdsift.com/docs/api. Agent instructions: https://www.birdsift.com/agent-instructions. Rate limit: 60 requests/minute per API key. Searches and outreach drafts consume workspace credits.",
    "contact": {
      "name": "BirdSift",
      "url": "https://www.birdsift.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.birdsift.com"
    }
  ],
  "externalDocs": {
    "description": "BirdSift API reference",
    "url": "https://www.birdsift.com/docs/api"
  },
  "tags": [
    {
      "name": "docs",
      "description": "Public reference material"
    },
    {
      "name": "auth",
      "description": "OAuth 2.0 discovery metadata"
    },
    {
      "name": "leads",
      "description": "Saved workspace leads"
    },
    {
      "name": "searches",
      "description": "Bounded lead discovery"
    },
    {
      "name": "credits",
      "description": "Workspace credit balance"
    },
    {
      "name": "mcp",
      "description": "Model Context Protocol server"
    }
  ],
  "paths": {
    "/openapi.json": {
      "get": {
        "tags": [
          "docs"
        ],
        "summary": "OpenAPI specification",
        "description": "Returns this OpenAPI 3.1 document for codegen and agent function calling.",
        "operationId": "getOpenApiSpec",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 JSON document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/docs": {
      "get": {
        "tags": [
          "docs"
        ],
        "summary": "API reference (markdown)",
        "description": "Public markdown API reference. No authentication required — use to discover endpoints before calling /v1.",
        "operationId": "getV1Docs",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown API reference",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "OAuth 2.0 authorization server metadata",
        "description": "RFC 8414 authorization server metadata for the OAuth 2.0 flow behind /mcp. No authentication required — fetch this first to discover the authorization, token and dynamic client registration endpoints.",
        "operationId": "getOAuthAuthorizationServerMetadata",
        "security": [],
        "responses": {
          "200": {
            "description": "RFC 8414 authorization server metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "OAuth 2.0 protected resource metadata",
        "description": "RFC 9728 protected resource metadata naming the authorization server that guards /mcp. No authentication required.",
        "operationId": "getOAuthProtectedResourceMetadata",
        "security": [],
        "responses": {
          "200": {
            "description": "RFC 9728 protected resource metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "MCP Streamable HTTP endpoint",
        "description": "Model Context Protocol endpoint (Streamable HTTP transport) exposing the BirdSift tools for local-business search, saved leads and outreach drafts. Unlike /v1 — which takes a workspace API key — this endpoint authenticates with an OAuth 2.0 access token; see /.well-known/oauth-protected-resource. Request and response bodies are JSON-RPC 2.0 messages defined by the MCP specification, not by this document.",
        "operationId": "postMcpMessage",
        "security": [
          {
            "OAuth2": [
              "email",
              "profile"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "description": "JSON-RPC 2.0 request as defined by the MCP specification",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC 2.0 response, or an SSE stream when the client accepts one",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid access token. The WWW-Authenticate header carries a `resource_metadata` pointer to /.well-known/oauth-protected-resource/mcp, which is where an MCP client starts OAuth discovery. The body is an RFC 6749 error object, not the /v1 ErrorResponse shape — see mcp/auth.ts.",
            "headers": {
              "WWW-Authenticate": {
                "description": "RFC 9728 challenge naming the protected resource metadata URL",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/leads": {
      "get": {
        "tags": [
          "leads"
        ],
        "summary": "List saved leads",
        "description": "Returns a cursor-paginated list of leads saved in the authenticated workspace.",
        "operationId": "listLeads",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a previous response's next_cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Lead page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests/minute per key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/leads/{id}": {
      "get": {
        "tags": [
          "leads"
        ],
        "summary": "Get one saved lead",
        "description": "Fetch a single lead by UUID, including website audit flags used for outreach.",
        "operationId": "getLead",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Lead UUID",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lead detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadDetail"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Lead not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "tags": [
          "credits"
        ],
        "summary": "Get credit balance",
        "description": "Returns the remaining credit balance for the authenticated workspace.",
        "operationId": "getCredits",
        "responses": {
          "200": {
            "description": "Current balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/searches": {
      "post": {
        "tags": [
          "searches"
        ],
        "summary": "Run a bounded search and save results",
        "description": "Queries Google Places for local businesses matching trade + location filters, verifies websites, saves matches as workspace leads, and debits credits.",
        "operationId": "createSearch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results saved as leads",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — malformed body, queries or max_results out of range, or more than 2 queries from a workspace that has never bought credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests/minute per key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/leads/{id}/outreach-draft": {
      "post": {
        "tags": [
          "leads"
        ],
        "summary": "Draft outreach email for a saved lead",
        "description": "Generates a fact-based cold email draft for a saved lead using verified Google Places and website audit data.",
        "operationId": "draftOutreach",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Lead UUID",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutreachDraftRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutreachDraftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Insufficient audit data to draft outreach",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "OAuthErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "RFC 6749 §5.2 error object, as returned by the OAuth-guarded /mcp endpoint.",
        "properties": {
          "error": {
            "type": "string",
            "description": "OAuth error code, e.g. `invalid_token`"
          },
          "error_description": {
            "type": "string",
            "description": "Human-readable error message"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code"
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message"
              }
            }
          }
        }
      },
      "LeadSummary": {
        "type": "object",
        "required": [
          "id",
          "place_id",
          "name",
          "category",
          "address",
          "website",
          "email",
          "outreach_status",
          "added_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "BirdSift lead UUID"
          },
          "place_id": {
            "type": "string",
            "description": "Google Places ID"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "Business category or trade"
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "website": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "outreach_status": {
            "type": "string",
            "enum": [
              "new",
              "contacted",
              "replied",
              "won",
              "lost"
            ],
            "description": "Where the lead sits in the outreach pipeline"
          },
          "added_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LeadDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeadSummary"
          },
          {
            "type": "object",
            "required": [
              "no_website_flag",
              "not_mobile_friendly_flag",
              "outdated_website_flag"
            ],
            "properties": {
              "no_website_flag": {
                "type": "boolean",
                "nullable": true
              },
              "not_mobile_friendly_flag": {
                "type": "boolean",
                "nullable": true
              },
              "outdated_website_flag": {
                "type": "boolean",
                "nullable": true
              },
              "last_media_year": {
                "type": "integer",
                "nullable": true,
                "description": "Year the newest photo on the homepage was uploaded. Null when no dated media path was readable — never assume recent."
              }
            }
          }
        ]
      },
      "LeadListResponse": {
        "type": "object",
        "required": [
          "leads",
          "next_cursor"
        ],
        "properties": {
          "leads": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeadSummary"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Pass as ?cursor= on the next page"
          }
        }
      },
      "CreditsResponse": {
        "type": "object",
        "required": [
          "balance"
        ],
        "properties": {
          "balance": {
            "type": "integer",
            "description": "Remaining workspace credits"
          }
        }
      },
      "SearchQuery": {
        "type": "object",
        "required": [
          "location",
          "category"
        ],
        "properties": {
          "location": {
            "type": "string",
            "description": "City or region, e.g. Austin, TX"
          },
          "category": {
            "type": "string",
            "description": "Trade or business type, e.g. dentist"
          }
        }
      },
      "SearchFilters": {
        "type": "object",
        "properties": {
          "min_rating": {
            "type": "number",
            "description": "Minimum Google rating (0–5)"
          },
          "no_website": {
            "type": "boolean",
            "description": "Only businesses with no website"
          },
          "not_mobile_friendly": {
            "type": "boolean",
            "description": "Only non-mobile-friendly sites"
          },
          "outdated_website": {
            "type": "boolean",
            "description": "Only outdated-looking sites"
          },
          "stale_content": {
            "type": "boolean",
            "description": "Only sites whose newest homepage photo is 3+ years old. Narrow — the date is only readable on sites that file media by date."
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "queries",
          "max_results"
        ],
        "properties": {
          "queries": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "description": "Up to 5 queries. A workspace that has never bought credits can send at most 2 (400 invalid_request otherwise).",
            "items": {
              "$ref": "#/components/schemas/SearchQuery"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/SearchFilters"
          },
          "max_results": {
            "type": "integer",
            "minimum": 1,
            "maximum": 25,
            "description": "Maximum leads to return across all queries"
          }
        }
      },
      "SearchResultLead": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LeadSummary"
          },
          {
            "type": "object",
            "properties": {
              "has_website": {
                "type": "boolean",
                "nullable": true
              },
              "is_mobile_friendly": {
                "type": "boolean",
                "nullable": true
              },
              "is_outdated": {
                "type": "boolean",
                "nullable": true
              }
            }
          }
        ]
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "results",
          "credits_spent",
          "truncated"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchResultLead"
            }
          },
          "credits_spent": {
            "type": "integer",
            "description": "Credits consumed by this search"
          },
          "truncated": {
            "type": "boolean",
            "description": "True when max_results or the per-query cap (60) stopped the scan early, or analysis budget ran out"
          }
        }
      },
      "OutreachDraftRequest": {
        "type": "object",
        "properties": {
          "locale": {
            "type": "string",
            "enum": [
              "en",
              "fr"
            ],
            "description": "Draft language"
          }
        }
      },
      "OutreachDraftResponse": {
        "type": "object",
        "required": [
          "subject",
          "body",
          "facts_used"
        ],
        "properties": {
          "subject": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "facts_used": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Verified facts referenced in the draft"
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Workspace API key (`bsk_live_…`). Create from the app."
      },
      "OAuth2": {
        "type": "oauth2",
        "description": "Supabase OAuth 2.0 access token for workspace-scoped MCP access. Metadata: GET /.well-known/oauth-authorization-server",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://tzngbfxhkxvbtoxsypii.supabase.co/auth/v1/oauth/authorize",
            "tokenUrl": "https://tzngbfxhkxvbtoxsypii.supabase.co/auth/v1/oauth/token",
            "scopes": {
              "email": "Email address",
              "profile": "Basic profile"
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ]
}
