openapi: 3.1.0
info:
  title: TopIndexer API
  version: "1.0.0"
  summary: Submit URLs for Google indexing and read back verified index status.
  description: |
    The TopIndexer public REST API. We trigger the crawl, Google decides, and we
    verify whether the page actually lands in the index — refunding what doesn't
    within the 7-day guarantee.

    **Authentication.** Every endpoint requires a Bearer API key created in the
    dashboard (Settings → API keys). Keys look like `ti_live_…`, are scoped to one
    team, and don't expire (but can be revoked).

    **Credits.** `POST /submit` deducts one credit per accepted URL atomically.
    A URL that isn't indexed within 7 days is auto-refunded.
  contact:
    name: TopIndexer Support
    email: hello@topindexer.com
    url: https://topindexer.com/docs/api
  license:
    name: Proprietary
servers:
  - url: https://api.topindexer.com
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Indexing
    description: Submit URLs and track their index status.
  - name: Account
    description: Read your team and credit balance.
paths:
  /api/v1/submit:
    post:
      operationId: submitUrls
      tags: [Indexing]
      summary: Submit up to 50 URLs for indexing
      description: |
        Validate 1–50 URLs, deduct one credit per accepted URL in a single atomic
        batch, and begin triggering the crawl immediately. Non-http(s) URLs are
        dropped and counted in `ignoredInvalid`. Returns `202 Accepted`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubmitRequest"
            examples:
              single:
                summary: One URL
                value:
                  urls: ["https://example.com/page-a"]
              batch:
                summary: A small batch
                value:
                  urls:
                    - "https://example.com/page-a"
                    - "https://example.com/page-b"
      responses:
        "202":
          description: Accepted. Submissions created and credited.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubmitResponse"
              example:
                submitted: 1
                ignoredInvalid: 0
                submissions:
                  - id: "01J9Z0R8H8Q9X2K3M4N5P6Q7R8"
                    url: "https://example.com/page-a"
                    status: "queued"
        "400":
          description: |
            `bad_request` (missing/empty `urls`, or no valid http(s) URL) or
            `too_many` (more than 50 URLs).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                bad_request:
                  value: { error: "bad_request", message: "Provide a non-empty `urls` array." }
                too_many:
                  value: { error: "too_many", message: "Max 50 URLs per request." }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Not enough credits to cover the batch. Nothing is charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
              example:
                error: "insufficient_credits"
                message: "Need 5 credits, have 2."
                required: 5
                available: 2
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/submissions:
    get:
      operationId: listSubmissions
      tags: [Indexing]
      summary: List your team's submissions
      description: Returns your team's submissions, newest first (ULID ids sort by creation time).
      parameters:
        - name: status
          in: query
          required: false
          description: Filter by a single status.
          schema:
            $ref: "#/components/schemas/SubmissionStatus"
        - name: limit
          in: query
          required: false
          description: Max rows to return. Default 50, capped at 200.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: A list of submissions.
          content:
            application/json:
              schema:
                type: object
                required: [submissions]
                properties:
                  submissions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Submission"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /api/v1/me:
    get:
      operationId: getMe
      tags: [Account]
      summary: Authenticated team and credit balance
      description: Returns the team bound to the API key plus its current credit balance.
      responses:
        "200":
          description: The authenticated team.
          content:
            application/json:
              schema:
                type: object
                required: [team, keyId]
                properties:
                  team:
                    $ref: "#/components/schemas/Team"
                  keyId:
                    type: string
                    description: Id of the API key used for this request.
                    example: "key_2b9f1c"
              example:
                team: { id: "team_8a1b2c", name: "Acme", creditBalance: 480 }
                keyId: "key_2b9f1c"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: The team no longer exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example: { error: "not_found" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "ti_live_…"
      description: "A TopIndexer API key. Send as `Authorization: Bearer ti_live_…`."
  responses:
    Unauthorized:
      description: Missing, malformed, invalid, or revoked API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example: { error: "unauthorized", message: "Provide a Bearer API key." }
    InternalError:
      description: Something failed on our side.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example: { error: "internal", message: "Something failed on our side." }
  schemas:
    SubmitRequest:
      type: object
      required: [urls]
      properties:
        urls:
          type: array
          description: 1–50 http(s) URLs to submit for indexing.
          minItems: 1
          maxItems: 50
          items:
            type: string
            format: uri
            example: "https://example.com/page-a"
    SubmitResponse:
      type: object
      required: [submitted, ignoredInvalid, submissions]
      properties:
        submitted:
          type: integer
          description: Count of accepted URLs (credits deducted).
        ignoredInvalid:
          type: integer
          description: Count of dropped non-http(s) URLs.
        submissions:
          type: array
          items:
            type: object
            required: [id, url, status]
            properties:
              id:
                type: string
                description: ULID submission id.
              url:
                type: string
                format: uri
              status:
                $ref: "#/components/schemas/SubmissionStatus"
    Submission:
      type: object
      description: A full submission record.
      required: [id, teamId, url, normalizedUrl, status, attempts, createdAt]
      properties:
        id:
          type: string
          description: ULID submission id.
        teamId:
          type: string
        projectId:
          type: [string, "null"]
        url:
          type: string
          format: uri
        normalizedUrl:
          type: string
          format: uri
        status:
          $ref: "#/components/schemas/SubmissionStatus"
        providerRef:
          type: [string, "null"]
          description: Third-party submission id from the indexing provider.
        submittedAt:
          type: [string, "null"]
          format: date-time
        botHitAt:
          type: [string, "null"]
          format: date-time
          description: When Googlebot was first observed hitting the URL.
        indexedAt:
          type: [string, "null"]
          format: date-time
        lastCheckedAt:
          type: [string, "null"]
          format: date-time
        attempts:
          type: integer
        createdAt:
          type: string
          format: date-time
    SubmissionStatus:
      type: string
      description: |
        Lifecycle: `queued → processing → submitted → crawled → indexed`.
        Terminal alternatives: `not_indexed` (auto-refunded after 7 days),
        `failed` (provider rejected the URL), `refunded`.
      enum:
        - queued
        - processing
        - submitted
        - crawled
        - indexed
        - not_indexed
        - failed
        - refunded
    Team:
      type: object
      required: [id, name, creditBalance]
      properties:
        id:
          type: string
        name:
          type: string
        creditBalance:
          type: integer
          description: Current credit balance.
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable explanation.
    InsufficientCreditsError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            required:
              type: integer
              description: Credits needed for the batch.
            available:
              type: integer
              description: Credits currently available.
