{
  "openapi": "3.1.0",
  "info": {
    "title": "ApexApi",
    "version": "1.0.0",
    "summary": "Unified OpenAI-compatible AI gateway with live web context for agents",
    "description": "One API key (`ak-...`) and one base URL to call 120+ models from OpenAI, Anthropic, Google, Alibaba (Qwen), DeepSeek, Mistral, and more — billed by usage from a prepaid credit balance. The same key also gives agents live web context: scrape a page, crawl a site, or pull structured data. Autonomous agents can self-register, fund with card or USDC on Base, and start calling with no human in the loop (see the `Agent onboarding` endpoints and https://apexapi.dev/auth.md).\n\nChat completions are OpenAI-compatible: point any OpenAI SDK at `https://api.apexapi.dev/v1` and pass your `ak-` key as the bearer token.\n\nAll errors use the envelope `{ \"error\": { \"code\", \"message\", \"type\" } }`.",
    "termsOfService": "https://apexapi.dev/legal/terms",
    "contact": {
      "name": "ApexApi",
      "url": "https://apexapi.dev"
    }
  },
  "servers": [
    {
      "url": "https://api.apexapi.dev",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "Chat", "description": "OpenAI-compatible chat completions" },
    { "name": "Images", "description": "Image generation" },
    { "name": "Videos", "description": "Video generation (async jobs)" },
    { "name": "Audio", "description": "Text-to-speech" },
    { "name": "Models", "description": "Live model catalog with pricing" },
    { "name": "Web Context", "description": "Live web data for AI: scrape, crawl, structured extract. Failures are free." },
    { "name": "Agent onboarding", "description": "Autonomous agent registration, funding (card or USDC on Base), and optional human claim. Machine-followable recipe: https://apexapi.dev/auth.md" },
    { "name": "Discovery", "description": "Machine-readable service metadata" }
  ],
  "paths": {
    "/v1/chat/completions": {
      "post": {
        "tags": ["Chat"],
        "operationId": "createChatCompletion",
        "summary": "Create a chat completion",
        "description": "OpenAI Chat Completions-compatible. Unknown fields (e.g. `response_format`, `seed`) are passed through to the upstream provider. Supports streaming via Server-Sent Events when `stream: true`. Multimodal content parts (`image_url` for vision models, `file` for PDF-capable models) use the standard OpenAI part format.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ChatCompletionRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat completion (OpenAI response shape). When `stream: true`, the response is `text/event-stream` SSE chunks instead.",
            "headers": {
              "X-ApexApi-Cost": { "$ref": "#/components/headers/XApexApiCost" },
              "X-ApexApi-Credits-Remaining": { "$ref": "#/components/headers/XApexApiCreditsRemaining" },
              "X-ApexApi-Request-Id": { "$ref": "#/components/headers/XApexApiRequestId" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatCompletionResponse" }
              },
              "text/event-stream": {
                "schema": { "type": "string", "description": "SSE stream of OpenAI-format chat completion chunks, terminated by `data: [DONE]`." }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/images/generations": {
      "post": {
        "tags": ["Images"],
        "operationId": "createImage",
        "summary": "Generate images",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ImageGenerationRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated image URL(s).",
            "headers": {
              "X-ApexApi-Cost": { "$ref": "#/components/headers/XApexApiCost" },
              "X-ApexApi-Credits-Remaining": { "$ref": "#/components/headers/XApexApiCreditsRemaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MediaUrlResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/videos/generations": {
      "post": {
        "tags": ["Videos"],
        "operationId": "createVideo",
        "summary": "Start a video generation job",
        "description": "Async: returns a job id immediately. Poll `GET /v1/videos/generations/{id}` until `status` is `completed` or `failed`. Billing finalizes when the job settles.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/VideoGenerationRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAccepted" },
                "example": { "id": "d1f8a9c2-…", "status": "queued" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/v1/videos/generations/{id}": {
      "get": {
        "tags": ["Videos"],
        "operationId": "getVideoJob",
        "summary": "Poll a video generation job",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Job status. `completed` includes the result `url`; `failed` includes `error`; otherwise `processing`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VideoJobStatus" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/audio/speech": {
      "post": {
        "tags": ["Audio"],
        "operationId": "createSpeech",
        "summary": "Generate speech (text-to-speech)",
        "description": "Generates voiceover via ElevenLabs models. Returns JSON with a URL to an MP3 file (not raw audio bytes). Billed per 1,000 characters.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SpeechRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "URL of the generated MP3.",
            "headers": {
              "X-ApexApi-Cost": { "$ref": "#/components/headers/XApexApiCost" },
              "X-ApexApi-Credits-Remaining": { "$ref": "#/components/headers/XApexApiCreditsRemaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MediaUrlResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/models": {
      "get": {
        "tags": ["Models"],
        "operationId": "listModels",
        "summary": "List available models",
        "description": "Public — no authentication required. Returns the live catalog with per-model capabilities and markup-applied pricing.",
        "security": [],
        "responses": {
          "200": {
            "description": "Model list.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ModelList" }
              }
            }
          }
        }
      }
    },
    "/v1/scrape": {
      "post": {
        "tags": ["Web Context"],
        "operationId": "scrapePage",
        "summary": "Scrape a URL into LLM-ready content",
        "description": "Fetches a single page, strips boilerplate, and returns the main content as markdown, HTML, or text. `stealth: \"auto\"` escalates to the anti-bot rail only when needed. Billed per successfully returned page ($0.002 standard, $0.008 stealth); failed fetches are free. Respects robots.txt; private/internal addresses are rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ScrapeRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scraped content.",
            "headers": {
              "X-ApexApi-Cost": { "$ref": "#/components/headers/XApexApiCost" },
              "X-ApexApi-Credits-Remaining": { "$ref": "#/components/headers/XApexApiCreditsRemaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScrapeResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": {
            "description": "Domain blocked or disallowed by robots.txt (`blocked_domain`, `robots_disallowed`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "422": {
            "description": "Page fetched but no extractable content (`no_content`). Not billed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": {
            "description": "Fetch failed or page blocked by anti-bot (`fetch_failed`, `scrape_blocked`). Not billed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/v1/crawl": {
      "post": {
        "tags": ["Web Context"],
        "operationId": "startCrawl",
        "summary": "Crawl a site into LLM-ready content",
        "description": "Async: crawls same-domain pages from the seed URL. Returns a job id; poll `GET /v1/crawl/{id}`. Billed $0.005 per successfully crawled page, capped at `limit` (max cost = limit × $0.005). Failed pages are free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CrawlRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Crawl job accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAccepted" },
                "example": { "id": "d1f8a9c2-…", "status": "running" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": {
            "description": "Domain blocked or disallowed by robots.txt (`blocked_domain`, `robots_disallowed`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/crawl/{id}": {
      "get": {
        "tags": ["Web Context"],
        "operationId": "getCrawlJob",
        "summary": "Poll a crawl job",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Job status. `completed` includes `page_count`, `cost`, `result_url` (full page set as JSON) and, when freshly settled, inline `pages`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CrawlJobStatus" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/extract": {
      "post": {
        "tags": ["Web Context"],
        "operationId": "startExtract",
        "summary": "Extract structured data",
        "description": "Async: runs a ready-made scraper (Amazon, Walmart, Crunchbase, G2, Google Maps, Zillow — list via `GET /v1/extract/scrapers`) and returns structured JSON records. Poll `GET /v1/extract/{id}`. Billed $0.01 per record returned, capped at `limit`. Failed or empty extractions are free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ExtractRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Extract job accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAccepted" },
                "example": { "id": "a7c1b3d4-…", "status": "running" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/v1/extract/{id}": {
      "get": {
        "tags": ["Web Context"],
        "operationId": "getExtractJob",
        "summary": "Poll an extract job",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Job status. `completed` includes `record_count`, `cost`, `result_url` and, when freshly settled, inline `records`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ExtractJobStatus" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/extract/scrapers": {
      "get": {
        "tags": ["Web Context"],
        "operationId": "listScrapers",
        "summary": "List available extract scrapers",
        "responses": {
          "200": {
            "description": "Scraper catalog.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScraperList" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/agent/register": {
      "post": {
        "tags": ["Agent onboarding"],
        "operationId": "agentRegister",
        "summary": "Register an anonymous agent account",
        "description": "No authentication required. Returns a working `ak-` API key and a `claim_token` — STORE BOTH DURABLY; the claim token is the only recovery path. Accounts that stay unfunded and unclaimed are deleted after ~30 days; funded accounts never expire. Rate limit: 10 registrations per IP per day.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["type"],
                "properties": {
                  "type": { "type": "string", "enum": ["anonymous"] }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["account_id", "api_key", "claim_token", "fund_url", "expires_unless_funded_at", "instructions"],
                  "properties": {
                    "account_id": { "type": "string", "description": "Account UUID." },
                    "api_key": { "type": "string", "description": "Plaintext `ak-` API key — shown once." },
                    "claim_token": { "type": "string", "description": "`act_`-prefixed token — shown once; the only recovery path." },
                    "fund_url": { "type": "string", "format": "uri", "description": "No-login funding page (card or USDC)." },
                    "expires_unless_funded_at": { "type": "string", "format": "date-time" },
                    "instructions": { "type": "string" },
                    "welcome_credits_usd": { "type": "string", "example": "0.05", "description": "Present only when a one-time starter credit was granted at registration — added to the balance immediately, so first API calls work before any funding. Grants are limited per IP per day; absent = no credit granted." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": {
            "description": "Per-IP daily registration cap exceeded (`agent_register_rate_limited`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/agent/account": {
      "get": {
        "tags": ["Agent onboarding"],
        "operationId": "agentAccount",
        "summary": "Get account balance and claim status",
        "description": "Poll this (≥5s interval) after showing `fund_url` to detect funding. `unclaimed_funding_remaining_usd` reflects the $500 card-funding cap for unclaimed accounts (USDC funding is not capped).",
        "responses": {
          "200": {
            "description": "Account state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["balance_usd", "claimed"],
                  "properties": {
                    "balance_usd": { "type": "string", "description": "Decimal string, 6 fraction digits." },
                    "claimed": { "type": "boolean" },
                    "fund_url": { "type": ["string", "null"], "format": "uri" },
                    "expires_unless_funded_at": { "type": ["string", "null"], "format": "date-time" },
                    "unclaimed_funding_remaining_usd": { "type": ["string", "null"], "description": "Remaining card-funding headroom for unclaimed accounts; null once claimed." }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/agent/fund": {
      "post": {
        "tags": ["Agent onboarding"],
        "operationId": "agentFund",
        "summary": "Fund autonomously with USDC on Base",
        "description": "Returns a USDC deposit address on Base. Send exactly `amount_usdc` from your wallet, then poll `GET /agent/account` until `balance_usd` rises (settles in seconds). Whole USD amounts only, $5–$10,000 per call. USDC funding has no unclaimed cap.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["amount_usd"],
                "properties": {
                  "amount_usd": { "type": "integer", "minimum": 5, "maximum": 10000, "description": "Whole USD amount." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deposit instructions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["payment_intent", "network", "deposit_address", "token", "amount_usdc", "amount_usd", "instructions"],
                  "properties": {
                    "payment_intent": { "type": "string" },
                    "network": { "type": "string", "enum": ["base"] },
                    "deposit_address": { "type": "string" },
                    "token": { "type": "string", "enum": ["usdc"] },
                    "token_contract": { "type": ["string", "null"], "description": "USDC contract address on Base." },
                    "amount_usdc": { "type": "string" },
                    "amount_usd": { "type": "integer" },
                    "instructions": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": {
            "description": "Funding rail temporarily unavailable (`funding_unavailable`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/agent/claim/init": {
      "post": {
        "tags": ["Agent onboarding"],
        "operationId": "agentClaimInit",
        "summary": "Start a human claim (device flow)",
        "description": "RFC 8628-style device flow. Show the human BOTH `verification_uri` and `user_code`, then poll `/agent/claim/poll`. Claiming is optional — it adds dashboard visibility and removes card-funding caps; the key keeps working unchanged.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["claim_token"],
                "properties": {
                  "claim_token": { "type": "string", "minLength": 8 },
                  "email": { "type": "string", "format": "email", "description": "Optional; binds the claim to this address." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device-flow challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["user_code", "verification_uri", "verification_uri_complete", "expires_in", "interval"],
                  "properties": {
                    "user_code": { "type": "string", "description": "6-digit code." },
                    "verification_uri": { "type": "string", "format": "uri" },
                    "verification_uri_complete": { "type": "string", "format": "uri" },
                    "expires_in": { "type": "integer", "description": "Seconds (900)." },
                    "interval": { "type": "integer", "description": "Minimum poll interval in seconds (5)." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": {
            "description": "Unknown claim token (`invalid_claim_token`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/agent/claim/poll": {
      "post": {
        "tags": ["Agent onboarding"],
        "operationId": "agentClaimPoll",
        "summary": "Poll a pending claim",
        "description": "Poll at ≥`interval` seconds. Non-terminal states return HTTP 400 with codes `authorization_pending` (keep polling), `slow_down` (polling too fast), or `expired_token` (re-init for a new code).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["claim_token"],
                "properties": {
                  "claim_token": { "type": "string", "minLength": 8 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "dashboard_url"],
                  "properties": {
                    "status": { "type": "string", "enum": ["claimed"] },
                    "dashboard_url": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Non-terminal or error state: `authorization_pending`, `slow_down`, `expired_token`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "401": {
            "description": "Unknown claim token (`invalid_claim_token`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": ["Discovery"],
        "operationId": "getProtectedResourceMetadata",
        "summary": "Protected-resource metadata (RFC 9728) with agent_auth extension",
        "security": [],
        "responses": {
          "200": {
            "description": "Service metadata. The `agent_auth` block points agents at the registration and claim endpoints; the credential issued is a long-lived `ak-` API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resource": { "type": "string", "format": "uri" },
                    "bearer_methods_supported": { "type": "array", "items": { "type": "string" } },
                    "resource_documentation": { "type": "string", "format": "uri" },
                    "agent_auth": {
                      "type": "object",
                      "properties": {
                        "spec": { "type": "string", "format": "uri" },
                        "auth_md": { "type": "string", "format": "uri" },
                        "identity_endpoint": { "type": "string", "format": "uri" },
                        "claim_endpoint": { "type": "string", "format": "uri" },
                        "identity_types_supported": { "type": "array", "items": { "type": "string" } },
                        "credential_type": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["Discovery"],
        "operationId": "getHealth",
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "description": "Service health.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string" },
                    "version": { "type": "string" },
                    "timestamp": { "type": "string", "format": "date-time" },
                    "uptime": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "ApexApi API key, prefix `ak-` (48 chars). Create one in the dashboard (https://apexapi.dev/keys) or via agent self-registration (`POST /agent/register`)."
      }
    },
    "parameters": {
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Job id returned by the submit call. Jobs are scoped to the authenticated account; other accounts' ids return 404."
      }
    },
    "headers": {
      "XApexApiCost": {
        "description": "USD cost of this request (decimal string).",
        "schema": { "type": "string" }
      },
      "XApexApiCreditsRemaining": {
        "description": "Remaining credit balance in USD (decimal string).",
        "schema": { "type": "string" }
      },
      "XApexApiRequestId": {
        "description": "Request id for support and log correlation.",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "type"],
            "properties": {
              "code": { "type": "string", "description": "Machine-readable error code, e.g. `invalid_api_key`, `insufficient_credits`, `model_not_allowed`, `guardrail_blocked`, `authorization_pending`." },
              "message": { "type": "string" },
              "type": {
                "type": "string",
                "enum": ["invalid_request_error", "authentication_error", "authorization_error", "insufficient_credits_error", "rate_limit_error", "api_error"]
              }
            }
          }
        }
      },
      "ChatMessage": {
        "type": "object",
        "required": ["role", "content"],
        "properties": {
          "role": { "type": "string", "enum": ["system", "user", "assistant", "tool"] },
          "content": {
            "description": "String, null, or an array of multimodal parts (OpenAI format).",
            "oneOf": [
              { "type": "string" },
              { "type": "null" },
              { "type": "array", "items": { "$ref": "#/components/schemas/ContentPart" } }
            ]
          },
          "name": { "type": "string" },
          "tool_calls": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "type", "function"],
              "properties": {
                "id": { "type": "string" },
                "type": { "type": "string", "enum": ["function"] },
                "function": {
                  "type": "object",
                  "required": ["name", "arguments"],
                  "properties": {
                    "name": { "type": "string" },
                    "arguments": { "type": "string" }
                  }
                }
              }
            }
          },
          "tool_call_id": { "type": "string" }
        }
      },
      "ContentPart": {
        "type": "object",
        "required": ["type"],
        "properties": {
          "type": { "type": "string", "enum": ["text", "image_url", "file"] },
          "text": { "type": "string" },
          "image_url": {
            "type": "object",
            "required": ["url"],
            "properties": {
              "url": { "type": "string", "description": "HTTPS URL or base64 data URI." },
              "detail": { "type": "string", "enum": ["low", "high", "auto"] }
            }
          },
          "file": {
            "type": "object",
            "required": ["filename", "file_data"],
            "properties": {
              "filename": { "type": "string", "minLength": 1, "maxLength": 256 },
              "file_data": { "type": "string", "description": "Base64 `application/pdf` data URI, max 2 MB decoded. Max 4 file parts per request." }
            }
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": ["model", "messages"],
        "additionalProperties": true,
        "description": "Unknown fields are passed through to the upstream provider (e.g. `response_format`, `seed`).",
        "properties": {
          "model": { "type": "string", "minLength": 1, "description": "Model slug in `maker/model` format, e.g. `openai/gpt-4o`, `anthropic/claude-fable-5`. List: `GET /v1/models`." },
          "messages": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/ChatMessage" } },
          "stream": { "type": "boolean", "default": false },
          "temperature": { "type": "number", "minimum": 0, "maximum": 2 },
          "max_tokens": { "type": "integer", "minimum": 1 },
          "n": { "type": "integer", "minimum": 1, "maximum": 8 },
          "top_p": { "type": "number", "minimum": 0, "maximum": 1 },
          "frequency_penalty": { "type": "number", "minimum": -2, "maximum": 2 },
          "presence_penalty": { "type": "number", "minimum": -2, "maximum": 2 },
          "stop": {
            "oneOf": [
              { "type": "string" },
              { "type": "array", "items": { "type": "string" } }
            ]
          },
          "tools": { "type": "array", "items": {}, "description": "OpenAI tool definitions; forwarded to the provider unmodified." },
          "tool_choice": { "description": "Forwarded to the provider unmodified." }
        }
      },
      "ChatCompletionResponse": {
        "type": "object",
        "additionalProperties": true,
        "description": "Standard OpenAI chat completion object (passthrough from the provider).",
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string" },
          "created": { "type": "integer" },
          "model": { "type": "string" },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "index": { "type": "integer" },
                "message": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "role": { "type": "string" },
                    "content": { "type": ["string", "null"] }
                  }
                },
                "finish_reason": { "type": ["string", "null"] }
              }
            }
          },
          "usage": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "prompt_tokens": { "type": "integer" },
              "completion_tokens": { "type": "integer" },
              "total_tokens": { "type": "integer" }
            }
          }
        }
      },
      "ImageGenerationRequest": {
        "type": "object",
        "required": ["model", "prompt"],
        "properties": {
          "model": { "type": "string", "minLength": 1 },
          "prompt": { "type": "string", "minLength": 1, "maxLength": 4000 },
          "n": { "type": "integer", "minimum": 1, "maximum": 10, "default": 1 },
          "size": { "type": "string", "default": "1024x1024", "description": "`WIDTHxHEIGHT`, each dimension 1–4096." },
          "quality": { "type": "string", "enum": ["standard", "hd"], "default": "standard" },
          "response_format": { "type": "string", "enum": ["url", "b64_json"], "default": "url" },
          "image_url": { "type": "string", "format": "uri", "description": "Optional reference image (img2img/edit)." },
          "image_urls": { "type": "array", "maxItems": 10, "items": { "type": "string", "format": "uri" } }
        }
      },
      "MediaUrlResponse": {
        "type": "object",
        "required": ["created", "data"],
        "properties": {
          "created": { "type": "integer", "description": "Unix seconds." },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Hosted HTTPS URL of the generated media — always a real URL, never inline base64, regardless of the serving model. Links are temporary; download promptly."
                }
              }
            }
          }
        }
      },
      "VideoGenerationRequest": {
        "type": "object",
        "required": ["model", "prompt"],
        "properties": {
          "model": { "type": "string", "minLength": 1 },
          "prompt": { "type": "string", "minLength": 1, "maxLength": 20000 },
          "duration": { "type": "integer", "minimum": 1, "maximum": 60, "description": "Seconds; defaults to 5 when omitted." },
          "resolution": { "type": "string", "enum": ["480p", "720p", "1080p", "2k", "4k"], "description": "Defaults to 720p when omitted." },
          "aspect_ratio": { "type": "string", "enum": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"] },
          "audio": { "type": "boolean", "description": "Generate audio where the model supports it." },
          "image_url": { "type": "string", "format": "uri", "description": "First-frame / reference image (image-to-video). Must be an https URL, not a data URI." },
          "image_urls": { "type": "array", "maxItems": 10, "items": { "type": "string", "format": "uri" } },
          "end_image_url": { "type": "string", "format": "uri", "description": "Last-frame image (supported models only)." },
          "seed": { "type": "integer" },
          "video_url": { "type": "string", "format": "uri" },
          "video_urls": { "type": "array", "maxItems": 5, "items": { "type": "string", "format": "uri" } },
          "audio_url": { "type": "string", "format": "uri" },
          "audio_urls": { "type": "array", "maxItems": 5, "items": { "type": "string", "format": "uri" } }
        }
      },
      "JobAccepted": {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string" }
        }
      },
      "VideoJobStatus": {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["processing", "completed", "failed"] },
          "url": { "type": ["string", "null"], "description": "Result video URL (present when completed)." },
          "error": { "type": "string", "description": "Failure reason (present when failed)." }
        }
      },
      "SpeechRequest": {
        "type": "object",
        "required": ["model", "input"],
        "properties": {
          "model": { "type": "string", "minLength": 1, "description": "Audio model, e.g. `elevenlabs/tts/multilingual-v2`." },
          "input": { "type": "string", "minLength": 1, "maxLength": 10000 },
          "voice": { "type": "string", "description": "Optional voice name or id; empty = model default." }
        }
      },
      "ModelList": {
        "type": "object",
        "required": ["object", "data"],
        "properties": {
          "object": { "type": "string", "enum": ["list"] },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "description": "Model slug (`maker/model`)." },
                "object": { "type": "string", "enum": ["model"] },
                "created": { "type": "integer" },
                "owned_by": { "type": "string" },
                "type": { "type": "string", "description": "`chat`, `image`, `video`, or `audio`." },
                "display_name": { "type": "string" },
                "context_length": { "type": ["integer", "null"] },
                "max_output_tokens": { "type": ["integer", "null"] },
                "capabilities": {
                  "type": "object",
                  "properties": {
                    "streaming": { "type": "boolean" },
                    "images": { "type": "boolean" },
                    "tools": { "type": "boolean" }
                  }
                },
                "pricing": {
                  "type": "object",
                  "description": "USD prices in the model's billing unit. Chat models bill per token (multiply by 1,000,000 for the per-million-token rate); image models per image; video models per second; audio (TTS) models per 1,000 characters. `display` is the human-readable form — tiered media models show a `from $…` base price.",
                  "properties": {
                    "input": { "type": "number", "description": "USD per billing unit for input. 0 for media models." },
                    "output": { "type": "number", "description": "USD per billing unit for output." },
                    "unit": {
                      "type": "string",
                      "enum": ["per_token", "per_image", "per_second", "per_1k_chars"],
                      "description": "Billing unit the `input`/`output` numbers are denominated in."
                    },
                    "display": {
                      "type": "string",
                      "description": "Human-readable price, e.g. `$3.60/M input, $18.00/M output tokens` or `$0.03 per image`."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ScrapeRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Absolute http(s) URL. Private/loopback/internal addresses rejected." },
          "format": { "type": "string", "enum": ["markdown", "html", "text"], "default": "markdown" },
          "stealth": {
            "default": false,
            "description": "`false` = fast standard rail only; `\"auto\"` = escalate to stealth if blocked; `true` = straight to stealth.",
            "oneOf": [
              { "type": "boolean" },
              { "type": "string", "enum": ["auto"] }
            ]
          },
          "timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 60000, "default": 30000 }
        }
      },
      "ScrapeResponse": {
        "type": "object",
        "required": ["url", "final_url", "status", "format", "content", "tier", "cost"],
        "properties": {
          "url": { "type": "string" },
          "final_url": { "type": "string" },
          "status": { "type": "integer" },
          "format": { "type": "string" },
          "content": { "type": "string" },
          "metadata": { "type": "object", "additionalProperties": true },
          "tier": { "type": "string", "enum": ["standard", "stealth"], "description": "Which rail served the page — determines the price charged." },
          "cost": { "type": "number", "description": "USD charged for this page." }
        }
      },
      "CrawlRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Seed URL; same-domain pages are followed." },
          "format": { "type": "string", "enum": ["markdown", "html", "text"], "default": "markdown" },
          "limit": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100, "description": "Max pages — bounds cost (limit × $0.005)." }
        }
      },
      "CrawlJobStatus": {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["running", "completed", "failed"] },
          "page_count": { "type": "integer" },
          "cost": { "type": "number" },
          "result_url": { "type": ["string", "null"], "description": "JSON file with the full page set." },
          "pages": {
            "type": "array",
            "description": "Inline pages (present when freshly settled).",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "url": { "type": "string" },
                "title": { "type": "string" },
                "markdown": { "type": "string" },
                "status": { "type": "integer" }
              }
            }
          },
          "error": { "type": "string" }
        }
      },
      "ExtractRequest": {
        "type": "object",
        "required": ["scraper", "input"],
        "properties": {
          "scraper": { "type": "string", "minLength": 1, "description": "Scraper name from `GET /v1/extract/scrapers`, e.g. `amazon-product`." },
          "input": {
            "description": "Scraper input — a single object (usually `{ \"url\": ... }`) or a non-empty array of such objects.",
            "oneOf": [
              { "type": "object", "additionalProperties": true },
              { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": true } }
            ]
          },
          "limit": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100, "description": "Max records — bounds cost (limit × $0.01)." }
        }
      },
      "ExtractJobStatus": {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["running", "completed", "failed"] },
          "scraper": { "type": "string" },
          "record_count": { "type": "integer" },
          "cost": { "type": "number" },
          "result_url": { "type": ["string", "null"] },
          "records": {
            "type": "array",
            "description": "Inline records (present when freshly settled).",
            "items": { "type": "object", "additionalProperties": true }
          },
          "error": { "type": "string" }
        }
      },
      "ScraperList": {
        "type": "object",
        "required": ["scrapers"],
        "properties": {
          "scrapers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "category", "input_hint"],
              "properties": {
                "name": { "type": "string" },
                "category": { "type": "string", "enum": ["ecommerce", "business", "local", "real-estate"] },
                "input_hint": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request or validation failure (`invalid_request`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "Unauthorized": {
        "description": "Missing, invalid, revoked, or expired API key (`missing_api_key`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "InsufficientCredits": {
        "description": "Balance too low (`insufficient_credits`, `insufficient_org_credits`, `member_cap_exceeded`). Fund the account and retry.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "Forbidden": {
        "description": "Not permitted for this key (`model_not_allowed`, `guardrail_blocked`, `membership_pending`, `account_suspended`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "NotFound": {
        "description": "Model, job, or resource not found (`model_not_found`, `not_found`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "RateLimited": {
        "description": "Request limit or per-key spend guardrail exceeded (`rate_limit_error` codes incl. `credit_limit_exceeded`, `monthly_spend_limit_exceeded`, `daily_spend_limit_exceeded`). Honor `Retry-After`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "ProviderError": {
        "description": "Upstream provider error (`provider_error`). Retry with backoff or try another model.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      },
      "ServiceUnavailable": {
        "description": "Temporarily unavailable (`provider_unavailable`, `spend_check_unavailable`). Retry with backoff.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
      }
    }
  }
}
