openapi: 3.1.0
info:
  title: domainsearch API
  version: "1.0"
  description: |
    Free domain-availability API. Availability is answered from layered public sources
    (ICANN CZDS zone files, Common Crawl, DNS delegation, registry RDAP)
    and never guessed: `free` is only asserted on an authoritative RDAP
    answer. Human docs: https://domainsearch24.eu/docs
  contact:
    email: liam@domainsearch24.eu
  termsOfService: https://domainsearch24.eu/legal/terms-of-service
servers:
  - url: https://domainsearch24.eu/api/v1
security:
  - {}
  - bearer: []
paths:
  /check:
    get:
      summary: Check up to 30 domains
      operationId: checkBatch
      parameters:
        - name: domains
          in: query
          required: true
          description: Comma-separated registrable domain names (punycode for IDNs). Extra domains beyond 30 are dropped.
          schema: { type: string, example: "example.com,example.io" }
        - $ref: "#/components/parameters/fast"
      responses:
        "200":
          description: One result per input domain, in input order.
          content:
            application/json:
              schema:
                type: object
                required: [results]
                properties:
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/Result" }
        "400": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "501": { $ref: "#/components/responses/Error" }
  /check/{domain}:
    get:
      summary: Check one domain
      operationId: checkOne
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string, example: "example.com" }
        - $ref: "#/components/parameters/fast"
      responses:
        "200":
          description: The availability result for the domain.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Result" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "501": { $ref: "#/components/responses/Error" }
  /check/stream:
    get:
      summary: Check up to 30 domains, streaming results as they resolve
      operationId: checkStream
      description: |
        NDJSON response (`application/x-ndjson`): one Result object per
        line, written as each domain completes. Line order is completion
        order, not input order: match on `domain`.
      parameters:
        - name: domains
          in: query
          required: true
          schema: { type: string, example: "a.com,b.io,c.dev" }
        - $ref: "#/components/parameters/fast"
      responses:
        "200":
          description: Newline-delimited Result objects.
          content:
            application/x-ndjson:
              schema: { $ref: "#/components/schemas/Result" }
        "400": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "501": { $ref: "#/components/responses/Error" }
  /bulk/stream:
    get:
      summary: Check one label across every delegated TLD, streaming
      operationId: bulkStream
      description: |
        NDJSON stream (~1,450 lines): a `{"label","total"}` header, one
        BulkLine per domain (phase `local` first; with confirm=1, `confirm`
        lines supersede the `local` line for the same domain), then a
        `{"done","confirm","queued","confirmed","truncated"}` trailer.
        Default is local layers only: a clean miss classes as `free`
        (assumed; strict `status` stays `unknown`). Rate limited per IP as
        whole searches (confirming searches cost more).
      parameters:
        - name: label
          in: query
          required: true
          description: A single DNS label (a-z, 0-9, hyphens; punycode for IDNs). A full domain is accepted and reduced to its first label.
          schema: { type: string, example: "acme" }
        - name: confirm
          in: query
          required: false
          description: "`1` adds the budgeted RDAP confirmation phase (slower, registry traffic)."
          schema: { type: string, enum: ["1"] }
        - name: tlds
          in: query
          required: false
          description: Comma-separated TLD subset to sweep (unknown TLDs ignored; default all delegated TLDs).
          schema: { type: string, example: "com,io,dev" }
      responses:
        "200":
          description: Newline-delimited BulkLine objects with meta header/trailer lines.
          content:
            application/x-ndjson:
              schema: { $ref: "#/components/schemas/BulkLine" }
        "400": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "501": { $ref: "#/components/responses/Error" }
  /tlds:
    get:
      summary: List delegated TLDs with capability flags
      operationId: listTLDs
      responses:
        "200":
          description: Every root-zone TLD with what this service can assert for it.
          content:
            application/json:
              schema:
                type: object
                required: [tlds]
                properties:
                  tlds:
                    type: array
                    items:
                      type: object
                      required: [tld, rdap, zone]
                      properties:
                        tld: { type: string, example: "com" }
                        rdap:
                          type: boolean
                          description: An RDAP authority exists, so `free` is assertable.
                        zone:
                          type: boolean
                          description: A zone-file index is loaded (`registered` complete to ~24h).
        "503": { $ref: "#/components/responses/Error" }
  /status:
    get:
      summary: Layer readiness / health check
      operationId: status
      responses:
        "200":
          description: Service health and per-layer readiness.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  uptime_s: { type: integer }
                  layers:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        enabled: { type: boolean }
                        ready: { type: boolean }
                        detail: { type: string }
components:
  parameters:
    fast:
      name: fast
      in: query
      required: false
      description: |
        `1` skips RDAP: millisecond answers from local layers only.
        Can assert `registered`, never `free`.
      schema: { type: string, enum: ["1"] }
  schemas:
    Result:
      type: object
      required: [domain, status]
      properties:
        domain:
          type: string
          description: Normalized domain the result is for.
        status:
          type: string
          enum: [registered, free, unknown]
        source:
          type: string
          enum: [czds, commoncrawl, dns, rdap]
          description: Which layer produced the answer. Absent for `unknown`.
        reason:
          type: string
          description: For `unknown`, why availability could not be determined.
        class:
          type: string
          enum: [taken, free, premium, aftermarket, unknown]
          description: |
            Product-level refinement of status: free also covers a clean
            local miss ("assumed free", status stays unknown); premium =
            registry status "reserved"; aftermarket = delegated to a known
            parking / for-sale marketplace nameserver (heuristic).
    BulkLine:
      allOf:
        - $ref: "#/components/schemas/Result"
        - type: object
          required: [phase]
          properties:
            phase:
              type: string
              enum: [local, confirm]
              description: A `confirm` line supersedes the `local` line for the same domain.
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
  responses:
    Error:
      description: Error envelope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Anonymous per-IP allowance exhausted; honor Retry-After.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until the request would be admitted.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: Optional API key; lifts the anonymous rate limit.
