{
  "openapi": "3.1.0",
  "info": {
    "title": "SEO Bandwagon API",
    "version": "1.0.0",
    "summary": "The REST API behind SEO Bandwagon's Chrome extension and developer tools.",
    "description": "Page captures saved by the SEO Bandwagon Chrome extension, domain roll-ups built from them, the account behind an API key, and key management.\n\nAuthenticate with an API key (Authorization: Bearer sbw_...) created at https://seobandwagon.com/dashboard/api-keys, with the scope each operation names. How agents get access: https://seobandwagon.com/auth.md.\n\nThe MCP server at https://seobandwagon.com/api/mcp is described by its server card, https://seobandwagon.com/.well-known/mcp/server-card.json, not here.\n\nEvery error body carries `error` and may carry more fields; ignore the ones you do not recognize.",
    "contact": {
      "name": "SEO Bandwagon",
      "email": "info@seobandwagon.com",
      "url": "https://seobandwagon.com/contact"
    },
    "license": {
      "name": "SEO Bandwagon Terms of Service",
      "url": "https://seobandwagon.com/terms"
    }
  },
  "externalDocs": {
    "description": "API and MCP server documentation",
    "url": "https://seobandwagon.com/docs/api"
  },
  "servers": [
    {
      "url": "https://seobandwagon.com"
    }
  ],
  "tags": [
    {
      "name": "Captures",
      "description": "On-page SEO analyses saved by the SEO Bandwagon Chrome extension, private to the account that saved them."
    },
    {
      "name": "Domains",
      "description": "Per-domain roll-ups built from saved captures."
    },
    {
      "name": "Account",
      "description": "The account behind an API key, and its API keys."
    },
    {
      "name": "Billing",
      "description": "Buying Pro, checking a checkout, and the billing portal."
    },
    {
      "name": "Public",
      "description": "Endpoints that need no authentication."
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Check that the API is up",
        "description": "Liveness only: no database and no authentication. Format per draft-inadarei-api-health-check.",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The API is up.",
            "content": {
              "application/health+json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/api/keyword-suggest": {
      "get": {
        "operationId": "suggestKeywords",
        "summary": "Suggest keywords and metros for partly typed text",
        "description": "Suggestions from a bundled vocabulary of SEO keywords and the metros SEO Bandwagon tracks. It calls no other service and stores nothing, so the answer can be cached.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "What has been typed so far, 2 to 80 characters after trimming.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 80
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions, best first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeywordSuggestions"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/captures": {
      "get": {
        "operationId": "listCaptures",
        "summary": "List your captures of a page",
        "description": "Up to 50 captures of a URL saved by your account, newest first. The URL matches across protocol, www and trailing-slash variants, and falls back to the whole domain when no page matches.",
        "tags": [
          "Captures"
        ],
        "security": [
          {
            "apiKey": [
              "captures"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "A page URL or domain, with or without https:// (for example example.com/page).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Your captures of the page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "post": {
        "operationId": "createCapture",
        "summary": "Save a capture from the Chrome extension",
        "description": "Stores one page analysis for your account. It also updates the roll-up for the page's domain.",
        "tags": [
          "Captures"
        ],
        "security": [
          {
            "apiKey": [
              "captures"
            ]
          },
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "One page analysis from the SEO Bandwagon Chrome extension. Only url is required; every other field is the JSON the extension produces for that part of the analysis.",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "capturedAt": {
                    "type": "string"
                  },
                  "contentHash": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "seoScore": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "content": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "meta": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "headings": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "images": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "links": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "technical": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "techStack": {
                    "type": [
                      "array",
                      "null"
                    ]
                  },
                  "webVitals": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "performance": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "textToHtml": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "aboveFold": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "hreflang": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "schemaValidation": {
                    "anyOf": [
                      {
                        "type": "array"
                      },
                      {
                        "type": "object"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "lazyImages": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "mixedContent": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "iframes": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "socialCards": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "faviconAndFeeds": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "mediaTypes": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "keywordDensity": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "tocNavigation": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "ngrams": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "extensionVersion": {
                    "type": "string"
                  },
                  "userAgent": {
                    "type": "string"
                  },
                  "sessionId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Your account already captured this URL within the last hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Identify the account behind an API key",
        "description": "Returns the account and the key that authenticated the request. It proves a key, so a signed-in session does not satisfy it.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The key and its account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "No valid API key: `{\"error\":\"API key required\"}`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/billing/checkout": {
      "post": {
        "operationId": "startCheckout",
        "summary": "Start buying Pro",
        "description": "Returns a Stripe Checkout URL for a person to open and pay; nothing is charged until they do, and the plan changes once Stripe confirms the payment. An open checkout started earlier is handed back instead of a second one (reused). Send an Idempotency-Key header to make a retry safe. Needs a signed-in session, or an API key holding the `billing` scope, which extension keys never get. Only Pro is sold by checkout: Agentic Local, Enterprise and the services are bought by talking to us (not_self_serve).",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique token; the same token with a different body is idempotency_conflict.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The checkout to open.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Checkout"
                }
              }
            }
          },
          "400": {
            "description": "The body is not `{\"plan\": \"pro\"}` or empty (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the `billing` scope (`insufficient_scope`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account already pays for Pro (`already_subscribed`) or has a payment past due (`payment_past_due`): use the billing portal. Or the Idempotency-Key was used with a different body (`idempotency_conflict`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "That plan is not sold by checkout (`not_self_serve`); the fix links the contact page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "Stripe refused or failed (`stripe_error`). Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Checkout is not configured yet (`billing_unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/billing/checkout/{id}": {
      "get": {
        "operationId": "getCheckout",
        "summary": "Check a checkout",
        "description": "Whether a checkout this account started has been paid, and the plan the account is on now. Same access as startCheckout.",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The session_id from startCheckout.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The checkout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the `billing` scope (`insufficient_scope`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No checkout with that id belongs to this account (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Stripe could not be read (`stripe_error`). Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Checkout is not configured yet (`billing_unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/billing/portal": {
      "post": {
        "operationId": "openBillingPortal",
        "summary": "Open the billing portal",
        "description": "Returns a URL to Stripe's billing portal, where a person can change the card, see invoices or cancel; a cancelled plan runs to the end of the month already paid. Same access as startCheckout.",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "201": {
            "description": "The portal to open.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingPortal"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the `billing` scope (`insufficient_scope`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account has never started a checkout (`no_billing_account`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "Stripe refused or failed (`stripe_error`). Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Billing is not configured yet (`billing_unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List your API keys",
        "description": "Your live keys, newest first. Never returns a key itself: only its hash is stored. Needs a signed-in session, or an API key holding the `keys` scope.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your live keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create an API key",
        "description": "Creates a key with the scopes you choose and returns it once. Needs a signed-in session (a request from another origin is refused), or an API key holding the `keys` scope, which can grant only scopes it holds itself, never `keys`.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key. This is the only time it is shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewApiKey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The request came from another origin, or an API key asked for a scope it does not hold (`scope_not_allowed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account already holds as many live keys as it may (`{\"error\":\"key_limit\"}`, with the number in `limit`). Revoke one first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Revoke an API key",
        "description": "Revokes one of your keys, and every key it created. A revoked key is refused from its next request. Needs a signed-in session (a request from another origin is refused), or an API key holding the `keys` scope, which may revoke only itself or keys it created.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The key's id, from listApiKeys.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The request came from another origin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No live key with that id belongs to your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/signup": {
      "post": {
        "operationId": "requestSignupCode",
        "summary": "Email a one-time sign-up code",
        "description": "How an agent gets its first key without a browser. Sends a one-time code to the address; the person who reads it passes the code to verifySignupCode. Always answers 202 with the same body, whether or not the address has an account. Codes work once, for 15 minutes, and only the newest counts.",
        "tags": [
          "Account",
          "Public"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "A code was sent, or would have been.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupSent"
                }
              }
            }
          },
          "400": {
            "description": "The address is invalid (`email_invalid`), a throwaway-mail domain (`email_not_allowed`) or has no mail server (`email_undeliverable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many codes for this address, inbox, domain or IP (`signup_rate_limited`), with Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Email is not available right now (`email_unavailable`), with Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/signup/verify": {
      "post": {
        "operationId": "verifySignupCode",
        "summary": "Exchange a sign-up code for an API key",
        "description": "Creates a free account when the address has none, or adds a key to the existing account, and returns the key once. The key carries mcp, score and keys.",
        "tags": [
          "Account",
          "Public"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupVerifyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The key, the account and what to do next. This is the only time the key is shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupVerified"
                }
              }
            }
          },
          "400": {
            "description": "The code is wrong (`code_invalid`, with attempts_remaining), expired, used or superseded (`code_expired`), or locked after 5 tries (`code_locked`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Admin accounts get keys from the dashboard (`admin_account`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account already holds 5 live keys (`key_limit`). Revoke one; the same code still works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts from this IP (`signup_rate_limited`), with Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/domain/{domain}/quick": {
      "get": {
        "operationId": "getDomainStats",
        "summary": "Get the roll-up for a domain",
        "description": "Averages across every capture of the domain, from any account; no individual capture is exposed. A domain with no captures answers 200 with nulls and sampleSize 0. A leading www. is ignored.",
        "tags": [
          "Domains"
        ],
        "security": [
          {
            "apiKey": [
              "captures"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "A bare hostname, for example example.com.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The roll-up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuickStats"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/domain/quick-batch": {
      "post": {
        "operationId": "getDomainStatsBatch",
        "summary": "Get the roll-ups for several domains",
        "description": "The same roll-up as getDomainStats for up to 20 domains in one request. Send bare domains only.",
        "tags": [
          "Domains"
        ],
        "security": [
          {
            "apiKey": [
              "captures"
            ]
          },
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuickBatchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One roll-up per valid domain, and the inputs that were not domains.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuickBatchResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sbw_ followed by 48 hex characters",
        "description": "An SEO Bandwagon API key, created at https://seobandwagon.com/dashboard/api-keys and shown once. It carries the scopes chosen when it was created (captures, citations, link-building:read, link-building:write, score, keywords, keys, mcp, billing); an operation names the scope it needs, and a key without it gets 403. A key created before scopes existed carries legacy:all, which grants every scope."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Secure-authjs.session-token",
        "description": "A signed-in browser session on seobandwagon.com. It stands for the whole account, so it holds every scope."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is not valid. On a validation failure, `details` holds what failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No valid API key and no signed-in session.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The API key lacks the scope this operation needs: `{\"error\":\"insufficient_scope\",\"required\":\"<scope>\"}`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Over the rate limit for your IP address.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the limit resets.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "The request failed on SEO Bandwagon's side.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "The database is not configured, or checking the API key failed. Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "CheckoutRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "pro"
            ],
            "description": "Only pro is sold by checkout. Defaults to pro."
          }
        }
      },
      "Checkout": {
        "type": "object",
        "required": [
          "url",
          "session_id",
          "amount_total",
          "currency",
          "expires_at",
          "reused"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Stripe Checkout, for a person to open."
          },
          "session_id": {
            "type": "string"
          },
          "amount_total": {
            "type": "integer",
            "description": "In cents, as Stripe reports it."
          },
          "currency": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "reused": {
            "type": "boolean",
            "description": "An open checkout started earlier was handed back."
          }
        }
      },
      "CheckoutStatus": {
        "type": "object",
        "required": [
          "status",
          "payment_status",
          "plan"
        ],
        "properties": {
          "status": {
            "type": "string",
            "description": "Stripe's session status: open, complete or expired."
          },
          "payment_status": {
            "type": "string",
            "description": "paid, unpaid or no_payment_required."
          },
          "plan": {
            "type": "string",
            "description": "The plan the account is on now."
          }
        }
      },
      "BillingPortal": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A short code or phrase, such as insufficient_scope."
          },
          "message": {
            "type": "string",
            "description": "A longer explanation, when there is one."
          },
          "details": {
            "type": "object",
            "description": "What failed validation, on a 400."
          },
          "required": {
            "type": "string",
            "description": "On insufficient_scope, the scope needed."
          },
          "limit": {
            "type": "integer",
            "description": "On key_limit, the most live keys allowed."
          },
          "invalid": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On a quick-batch 400, the inputs that were not domains."
          }
        },
        "additionalProperties": true
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "version",
          "releaseId",
          "description",
          "time"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pass"
            ]
          },
          "version": {
            "type": "string"
          },
          "releaseId": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KeywordSuggestions": {
        "type": "object",
        "required": [
          "suggestions"
        ],
        "properties": {
          "suggestions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "text",
                "kind",
                "secondary"
              ],
              "properties": {
                "text": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "keyword",
                    "expansion",
                    "location"
                  ]
                },
                "secondary": {
                  "type": "string"
                },
                "slug": {
                  "type": "string",
                  "description": "On a location, the metro page it leads to: /local-seo/<slug>."
                }
              }
            }
          }
        }
      },
      "CaptureCreated": {
        "type": "object",
        "required": [
          "id",
          "url",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Capture": {
        "type": "object",
        "required": [
          "id",
          "url",
          "domain",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "overallScore": {
            "type": [
              "integer",
              "null"
            ]
          },
          "seoScore": {
            "type": [
              "object",
              "null"
            ]
          },
          "contentHash": {
            "type": [
              "string",
              "null"
            ]
          },
          "content": {
            "type": [
              "object",
              "null"
            ]
          },
          "meta": {
            "type": [
              "object",
              "null"
            ]
          },
          "headings": {
            "type": [
              "object",
              "null"
            ]
          },
          "images": {
            "type": [
              "object",
              "null"
            ]
          },
          "links": {
            "type": [
              "object",
              "null"
            ]
          },
          "technical": {
            "type": [
              "object",
              "null"
            ]
          },
          "techStack": {
            "type": [
              "array",
              "null"
            ]
          },
          "webVitals": {
            "type": [
              "object",
              "null"
            ]
          },
          "performance": {
            "type": [
              "object",
              "null"
            ]
          },
          "textToHtml": {
            "type": [
              "object",
              "null"
            ]
          },
          "aboveFold": {
            "type": [
              "object",
              "null"
            ]
          },
          "hreflang": {
            "type": [
              "object",
              "null"
            ]
          },
          "schemaValidation": {
            "type": [
              "array",
              "object",
              "null"
            ]
          },
          "lazyImages": {
            "type": [
              "object",
              "null"
            ]
          },
          "mixedContent": {
            "type": [
              "object",
              "null"
            ]
          },
          "iframes": {
            "type": [
              "object",
              "null"
            ]
          },
          "socialCards": {
            "type": [
              "object",
              "null"
            ]
          },
          "faviconAndFeeds": {
            "type": [
              "object",
              "null"
            ]
          },
          "mediaTypes": {
            "type": [
              "object",
              "null"
            ]
          },
          "keywordDensity": {
            "type": [
              "object",
              "null"
            ]
          },
          "tocNavigation": {
            "type": [
              "object",
              "null"
            ]
          },
          "ngrams": {
            "type": [
              "object",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CaptureList": {
        "type": "object",
        "required": [
          "url",
          "domain",
          "totalCaptures",
          "captures"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "The url parameter as sent."
          },
          "domain": {
            "type": "string"
          },
          "totalCaptures": {
            "type": "integer"
          },
          "captures": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Capture"
            }
          }
        }
      },
      "QuickStats": {
        "type": "object",
        "required": [
          "domain",
          "avgWordCount",
          "schemaPercentage",
          "avgScore",
          "age",
          "sampleSize"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "avgWordCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "schemaPercentage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "avgScore": {
            "type": [
              "integer",
              "null"
            ]
          },
          "age": {
            "type": [
              "string",
              "null"
            ]
          },
          "sampleSize": {
            "type": "integer",
            "description": "How many captures the averages cover."
          }
        }
      },
      "QuickBatchRequest": {
        "type": "object",
        "required": [
          "domains"
        ],
        "properties": {
          "domains": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 255
            }
          }
        }
      },
      "QuickBatchResponse": {
        "type": "object",
        "required": [
          "results",
          "invalid"
        ],
        "properties": {
          "results": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/QuickStats"
            }
          },
          "invalid": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "userId",
          "email",
          "name",
          "plan",
          "key",
          "scopes",
          "capabilities"
        ],
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "enterprise"
            ]
          },
          "key": {
            "type": "object",
            "required": [
              "id",
              "name",
              "createdAt",
              "lastUsedAt"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "lastUsedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same list as scopes, kept for older extension builds."
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "id",
          "name",
          "scopes",
          "createdAt",
          "lastUsedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ApiKeyList": {
        "type": "object",
        "required": [
          "keys"
        ],
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKey"
            }
          }
        }
      },
      "NewApiKeyRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "scopes": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "captures",
                "citations",
                "link-building:read",
                "link-building:write",
                "score",
                "keywords",
                "keys",
                "mcp",
                "billing"
              ]
            },
            "description": "Defaults to the Chrome extension's set (captures, citations, link-building:read, link-building:write, score). Add mcp for the MCP server."
          }
        }
      },
      "SignupRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          }
        }
      },
      "SignupSent": {
        "type": "object",
        "required": [
          "status",
          "email",
          "expires_in",
          "next_step"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "sent"
            ]
          },
          "email": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the code works for."
          },
          "next_step": {
            "type": "object",
            "description": "The verifySignupCode request to make with the emailed code.",
            "properties": {
              "method": {
                "type": "string",
                "enum": [
                  "POST"
                ]
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "body": {
                "type": "object"
              }
            }
          },
          "docs_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "SignupVerifyRequest": {
        "type": "object",
        "required": [
          "email",
          "code"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "code": {
            "type": "string",
            "description": "The emailed code, such as K7QM-4TZP. Case, spaces and the hyphen are ignored."
          },
          "key_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          }
        }
      },
      "SignupVerified": {
        "type": "object",
        "required": [
          "key",
          "key_id",
          "scopes",
          "account"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^sbw_[0-9a-f]{48}$"
          },
          "key_id": {
            "type": "string",
            "format": "uuid"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "account": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "created",
                  "existing"
                ]
              },
              "plan": {
                "type": "string"
              }
            }
          },
          "next_steps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "docs": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "NewApiKey": {
        "type": "object",
        "required": [
          "id",
          "name",
          "scopes",
          "createdAt",
          "key"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "key": {
            "type": "string",
            "pattern": "^sbw_[0-9a-f]{48}$"
          }
        }
      }
    }
  }
}