openapi: 3.1.0
info:
  title: SEO Checker API
  version: "1.0.0"
  description: |
    Public, async **SEO data API**. The SERP service (below) is available today;
    indexing, SERM, seo-audit, competitors, and semantic services are on the way,
    all sharing this same API-key auth, credit billing, and task lifecycle.

    Submit a task, then poll for the result — the crawl runs in the background.
    Authenticate with a per-organization API key (`Authorization: Bearer sk_live_…`).
    Each task costs credits from your organization's balance.

    This document is the source of truth for the `/v1` reference. It is kept
    next to the API code (`back-end/api/docs/openapi/v1.yaml`); the docs site
    renders it and serves a copy for download.
  contact:
    name: SEO Checker
    url: https://seo-checker.com.ua
servers:
  - url: https://api.seo-checker.com.ua/v1
    description: Production
  - url: https://dev-api.seo-checker.com.ua/v1
    description: Development
security:
  - apiKey: []
tags:
  - name: Tasks
    description: Submit and retrieve SERP tasks.
  - name: Metadata
    description: Supported regions and other discovery data.
paths:
  /serp/tasks:
    post:
      tags: [Tasks]
      summary: Submit a SERP task
      operationId: createSerpTask
      description: |
        Queue a full organic SERP crawl for a keyword in a region on a device.
        Returns `202` with a `task_id` and `status: "queued"`. Poll
        `GET /serp/tasks/{id}` until `status` is `done` or `error`.

        Billing: the task cost (1 credit per keyword) is charged on create,
        **including cache hits** (a fresh identical crawl is reused but still
        billed). A terminal `error` is automatically refunded. Insufficient
        balance returns `402` and no task is created.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
            examples:
              default:
                value:
                  keyword: best running shoes
                  region: us
                  device: desktop
                  depth: 1
      responses:
        "202":
          description: Task accepted and queued (or served immediately from cache as `done`).
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTaskResponse"
              examples:
                queued:
                  value: { task_id: 58e6ea50-492b-494c-9767-f6f1396e9298, status: queued }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Tasks]
      summary: List tasks
      operationId: listSerpTasks
      description: List your organization's tasks, newest first.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: A page of tasks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks:
                    type: array
                    items: { $ref: "#/components/schemas/Task" }
                  total: { type: integer, description: Total task count for the org. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /serp/tasks/{id}:
    get:
      tags: [Tasks]
      summary: Get a task
      operationId: getSerpTask
      description: |
        Retrieve one task by id. Poll this endpoint after submitting — it
        returns the current `status`; `result` is populated once `status` is
        `done`, and `error` once `status` is `error`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: The task.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Task" }
              examples:
                done:
                  value:
                    task_id: 58e6ea50-492b-494c-9767-f6f1396e9298
                    type: serp_full
                    status: done
                    request: { keyword: best running shoes, region: us, device: desktop, depth: 1 }
                    result:
                      keyword: best running shoes
                      region: us
                      device: desktop
                      total_results: 1010
                      items:
                        - position: 1
                          url: https://example.com/best-running-shoes
                          title: "Best Running Shoes — 2026 Guide"
                          snippet: A representative organic result.
                          displayed_url: example.com
                    error: null
                    cost: 1
                    created_at: "2026-08-03T11:23:22Z"
                    started_at: "2026-08-03T11:23:22Z"
                    finished_at: "2026-08-03T11:23:23Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /regions:
    get:
      tags: [Metadata]
      summary: List supported regions
      operationId: listRegions
      description: The regions accepted in the `region` field of a task request.
      responses:
        "200":
          description: Supported regions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  regions:
                    type: array
                    items: { $ref: "#/components/schemas/Region" }
              examples:
                default:
                  value:
                    regions:
                      - { code: us, name: United States, google_domain: google.com, country: US, language: en }
                      - { code: ua, name: Ukraine, google_domain: google.com.ua, country: UA, language: uk }
        "401": { $ref: "#/components/responses/Unauthorized" }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_…
      description: |
        Your organization's API key, sent as `Authorization: Bearer sk_live_…`.
        Create and manage keys in the SEO Checker dashboard (API section).
  headers:
    X-RateLimit-Limit:
      description: Requests allowed per minute for this key.
      schema: { type: integer }
    X-RateLimit-Remaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    X-RateLimit-Reset:
      description: Unix epoch second when the window resets.
      schema: { type: integer }
    Retry-After:
      description: Seconds to wait before retrying (on 429).
      schema: { type: integer }
  schemas:
    CreateTaskRequest:
      type: object
      required: [keyword, region]
      properties:
        type:
          type: string
          enum: [serp_full]
          default: serp_full
          description: Task type. Only `serp_full` (full organic SERP) is available today.
        keyword:
          type: string
          minLength: 1
          description: The search query.
          examples: [best running shoes]
        region:
          type: string
          description: Region code from `GET /regions`.
          examples: [us]
        device:
          type: string
          enum: [desktop, mobile, tablet]
          default: desktop
        depth:
          type: integer
          minimum: 1
          maximum: 5
          default: 1
          description: Number of result pages to crawl (10 results per page).
    CreateTaskResponse:
      type: object
      properties:
        task_id: { type: string, format: uuid }
        status: { $ref: "#/components/schemas/TaskStatus" }
    Task:
      type: object
      properties:
        task_id: { type: string, format: uuid }
        type: { type: string, enum: [serp_full] }
        status: { $ref: "#/components/schemas/TaskStatus" }
        request: { $ref: "#/components/schemas/CreateTaskRequest" }
        result:
          oneOf:
            - $ref: "#/components/schemas/SerpResult"
            - type: "null"
          description: Present once `status` is `done`.
        error:
          oneOf:
            - $ref: "#/components/schemas/ErrorObject"
            - type: "null"
          description: Present once `status` is `error`.
        cost: { type: integer, description: Credits charged for this task. }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
    TaskStatus:
      type: string
      enum: [queued, running, done, error]
      description: |
        Lifecycle: `queued` → `running` → `done` | `error`.
        A cache hit is created directly as `done`.
    SerpResult:
      type: object
      properties:
        keyword: { type: string }
        region: { type: string }
        device: { type: string }
        total_results: { type: integer, description: Approximate total results reported by Google. }
        items:
          type: array
          items: { $ref: "#/components/schemas/OrganicItem" }
    OrganicItem:
      type: object
      properties:
        position: { type: integer, description: 1-based organic rank. }
        url: { type: string, format: uri }
        title: { type: string }
        snippet: { type: [string, "null"] }
        displayed_url: { type: string }
    Region:
      type: object
      properties:
        code: { type: string, examples: [us] }
        name: { type: string, examples: [United States] }
        google_domain: { type: string, examples: [google.com] }
        country: { type: string, examples: [US] }
        language: { type: string, examples: [en] }
    ErrorObject:
      type: object
      properties:
        code: { type: string, description: Stable machine-readable code, e.g. `serpapi.region_unknown`. }
        message: { type: string }
        task_id: { type: [string, "null"], format: uuid }
    ErrorResponse:
      type: object
      properties:
        error: { $ref: "#/components/schemas/ErrorObject" }
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            missing: { value: { error: { code: apikeys.missing, message: API key required } } }
            invalid: { value: { error: { code: apikeys.invalid, message: API key is invalid or revoked } } }
    InsufficientBalance:
      description: Not enough credits to run the task. No task is created.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            default: { value: { error: { code: payments.insufficient_balance, message: Insufficient credit balance } } }
    ValidationError:
      description: The request failed validation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            region: { value: { error: { code: serpapi.region_unknown, message: Unknown region code } } }
            device: { value: { error: { code: serpapi.device_invalid, message: device must be desktop, mobile or tablet } } }
    NotFound:
      description: No task with that id for your organization.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            default: { value: { error: { code: serpapi.task_not_found, message: Task not found } } }
    RateLimited:
      description: Per-key rate limit exceeded.
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
        X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
        X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          examples:
            default: { value: { error: { code: serpapi.rate_limited, message: Rate limit exceeded } } }
