{
  "openapi": "3.1.0",
  "info": {
    "title": "AI Search API",
    "version": "2026-06-27",
    "description": "One API to query every major consumer AI search surface (ChatGPT, Claude, Perplexity, Gemini, Copilot, Google AI Overview/Mode) and get the answer users actually see — plus the structured evidence behind it — as JSON.\n\nThis specification documents the public HTTP contract implemented by the production Worker at `api.aisearchapi.dev`. It is intentionally minimal and honest: it covers only endpoints that are actually served.",
    "contact": {
      "name": "AI Search API",
      "url": "https://aisearchapi.dev"
    }
  },
  "servers": [
    { "url": "https://api.aisearchapi.dev", "description": "Production" }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Search", "description": "Submit queries and read results." },
    { "name": "Jobs", "description": "Poll job state and fetch envelopes." },
    { "name": "Artifacts", "description": "Fetch durable capture artifacts." },
    { "name": "Account", "description": "Usage ledger and credit balance." },
    { "name": "Discovery", "description": "Public capability and targeting metadata." },
    { "name": "System", "description": "Liveness." }
  ],
  "paths": {
    "/v1/search": {
      "post": {
        "tags": ["Search"],
        "summary": "Create a search",
        "operationId": "createSearch",
        "description": "Submit a query to one or more surfaces. By default the request is asynchronous and returns `202` with a parent job plus one child per surface × region. A single-surface request with no webhook runs synchronously and returns the terminal envelope(s) in one `200`. Use `?mode=sync` / `Prefer: wait=<seconds>` to force sync, or `?mode=async` to force the durable path.",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "Force `sync` (bounded inline) or `async` (durable 202 + poll). Omitted: single-surface no-webhook requests run sync, everything else async.",
            "schema": { "type": "string", "enum": ["sync", "async"] }
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "`flat` projects the rich Envelope to a five-field flat shape (`surface`, `status`, `text`, `markdown`, `sources`).",
            "schema": { "type": "string", "enum": ["flat"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SearchRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Synchronous result — the terminal envelope(s).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    { "type": "array", "items": { "$ref": "#/components/schemas/Envelope" } },
                    { "$ref": "#/components/schemas/FlatEnvelope" }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Accepted — durable fan-out started.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAccepted" }
              }
            }
          },
          "400": {
            "description": "Validation failed (`VALIDATION_FAILED`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": {
            "description": "Missing or invalid key (`AUTH_INVALID`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "409": {
            "description": "Idempotency key reused with a different body (`IDEMPOTENCY_CONFLICT`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "422": {
            "description": "A surface has no path for the requested method (`UNSUPPORTED_METHOD_FOR_SURFACE`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Admission control — `RATE_LIMIT_EXCEEDED`, `CONCURRENCY_LIMIT_EXCEEDED`, or `QUEUE_CAPACITY_EXCEEDED`. Carries `Retry-After` and `X-RateLimit-*` headers.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/search/batch": {
      "post": {
        "tags": ["Search"],
        "summary": "Create a batch of searches",
        "operationId": "createBatch",
        "description": "Submit up to 500 independent search jobs in one call. Each item is validated and billed per-item; one bad item never sinks the batch — it returns a per-item rejection instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BatchRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item accept/reject results.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "400": {
            "description": "`items` must be a non-empty array of at most 500 requests (`VALIDATION_FAILED`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": {
            "description": "Missing or invalid key (`AUTH_INVALID`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Rate limit exceeded (`RATE_LIMIT_EXCEEDED`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": ["Jobs"],
        "summary": "Get a job",
        "operationId": "getJob",
        "description": "Read a job by id. A **parent** id (no dots, e.g. `job_8t2q`) returns a rollup of its children. A **child** id (dotted, e.g. `job_8t2q.chatgpt.us`) returns the canonical Envelope for one surface × region.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Parent job id (no dots) or child job id (dotted).",
            "schema": { "type": "string" },
            "example": "job_8t2q.chatgpt.us"
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "`flat` returns the five-field flat projection for a child.",
            "schema": { "type": "string", "enum": ["flat"] }
          }
        ],
        "responses": {
          "200": {
            "description": "A parent rollup or a child Envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/JobRollup" },
                    { "$ref": "#/components/schemas/Envelope" },
                    { "$ref": "#/components/schemas/FlatEnvelope" }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Unknown id (`JOB_NOT_FOUND`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/artifacts/{key}": {
      "get": {
        "tags": ["Artifacts"],
        "summary": "Fetch an artifact",
        "operationId": "getArtifact",
        "description": "Stream a durable capture artifact (raw capture, proof-of-page HTML, or screenshot) referenced by an envelope's `artifacts.*Url`. Private — requires the API key.",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "The artifact key from `job.artifacts` (URL-encoded).",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The artifact bytes, with the stored content type.",
            "content": {
              "application/octet-stream": { "schema": { "type": "string", "format": "binary" } },
              "text/html": { "schema": { "type": "string" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "401": {
            "description": "Missing or invalid key (`AUTH_INVALID`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "Unknown artifact key (`ARTIFACT_NOT_FOUND`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": ["Account"],
        "summary": "Read usage",
        "operationId": "getUsage",
        "description": "Return the account plan, a usage tally rolled up by the never-blended `(surface, method, region)` key, and the credit balance. Read-only.",
        "responses": {
          "200": {
            "description": "Usage snapshot.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UsageResponse" }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key (`AUTH_INVALID`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/surfaces": {
      "get": {
        "tags": ["Discovery"],
        "summary": "List surfaces",
        "operationId": "listSurfaces",
        "description": "Public discovery: every surface, its per-method capability state, and the `auto` default. No auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "Surface capability matrix.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SurfacesResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/regions": {
      "get": {
        "tags": ["Discovery"],
        "summary": "Describe region targeting",
        "operationId": "listRegions",
        "description": "Public discovery: how geo targeting works (country + optional state/city/language), with examples and popular countries. No auth.",
        "security": [],
        "responses": {
          "200": {
            "description": "Region targeting metadata.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RegionsResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "tags": ["System"],
        "summary": "Liveness probe",
        "operationId": "getHealth",
        "description": "Unauthenticated liveness probe. Reports the current schema version and whether the Worker's bindings are wired.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send your API key as `Authorization: Bearer <API_KEY>`."
      }
    },
    "schemas": {
      "Surface": {
        "type": "string",
        "description": "An AI search surface to capture.",
        "enum": [
          "chatgpt",
          "claude",
          "perplexity",
          "gemini",
          "copilot",
          "google_ai_overview",
          "google_ai_mode"
        ]
      },
      "Method": {
        "type": "string",
        "description": "How a surface is acquired. `auto` lets the API route to the best available path.",
        "enum": ["auto", "official-api", "managed-vendor", "own-fleet"]
      },
      "Region": {
        "type": "object",
        "description": "Where to run a surface. One child job is created per surface × region.",
        "required": ["country"],
        "properties": {
          "country": { "type": "string", "description": "ISO 3166-1 alpha-2 country code.", "example": "US" },
          "state": { "type": "string", "description": "Optional sub-national code/name.", "example": "NY" },
          "city": { "type": "string", "description": "Optional city name.", "example": "Buffalo" },
          "language": { "type": "string", "description": "Optional BCP-47 language hint.", "example": "en" }
        }
      },
      "Webhook": {
        "type": "object",
        "description": "Receive a signed POST on each child's terminal state instead of polling.",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "HTTPS endpoint to notify. SSRF-guarded." },
          "secret": { "type": "string", "description": "Shared secret used to HMAC-sign the body. Omit to sign with your account's active managed secret." }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": ["query", "surfaces"],
        "properties": {
          "query": { "type": "string", "minLength": 1, "maxLength": 10000, "description": "The prompt to run against each surface." },
          "surfaces": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/Surface" },
            "description": "One or more surfaces. Each produces one child job per region."
          },
          "regions": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Region" },
            "description": "Where to run each surface. Defaults to a single US region.",
            "default": [{ "country": "US" }]
          },
          "method": { "$ref": "#/components/schemas/Method" },
          "priority": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5, "description": "Queue priority within your account." },
          "webhook": { "$ref": "#/components/schemas/Webhook" },
          "idempotencyKey": { "type": "string", "description": "Re-submit with the same key + same body to replay the original job; a different body under the same key is a 409." },
          "shadow": { "type": "boolean", "description": "Run a best-effort shadow (benchmark) lane alongside the primary. Excluded from the parent status rollup." }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": { "$ref": "#/components/schemas/SearchRequest" },
            "description": "1–500 independent search requests."
          }
        }
      },
      "BatchResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": { "type": "integer" },
                "status": { "type": "string", "enum": ["accepted", "rejected"] },
                "jobId": { "type": "string", "description": "Present when accepted." },
                "children": { "type": "array", "items": { "type": "string" }, "description": "Child ids, present when accepted." },
                "code": { "type": "string", "description": "Present when rejected." },
                "error": { "type": "string", "description": "Present when rejected." }
              },
              "required": ["index", "status"]
            }
          }
        }
      },
      "JobAccepted": {
        "type": "object",
        "properties": {
          "jobId": { "type": "string", "example": "job_8t2q" },
          "status": { "type": "string", "example": "processing" },
          "children": {
            "type": "array",
            "items": { "type": "string" },
            "example": ["job_8t2q.chatgpt.us"]
          }
        },
        "required": ["jobId", "status", "children"]
      },
      "JobRollup": {
        "type": "object",
        "description": "Parent job status plus its children.",
        "properties": {
          "job": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "status": { "type": "string", "enum": ["processing", "running", "succeeded", "partial", "failed"] }
            }
          },
          "children": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "status": { "type": "string" }
              }
            }
          }
        }
      },
      "Envelope": {
        "type": "object",
        "description": "The canonical §6 result for one surface × region: the job, the answer, its provenance, and its evidence.",
        "properties": {
          "job": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "surface": { "$ref": "#/components/schemas/Surface" },
              "status": { "type": "string", "enum": ["succeeded", "partial", "failed", "running"] },
              "warnings": { "type": "array", "items": { "type": "string" } },
              "artifacts": {
                "type": "object",
                "description": "Durable capture artifacts. `*Url` fields are directly fetchable via GET /v1/artifacts/:key.",
                "properties": {
                  "rawUrl": { "type": "string", "format": "uri", "nullable": true },
                  "proofHtmlUrl": { "type": "string", "format": "uri", "nullable": true },
                  "screenshotUrl": { "type": "string", "format": "uri", "nullable": true }
                }
              }
            }
          },
          "answer": {
            "type": "object",
            "properties": {
              "text": { "type": "string" },
              "markdown": { "type": "string" }
            }
          },
          "provenance": {
            "type": "object",
            "properties": {
              "surface": { "$ref": "#/components/schemas/Surface" },
              "method": { "$ref": "#/components/schemas/Method" },
              "region": { "$ref": "#/components/schemas/Region" }
            }
          },
          "evidence": {
            "type": "object",
            "properties": {
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "url": { "type": "string", "format": "uri" },
                    "title": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      },
      "FlatEnvelope": {
        "type": "object",
        "description": "The `?view=flat` projection — the five fields a simple consumer needs.",
        "properties": {
          "surface": { "$ref": "#/components/schemas/Surface" },
          "status": { "type": "string" },
          "text": { "type": "string" },
          "markdown": { "type": "string" },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": { "type": "string", "format": "uri" },
                "title": { "type": "string" }
              }
            }
          },
          "warnings": { "type": "array", "items": { "type": "string" } }
        }
      },
      "UsageResponse": {
        "type": "object",
        "properties": {
          "plan": { "type": "string", "example": "internal" },
          "usage": {
            "type": "object",
            "properties": {
              "byGroup": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "surface": { "$ref": "#/components/schemas/Surface" },
                    "method": { "$ref": "#/components/schemas/Method" },
                    "region": { "type": "string" },
                    "callCount": { "type": "integer" },
                    "creditsCharged": { "type": "number" },
                    "cogsUsd": { "type": "number" }
                  }
                }
              },
              "totals": {
                "type": "object",
                "properties": {
                  "callCount": { "type": "integer" },
                  "creditsCharged": { "type": "number" },
                  "cogsUsd": { "type": "number" }
                }
              }
            }
          },
          "balance": {
            "type": "object",
            "properties": {
              "plan": { "type": "string" },
              "credits": { "type": "number", "nullable": true },
              "unlimited": { "type": "boolean" }
            }
          }
        }
      },
      "SurfacesResponse": {
        "type": "object",
        "properties": {
          "surfaces": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "$ref": "#/components/schemas/Surface" },
                "autoDefault": { "$ref": "#/components/schemas/Method" },
                "methods": {
                  "type": "object",
                  "description": "Per-method capability state for the surface.",
                  "additionalProperties": true
                }
              }
            }
          },
          "schemaVersion": { "type": "string", "example": "2026-06-27" }
        }
      },
      "RegionsResponse": {
        "type": "object",
        "properties": {
          "targeting": {
            "type": "object",
            "properties": {
              "country": { "type": "string" },
              "state": { "type": "string" },
              "city": { "type": "string" },
              "language": { "type": "string" }
            }
          },
          "examples": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Region" }
          },
          "popularCountries": {
            "type": "array",
            "items": { "type": "string" }
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "schemaVersion": { "type": "string", "example": "2026-06-27" },
          "bindings": {
            "type": "object",
            "properties": {
              "artifacts": { "type": "boolean" },
              "edgeCache": { "type": "boolean" },
              "hyperdrive": { "type": "boolean" }
            }
          }
        },
        "required": ["status", "schemaVersion", "bindings"]
      },
      "Error": {
        "type": "object",
        "description": "A stable, machine-readable error.",
        "properties": {
          "code": { "type": "string", "description": "Stable error code, e.g. AUTH_INVALID, VALIDATION_FAILED, RATE_LIMIT_EXCEEDED, IDEMPOTENCY_CONFLICT, UNSUPPORTED_METHOD_FOR_SURFACE, JOB_NOT_FOUND, ARTIFACT_NOT_FOUND." },
          "error": { "type": "string", "description": "Human-readable detail." }
        },
        "required": ["code"]
      }
    }
  }
}
