{
  "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\ngeocoding providers behind one normalized confidence score (0-100) and verdict\n(`valid` / `correctable` / `invalid`).\n\n## Authentication\n\nServer-side endpoints (`/v1/validate`, `/v1/autocomplete`, `/v1/usage`) authenticate\nwith a secret API key sent as a bearer token:\n\n```\nAuthorization: Bearer ak_live_...\n```\n\nKeys are created in the [GeoVerdict console](https://geoverdict.com/dashboard) and\nshown once at creation. Keys exist in `ak_live_` and `ak_test_` variants. Test keys\ncall the same real providers and bill from the same credit balance; they exist to keep\ndevelopment traffic separable and carry a best-effort 60-request/minute limit.\n\nThe browser widget endpoint (`/v1/widget/autocomplete`) instead uses a publishable\ntoken (`gv_pk_live_...` / `gv_pk_test_...`) restricted to an exact HTTPS origin\nallowlist. It never accepts secret API keys, and secret keys must never ship in\nbrowser code.\n\n## Credits\n\nEvery uncached lookup costs 1 credit per provider that answered. A request where no\nprovider answers costs 0, as does a cache hit. `GET /v1/usage` and `GET /healthz`\nconsume no credits.\nAutocomplete is billed the same way, per call, per answering provider.\nWhen the monthly plan quota plus purchased extra credits is exhausted the API\nreturns `429`.\n\nA `valid` verdict means the checked address components matched provider data at\nthe configured confidence threshold. It is not a carrier guarantee of\ndeliverability.\n",
    "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\nstandardized address, and per-component comparison results. Provide either a\nfree-form `query` or structured `components` (at least one is required).\n\nCosts 1 credit per provider that answered; a cache hit or a request where no\nprovider answers costs 0.\n",
        "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.\nSuggestions are deduplicated across providers.\n\nEach uncached call costs 1 credit per provider that answered. A cache hit or a\nrequest where no provider answers costs 0. `sessionToken` is echoed back for\nclient-side correlation only; it does not deduplicate billing.\n\nFor browser-side autocomplete use the embeddable widget with a publishable\ntoken (`/v1/widget/autocomplete`) instead of exposing a secret key.\n",
        "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\nwith a project-scoped publishable token (`gv_pk_live_...` / `gv_pk_test_...`)\nand requires a request `Origin` on the token's exact HTTPS origin allowlist.\nSecret API keys (`ak_...`) and console sessions are rejected.\n\nSuggestions are mapped to a browser-safe shape with opaque ids; provider-native\nidentifiers and raw metadata are never returned. Per-token and per-origin rate\nlimits of 120 requests/minute apply in addition to the account credit gate.\nBilling matches server autocomplete: 1 credit per answering provider per\nuncached call.\n\nMost integrations should not call this endpoint directly; the hosted widget\n(`https://geoverdict.com/widget/v1/geoverdict-autocomplete.js`) implements the\ncontract, keyboard navigation, and accessibility.\n",
        "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\n`Authorization` value during preflight, the server reflects an origin only when\nan active publishable token currently allows it; the subsequent `POST` still\nbinds the exact token and origin.\n",
        "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\nthe API key. This call consumes no credits and never writes a usage event, but\nthe shared API-key quota gate still returns `429` after the account exhausts its\ncredits. Displays can lag the ledger by up to 60 seconds.\n",
        "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\nthe Netherlands (`country: NL`), costs 0 credits, and is rate limited per\nvisitor IP over rolling windows: 10 requests / 5 minutes, 20 / 24 hours,\n50 / 7 days, and 200 / 30 days. Not intended for production integrations; use\n`/v1/validate` with an API key instead.\n",
        "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\n`ak_(live|test)_<base64url>`. Send as `Authorization: Bearer ak_live_...`.\n`ak_test_` keys call real providers with a separate 5,000-credit daily allowance\nand best-effort 60-request/minute limit; they do not consume monthly credits.\n"
      },
      "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.\nOnly valid on `/v1/widget/autocomplete`. Safe to expose in browser code;\ncannot call any other endpoint.\n"
      }
    },
    "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.\n- `correctable`: the address was found but something was fixed (the response carries the standardized address).\n- `invalid`: below the correctable threshold, or a supplied house number could not be matched.\n"
      },
      "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)."
          }
        }
      }
    }
  }
}
