openapi: 3.1.0
info:
  title: GeoVerdict API
  version: 1.0.0
  summary: Address validation and autocomplete through one API.
  description: |
    GeoVerdict validates and autocompletes postal addresses by orchestrating multiple
    geocoding providers behind one normalized confidence score (0-100) and verdict
    (`valid` / `correctable` / `invalid`).

    ## Authentication

    Server-side endpoints (`/v1/validate`, `/v1/autocomplete`, `/v1/usage`) authenticate
    with a secret API key sent as a bearer token:

    ```
    Authorization: Bearer ak_live_...
    ```

    Keys are created in the [GeoVerdict console](https://geoverdict.com/dashboard) and
    shown once at creation. Keys exist in `ak_live_` and `ak_test_` variants. Test keys
    call the same real providers and bill from the same credit balance; they exist to keep
    development traffic separable and carry a best-effort 60-request/minute limit.

    The browser widget endpoint (`/v1/widget/autocomplete`) instead uses a publishable
    token (`gv_pk_live_...` / `gv_pk_test_...`) restricted to an exact HTTPS origin
    allowlist. It never accepts secret API keys, and secret keys must never ship in
    browser code.

    ## Credits

    Every uncached lookup costs 1 credit per provider that answered. A request where no
    provider answers costs 0, as does a cache hit. `GET /v1/usage` and `GET /healthz`
    consume no credits.
    Autocomplete is billed the same way, per call, per answering provider.
    When the monthly plan quota plus purchased extra credits is exhausted the API
    returns `429`.

    A `valid` verdict means the checked address components matched provider data at
    the configured confidence threshold. It is not a carrier guarantee of
    deliverability.
  contact:
    name: GeoVerdict
    url: https://geoverdict.com
servers:
  - url: https://geoverdict.com
    description: Production
tags:
  - name: Validation
    description: Address validation with a normalized verdict and confidence score.
  - name: Autocomplete
    description: Address suggestions while the user types.
  - name: Usage
    description: Credit and usage reporting for the authenticated account.
  - name: Status
    description: Service health.
  - name: Demo
    description: Unauthenticated, rate-limited demo used by the geoverdict.com homepage.
paths:
  /healthz:
    get:
      operationId: getHealth
      tags: [Status]
      summary: Service health
      description: Returns service status and the ids of the currently active geocoding providers. Free, unauthenticated.
      security: []
      responses:
        '200':
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                required: [status, providers]
                properties:
                  status:
                    type: string
                    const: ok
                  providers:
                    type: array
                    description: Ids of globally active providers (subset of `bag`, `opencage`, `photon`, `nominatim`).
                    items:
                      type: string
              example:
                status: ok
                providers: [bag]
  /v1/validate:
    post:
      operationId: validateAddress
      tags: [Validation]
      summary: Validate an address
      description: |
        Validates a single address and returns a verdict, a 0-100 confidence score, the
        standardized address, and per-component comparison results. Provide either a
        free-form `query` or structured `components` (at least one is required).

        Costs 1 credit per provider that answered; a cache hit or a request where no
        provider answers costs 0.
      security:
        - ApiKey: []
      parameters:
        - name: source
          in: query
          required: false
          description: Fallback for the `source` body field; the body value wins when both are set.
          schema:
            type: string
            maxLength: 64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
            examples:
              freeForm:
                summary: Free-form query
                value:
                  query: 'Prinsengracht 263, Amsterdam'
                  country: NL
              structured:
                summary: Structured components
                value:
                  components:
                    street: Prinsengracht
                    houseNumber: '263'
                    postcode: 1016 GV
                    city: Amsterdam
                    country: NL
              debug:
                summary: With provider trail
                value:
                  query: 'Prinsengracht 263, Amsterdam'
                  country: NL
                  debug: true
      responses:
        '200':
          description: 'Validation result. `candidates` and `trace` are present only when the request set `"debug": true`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
              examples:
                correctable:
                  summary: Typo fixed by the engine
                  value:
                    verdict: correctable
                    confidence: 84
                    address:
                      street: Prinsengracht
                      houseNumber: '263'
                      postcode: 1016 GV
                      city: Amsterdam
                      country: NL
                      lat: 52.3752
                      lng: 4.8836
                    components:
                      street: corrected
                      houseNumber: match
                      city: corrected
                      country: match
                    provider: bag
                    reasons: [components_corrected]
                    cached: false
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/CreditsExhausted'
  /v1/autocomplete:
    post:
      operationId: autocompleteAddress
      tags: [Autocomplete]
      summary: Autocomplete an address
      description: |
        Returns up to `limit` (default 5) address suggestions for a partial query.
        Suggestions are deduplicated across providers.

        Each uncached call costs 1 credit per provider that answered. A cache hit or a
        request where no provider answers costs 0. `sessionToken` is echoed back for
        client-side correlation only; it does not deduplicate billing.

        For browser-side autocomplete use the embeddable widget with a publishable
        token (`/v1/widget/autocomplete`) instead of exposing a secret key.
      security:
        - ApiKey: []
      parameters:
        - name: source
          in: query
          required: false
          description: Fallback for the `source` body field; the body value wins when both are set.
          schema:
            type: string
            maxLength: 64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutocompleteRequest'
            example:
              query: Prinsengracht 26
              country: NL
              limit: 5
      responses:
        '200':
          description: Suggestions ordered by provider result order, deduplicated by label.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutocompleteResponse'
              example:
                suggestions:
                  - label: Prinsengracht 263, 1016GV Amsterdam
                    components:
                      street: Prinsengracht
                      houseNumber: '263'
                      postcode: 1016 GV
                      city: Amsterdam
                      country: NL
                    location:
                      lat: 52.3752
                      lng: 4.8836
                    provider: bag
                sessionToken: null
                cached: false
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/CreditsExhausted'
  /v1/widget/autocomplete:
    post:
      operationId: widgetAutocomplete
      tags: [Autocomplete]
      summary: Browser widget autocomplete
      description: |
        Browser-safe autocomplete for the embeddable GeoVerdict widget. Authenticates
        with a project-scoped publishable token (`gv_pk_live_...` / `gv_pk_test_...`)
        and requires a request `Origin` on the token's exact HTTPS origin allowlist.
        Secret API keys (`ak_...`) and console sessions are rejected.

        Suggestions are mapped to a browser-safe shape with opaque ids; provider-native
        identifiers and raw metadata are never returned. Per-token and per-origin rate
        limits of 120 requests/minute apply in addition to the account credit gate.
        Billing matches server autocomplete: 1 credit per answering provider per
        uncached call.

        Most integrations should not call this endpoint directly; the hosted widget
        (`https://geoverdict.com/widget/v1/geoverdict-autocomplete.js`) implements the
        contract, keyboard navigation, and accessibility.
      security:
        - PublishableToken: []
      parameters:
        - name: Origin
          in: header
          required: true
          description: Exact HTTPS origin of the embedding page. Must be on the publishable token's origin allowlist.
          schema:
            type: string
            example: https://shop.example
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutocompleteRequest'
            example:
              query: Prinsengracht 26
              country: NL
              limit: 5
              sessionToken: 3f5a1c9e-session
              source: widget
      responses:
        '200':
          description: Browser-safe suggestions. `Access-Control-Allow-Origin` echoes the validated origin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetAutocompleteResponse'
              example:
                suggestions:
                  - id: gv_s_1f7c0a92be34d56e78a90c12
                    label: Prinsengracht 263, 1016GV Amsterdam
                    provider: bag
                    address:
                      street: Prinsengracht
                      houseNumber: '263'
                      postcode: 1016 GV
                      city: Amsterdam
                      country: NL
                sessionToken: 3f5a1c9e-session
                cached: false
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          description: Missing, malformed, revoked, or origin-restricted publishable token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Unknown, revoked, or origin-restricted publishable token
        '403':
          description: The request carried no `Origin` header or the origin is not an exact HTTPS origin.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: A permitted HTTPS Origin is required
        '429':
          description: Widget rate limit reached (120 requests/minute per live token and per origin; 60/minute for test tokens) or the applicable live/test credit allowance is exhausted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimit:
                  value:
                    message: Widget autocomplete rate limit reached
                credits:
                  value:
                    message: Monthly project credits exhausted
    options:
      operationId: widgetAutocompletePreflight
      tags: [Autocomplete]
      summary: Widget CORS preflight
      description: |
        CORS preflight for the widget endpoint. Because browsers omit the
        `Authorization` value during preflight, the server reflects an origin only when
        an active publishable token currently allows it; the subsequent `POST` still
        binds the exact token and origin.
      security: []
      parameters:
        - name: Origin
          in: header
          required: true
          schema:
            type: string
        - name: Access-Control-Request-Method
          in: header
          required: true
          schema:
            type: string
            const: POST
      responses:
        '204':
          description: Origin currently allowed by at least one active token; CORS headers set.
        '403':
          description: Origin missing, not HTTPS, or not on any active token's allowlist.
  /v1/usage:
    get:
      operationId: getUsage
      tags: [Usage]
      summary: Usage and credit balance
      description: |
        Current-month credit consumption plus recent activity for the account that owns
        the API key. This call consumes no credits and never writes a usage event, but
        the shared API-key quota gate still returns `429` after the account exhausts its
        credits. Displays can lag the ledger by up to 60 seconds.
      security:
        - ApiKey: []
      parameters:
        - name: source
          in: query
          required: false
          description: Filter `summary`, `daily`, and `recent` to a single `source` tag. `sources` always covers all tags.
          schema:
            type: string
      responses:
        '200':
          description: Usage report.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              example:
                plan: free
                quota: 500
                extraCredits: 0
                creditsUsedThisMonth: 42
                filteredBySource: null
                daily:
                  - day: '2026-08-02'
                    n: 12
                    credits: 10
                summary:
                  - endpoint: validate
                    verdict: valid
                    n: 30
                    credits: 28
                sources:
                  - source: checkout
                    n: 25
                    credits: 22
                  - source: ''
                    n: 17
                    credits: 20
                recent:
                  - endpoint: validate
                    verdict: valid
                    provider: bag
                    latency_ms: 132
                    cached: 0
                    credits: 1
                    query: Prinsengracht 263, Amsterdam
                    source: checkout
                    at: '2026-08-02 14:03:11'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/CreditsExhausted'
  /demo/validate:
    post:
      operationId: demoValidate
      tags: [Demo]
      summary: Homepage demo validation
      description: |
        Unauthenticated validation used by the geoverdict.com homepage demo. Forced to
        the Netherlands (`country: NL`), costs 0 credits, and is rate limited per
        visitor IP over rolling windows: 10 requests / 5 minutes, 20 / 24 hours,
        50 / 7 days, and 200 / 30 days. Not intended for production integrations; use
        `/v1/validate` with an API key instead.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  minLength: 1
                  maxLength: 200
            example:
              query: prinsengrach 263, amsterdm
      responses:
        '200':
          description: Validation result (same core fields as `/v1/validate`, without `cached`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DemoValidateResponse'
        '400':
          description: Missing or invalid query.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: Type an address first
        '429':
          description: One or more rolling demo allowances are exhausted. The `Retry-After` header carries the longest required wait in seconds.
          headers:
            Retry-After:
              description: Seconds until the longest exhausted window frees up.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DemoRateLimitError'
              example:
                message: Demo request allowance reached.
                limits:
                  - window: five_minute
                    max: 10
                    periodSeconds: 300
                    retryAfterSeconds: 233
                retryAfterSeconds: 233
                freeMonthlyCredits: 500
components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: ak_live_... or ak_test_...
      description: |
        Secret server-side API key, created in the GeoVerdict console. Format
        `ak_(live|test)_<base64url>`. Send as `Authorization: Bearer ak_live_...`.
        `ak_test_` keys call real providers with a separate 5,000-credit daily allowance
        and best-effort 60-request/minute limit; they do not consume monthly credits.
    PublishableToken:
      type: http
      scheme: bearer
      bearerFormat: gv_pk_live_... or gv_pk_test_...
      description: |
        Project-scoped publishable widget token with an exact HTTPS origin allowlist.
        Only valid on `/v1/widget/autocomplete`. Safe to expose in browser code;
        cannot call any other endpoint.
  responses:
    Unauthorized:
      description: Missing, malformed, unknown, or revoked API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                message: 'Missing API key. Send it as: Authorization: Bearer ak_live_…'
            malformed:
              value:
                message: Malformed API key
            unknown:
              value:
                message: Unknown or revoked API key
    ValidationFailed:
      description: The request body failed schema validation. `issues` lists each violation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
          example:
            message: Validation failed
            issues:
              - code: too_small
                minimum: 1
                path: [query]
                message: Too small, expected string to have >=1 characters
    CreditsExhausted:
      description: The account's monthly plan credits plus purchased extra credits are used up. Resolve by upgrading the plan or buying a credit pack in the console; the counter resets on the 1st of each month (UTC).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: Monthly credits exhausted (500 plan credits on the free plan). Upgrade or buy credits at https://geoverdict.com/dashboard
  schemas:
    Error:
      type: object
      required: [message]
      properties:
        message:
          type: string
    ValidationError:
      type: object
      required: [message, issues]
      properties:
        message:
          type: string
          const: Validation failed
        issues:
          type: array
          description: Machine-readable list of schema violations (Zod issue objects).
          items:
            type: object
            required: [code, path, message]
            properties:
              code:
                type: string
              path:
                type: array
                items:
                  type: [string, number]
              message:
                type: string
            additionalProperties: true
    AddressComponents:
      type: object
      description: Structured address parts. All fields optional.
      properties:
        street:
          type: string
          minLength: 1
          maxLength: 200
        houseNumber:
          type: string
          minLength: 1
          maxLength: 20
          description: House number including any suffix, e.g. `263`, `2B`, `2/B`.
        postcode:
          type: string
          minLength: 1
          maxLength: 20
        city:
          type: string
          minLength: 1
          maxLength: 100
        state:
          type: string
          minLength: 1
          maxLength: 100
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 code, e.g. `NL`.
    ValidateRequest:
      type: object
      description: Provide `query`, `components`, or both. At least one is required.
      properties:
        query:
          type: string
          minLength: 1
          maxLength: 500
          description: Free-form address text.
        components:
          $ref: '#/components/schemas/AddressComponents'
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 country bias/filter. Providers that do not cover this country are skipped.
        language:
          type: string
          minLength: 2
          maxLength: 8
          description: Preferred response language (e.g. `en`, `nl-NL`).
        source:
          type: string
          minLength: 1
          maxLength: 64
          description: Free-form tag for segmenting usage per feature or system (e.g. `checkout`). Appears in `/v1/usage` and the console.
        debug:
          type: boolean
          description: When true, the response additionally includes `candidates` (all scored provider results) and `trace` (the provider routing trail).
    Verdict:
      type: string
      enum: [valid, correctable, invalid]
      description: |
        - `valid`: confidence at or above the account's valid threshold and every checked component matched.
        - `correctable`: the address was found but something was fixed (the response carries the standardized address).
        - `invalid`: below the correctable threshold, or a supplied house number could not be matched.
    Reason:
      type: string
      enum: [no_results, low_confidence, no_house_number_match, components_incomplete, components_corrected]
    ComponentVerdict:
      type: string
      enum: [match, corrected, mismatch, missing, not-checked]
    ComponentVerdicts:
      type: object
      description: Per-component comparison of the best result against the input. Keys are only present for checked components.
      properties:
        street:
          $ref: '#/components/schemas/ComponentVerdict'
        houseNumber:
          $ref: '#/components/schemas/ComponentVerdict'
        postcode:
          $ref: '#/components/schemas/ComponentVerdict'
        city:
          $ref: '#/components/schemas/ComponentVerdict'
        country:
          $ref: '#/components/schemas/ComponentVerdict'
    ResolvedAddress:
      description: The standardized address of the best result, or `null` when no provider returned anything.
      type: [object, 'null']
      properties:
        street:
          type: string
        houseNumber:
          type: string
        postcode:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2.
        lat:
          type: number
        lng:
          type: number
    GeoPoint:
      type: object
      required: [lat, lng]
      properties:
        lat:
          type: number
        lng:
          type: number
    TraceStep:
      type: object
      description: One provider attempt in the routing trail.
      required: [provider, status]
      properties:
        provider:
          type: string
        status:
          type: string
          enum: [ok, error, timeout, aborted, skipped, circuit-open, no-coverage, unsupported, unknown-provider]
        latencyMs:
          type: integer
        resultCount:
          type: integer
        error:
          type: string
    Candidate:
      type: object
      description: A scored provider result (debug only).
      required: [provider, components, confidence, componentVerdicts]
      properties:
        provider:
          type: string
        components:
          $ref: '#/components/schemas/AddressComponents'
        location:
          $ref: '#/components/schemas/GeoPoint'
        providerConfidence:
          type: number
          minimum: 0
          maximum: 1
          description: Provider-native confidence normalized to 0..1; absent when the provider reports none.
        label:
          type: string
        raw:
          description: Provider-native payload; shape varies per provider.
        confidence:
          type: integer
          minimum: 0
          maximum: 100
        componentVerdicts:
          $ref: '#/components/schemas/ComponentVerdicts'
    ValidateResponse:
      type: object
      required: [verdict, confidence, address, components, provider, reasons, cached]
      properties:
        verdict:
          $ref: '#/components/schemas/Verdict'
        confidence:
          type: integer
          minimum: 0
          maximum: 100
          description: Unified confidence score, comparable across providers. 0 when no result was found.
        address:
          $ref: '#/components/schemas/ResolvedAddress'
        components:
          $ref: '#/components/schemas/ComponentVerdicts'
        provider:
          type: [string, 'null']
          description: Id of the provider that produced the best result, or `null` when there was none.
        reasons:
          type: array
          items:
            $ref: '#/components/schemas/Reason'
        cached:
          type: boolean
          description: True when served from the response cache. Cached responses cost 0 credits.
        candidates:
          type: array
          description: 'Only present when the request set `"debug": true`.'
          items:
            $ref: '#/components/schemas/Candidate'
        trace:
          type: array
          description: 'Only present when the request set `"debug": true`.'
          items:
            $ref: '#/components/schemas/TraceStep'
    AutocompleteRequest:
      type: object
      required: [query]
      properties:
        query:
          type: string
          minLength: 1
          maxLength: 200
          description: Partial address text.
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 country bias/filter.
        language:
          type: string
          minLength: 2
          maxLength: 8
        limit:
          type: integer
          minimum: 1
          maximum: 10
          default: 5
          description: Maximum number of suggestions.
        sessionToken:
          type: string
          maxLength: 64
          description: Client-generated correlation id, echoed back unchanged. Correlation only; it does not deduplicate billing.
        source:
          type: string
          minLength: 1
          maxLength: 64
          description: Usage segmentation tag, as on `/v1/validate`.
    Suggestion:
      type: object
      required: [label, components, provider]
      properties:
        label:
          type: string
          description: Formatted display label.
        components:
          $ref: '#/components/schemas/AddressComponents'
        location:
          $ref: '#/components/schemas/GeoPoint'
        provider:
          type: string
    AutocompleteResponse:
      type: object
      required: [suggestions, sessionToken, cached]
      properties:
        suggestions:
          type: array
          items:
            $ref: '#/components/schemas/Suggestion'
        sessionToken:
          type: [string, 'null']
          description: The request's `sessionToken`, echoed back; `null` when none was sent.
        cached:
          type: boolean
    WidgetSuggestion:
      type: object
      required: [id, label, provider, address]
      properties:
        id:
          type: string
          description: Opaque suggestion id scoped to the publishable token (prefix `gv_s_`).
        label:
          type: string
        provider:
          type: string
        address:
          $ref: '#/components/schemas/AddressComponents'
    WidgetAutocompleteResponse:
      type: object
      required: [suggestions, sessionToken, cached]
      properties:
        suggestions:
          type: array
          items:
            $ref: '#/components/schemas/WidgetSuggestion'
        sessionToken:
          type: [string, 'null']
        cached:
          type: boolean
    UsageResponse:
      type: object
      required: [plan, quota, extraCredits, creditsUsedThisMonth, filteredBySource, daily, summary, sources, recent]
      properties:
        plan:
          type: string
          description: Current plan id (`free`, `starter`, `growth`, `scale`).
        quota:
          type: integer
          description: Monthly plan credits.
        extraCredits:
          type: integer
          description: Purchased pack credits still attributed to the account. Consumed only after the monthly grant.
        creditsUsedThisMonth:
          type: integer
          description: Credits consumed this calendar month (UTC). May lag the ledger by up to 60 seconds.
        filteredBySource:
          type: [string, 'null']
          description: The `source` filter applied to `summary`, `daily`, and `recent`, or `null`.
        daily:
          type: array
          description: Daily request and credit counts for the last 14 days.
          items:
            type: object
            required: [day, n, credits]
            properties:
              day:
                type: string
                format: date
              n:
                type: integer
              credits:
                type: integer
        summary:
          type: array
          description: Current-month totals grouped by endpoint and verdict.
          items:
            type: object
            required: [endpoint, n, credits]
            properties:
              endpoint:
                type: string
              verdict:
                type: [string, 'null']
              n:
                type: integer
              credits:
                type: integer
        sources:
          type: array
          description: Current-month totals per `source` tag (untagged traffic groups under an empty string). Always account-wide, ignoring the `source` filter.
          items:
            type: object
            required: [source, n, credits]
            properties:
              source:
                type: string
              n:
                type: integer
              credits:
                type: integer
        recent:
          type: array
          description: The 50 most recent usage events.
          items:
            type: object
            required: [endpoint, latency_ms, cached, credits, at]
            properties:
              endpoint:
                type: string
              verdict:
                type: [string, 'null']
              provider:
                type: [string, 'null']
              latency_ms:
                type: integer
              cached:
                type: integer
                description: 1 when the response came from cache, else 0.
              credits:
                type: integer
              query:
                type: [string, 'null']
              source:
                type: [string, 'null']
              at:
                type: string
                description: UTC timestamp, `YYYY-MM-DD HH:MM:SS`.
    DemoValidateResponse:
      type: object
      required: [verdict, confidence, address, components, provider, reasons]
      properties:
        verdict:
          $ref: '#/components/schemas/Verdict'
        confidence:
          type: integer
          minimum: 0
          maximum: 100
        address:
          $ref: '#/components/schemas/ResolvedAddress'
        components:
          $ref: '#/components/schemas/ComponentVerdicts'
        provider:
          type: [string, 'null']
        reasons:
          type: array
          items:
            $ref: '#/components/schemas/Reason'
    DemoRateLimitError:
      type: object
      required: [message, limits, retryAfterSeconds, freeMonthlyCredits]
      properties:
        message:
          type: string
        limits:
          type: array
          description: Every rolling window that is currently exhausted.
          items:
            type: object
            required: [window, max, periodSeconds, retryAfterSeconds]
            properties:
              window:
                type: string
                enum: [five_minute, daily, weekly, monthly]
              max:
                type: integer
              periodSeconds:
                type: integer
              retryAfterSeconds:
                type: integer
        retryAfterSeconds:
          type: integer
          description: The longest wait among all exhausted windows.
        freeMonthlyCredits:
          type: integer
          description: Monthly credits included with a free account (sign-up removes the demo limits).
