openapi: 3.1.0
info:
  title: zipcodestack
  version: "1"
  summary: Worldwide postal / zip code lookup, distance, radius and geometry queries.
  description: >-
    zipcodestack resolves postal (zip) codes worldwide to location data and
    supports distance, radius, polygon, city/state and coverage queries. All
    endpoints are served over HTTPS and return JSON. Authenticate every request
    with your API key, either as the `apikey` query parameter or the `apikey`
    request header.


    `HEAD` answers 405 (`method_not_allowed`; `Allow` names the methods the
    endpoint takes) on every billed endpoint, without running or billing it.
    `/v1/status` and the other free endpoints answer it like `GET`.


    This document is generated from the zipcodestack service implementation and
    is the machine-readable source of truth for the v1 REST API. A human-readable
    version is available at https://zipcodestack.com/docs.


    Note: on all endpoints except `/v1/validate`, `/v1/autocomplete` and
    `/v1/reverse` (which return 422) a missing required parameter currently
    returns HTTP 200 with an error-shaped body (e.g. `{"code": "required",
    "info": ...}`) rather than a 4xx status. Postal codes that `/v1/match` and
    `/v1/radius` cannot find return 404.
  contact:
    name: zipcodestack support
    email: support@zipcodestack.com
servers:
  - url: https://api.zipcodestack.com
    description: Production
security:
  - ApiKeyQuery: []
  - ApiKeyHeader: []
tags:
  - name: Lookup
    description: Postal code lookups and geometry queries.
  - name: Reference
    description: Provinces and coverage.
  - name: Metadata
    description: Account status.
paths:
  /v1/search:
    get:
      tags: [Lookup]
      summary: Look up postal codes
      operationId: search
      description: Returns location data for one or more postal codes (up to 100).
      parameters:
        - name: codes
          in: query
          required: true
          description: Comma-separated list of postal codes (max 100).
          schema: { type: string }
          example: "99501,90210"
        - name: country
          in: query
          required: false
          description: Two-letter ISO country code to disambiguate the codes.
          schema: { type: string }
          example: "us"
      responses:
        '200':
          description: Postal code results keyed by the requested code.
          headers:
            X-RateLimit-Remaining-Quota-Month: { $ref: '#/components/headers/RateLimitRemainingQuotaMonth' }
            X-Cost: { $ref: '#/components/headers/Cost' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: object
                    properties:
                      codes: { type: array, items: { type: string } }
                      country: { type: [string, "null"] }
                  results:
                    type: object
                    additionalProperties:
                      type: array
                      items: { $ref: '#/components/schemas/PostalCode' }
                required: [query, results]
              example:
                query: { codes: ["99501"], country: "us" }
                results:
                  "99501":
                    - postal_code: "99501"
                      country_code: "US"
                      latitude: 61.2181
                      longitude: -149.9003
                      city: "Anchorage"
                      state: "Alaska"
                      city_en: "Anchorage"
                      state_en: "Alaska"
                      state_code: "AK"
                      province: "Anchorage"
                      province_code: "020"
                      community: null
                      community_code: null
                      accuracy: 4
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/distance:
    get:
      tags: [Lookup]
      summary: Distance from one code to others
      operationId: distance
      description: >-
        Calculates the distance between one postal code and a list of others
        (up to 100).
      parameters:
        - { name: code, in: query, required: true, description: "The origin postal code.", schema: { type: string }, example: "99501" }
        - { name: compare, in: query, required: true, description: "Comma-separated postal codes to compare against (max 100).", schema: { type: string }, example: "90210,15106" }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: unit, in: query, required: false, description: "Distance unit (`mi` is accepted for `miles`).", schema: { type: string, enum: [km, miles, mi], default: km } }
      responses:
        '200':
          description: Distances keyed by the compared code (in the requested unit).
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: object
                    additionalProperties: { type: number }
                required: [query, results]
              example:
                query: { code: "99501", compare: ["90210", "15106"], country: "us", unit: "km" }
                results: { "90210": 4368.2, "15106": 5389.7 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`code` sent as a list (`code[]=`): a validation error (`message`, the reason under `errors.code`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/radius:
    get:
      tags: [Lookup]
      summary: Codes within a radius
      operationId: radius
      description: >-
        Returns the postal codes within a given radius of a postal code,
        closest first. The radius must be 500 or less (in the requested unit).
        The results come in pages of up to 10,000 codes (`page`, `limit`);
        `total` counts the codes on all pages and `has_more` says whether
        another page follows. Each page costs 10 requests.
      parameters:
        - { name: code, in: query, required: true, description: "The center postal code.", schema: { type: string }, example: "99501" }
        - { name: radius, in: query, required: true, description: "Radius (must be <= 500).", schema: { type: integer, maximum: 500 }, example: 50 }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: unit, in: query, required: false, description: "Distance unit (`mi` is accepted for `miles`).", schema: { type: string, enum: [km, miles, mi], default: km } }
        - { name: page, in: query, required: false, description: "Page of the results, from 1. `has_more` says whether another page follows.", schema: { type: integer, minimum: 1, default: 1 }, example: 1 }
        - { name: limit, in: query, required: false, description: "Codes per page, at most 10000 (a larger limit reads as 10000, and one that is no whole number from 1 as the default).", schema: { type: integer, minimum: 1, maximum: 10000, default: 10000 }, example: 100 }
      responses:
        '200':
          description: Surrounding postal codes with their distance from the center, closest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/RadiusCode' }
                  total: { $ref: '#/components/schemas/PagingTotal' }
                  page: { $ref: '#/components/schemas/PagingPage' }
                  limit: { $ref: '#/components/schemas/PagingLimit' }
                  has_more: { $ref: '#/components/schemas/PagingHasMore' }
                required: [query, results, total, page, limit, has_more]
              example:
                query: { code: "99501", unit: "km", radius: 50, country: "us" }
                results:
                  - { code: "99502", city: "Anchorage", state: "AK", city_en: "Anchorage", state_en: "Alaska", distance: 5.23 }
                total: 1
                page: 1
                limit: 10000
                has_more: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: "`radius` is no number, `page` is no whole number from 1, or `code`, `country` or `unit` was sent as a list (`code[]=`): a validation error (`message`, the reason under `errors.<parameter>`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/polygon:
    post:
      tags: [Lookup]
      summary: Codes inside a polygon
      operationId: polygon
      description: >-
        Returns all postal codes inside a polygon. The polygon is supplied as a
        JSON body of coordinate pairs `[longitude, latitude]` (at least 2), and
        the `country` is a query parameter. The codes closest to the
        polygon's centroid come first, in pages of up to 10,000 codes (`page`,
        `limit`); `total` counts the codes on all pages and `has_more` says
        whether another page follows. Each page costs 10 requests.
      parameters:
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: page, in: query, required: false, description: "Page of the results, from 1. `has_more` says whether another page follows.", schema: { type: integer, minimum: 1, default: 1 }, example: 1 }
        - { name: limit, in: query, required: false, description: "Codes per page, at most 10000 (a larger limit reads as 10000, and one that is no whole number from 1 as the default).", schema: { type: integer, minimum: 1, maximum: 10000, default: 10000 }, example: 100 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                coordinates:
                  type: array
                  minItems: 2
                  description: Array of `[longitude, latitude]` pairs describing the polygon.
                  items:
                    type: array
                    items: { type: number }
                    minItems: 2
                    maxItems: 2
              required: [coordinates]
            example:
              coordinates:
                - [-122.4, 37.8]
                - [-122.5, 37.9]
                - [-122.3, 37.9]
      responses:
        '200':
          description: Postal codes located inside the polygon.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/PolygonCode' }
                  total: { $ref: '#/components/schemas/PagingTotal' }
                  page: { $ref: '#/components/schemas/PagingPage' }
                  limit: { $ref: '#/components/schemas/PagingLimit' }
                  has_more: { $ref: '#/components/schemas/PagingHasMore' }
                required: [query, results, total, page, limit, has_more]
              example:
                query: { country: "us" }
                results:
                  - { code: "99501", city: "Anchorage", state: "AK", city_en: "Anchorage", state_en: "Alaska", latitude: 61.2181, longitude: -149.9003, distance: 0 }
                total: 1
                page: 1
                limit: 10000
                has_more: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`coordinates` is no array, a vertex is no `[longitude, latitude]` pair within ±180 and ±90, or `page` is no whole number from 1: a validation error (`message`, the reason under `errors.<parameter>`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/match:
    get:
      tags: [Lookup]
      summary: Match code pairs within a distance
      operationId: match
      description: >-
        Returns the pairs of the supplied postal codes (up to 100) whose
        distance is within the given maximum.
      parameters:
        - { name: codes, in: query, required: true, description: "Comma-separated postal codes (max 100).", schema: { type: string }, example: "99501,90210,15106" }
        - { name: distance, in: query, required: true, description: "Maximum distance between codes.", schema: { type: integer }, example: 100 }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: unit, in: query, required: false, description: "Distance unit (`mi` is accepted for `miles`).", schema: { type: string, enum: [km, miles, mi], default: km } }
      responses:
        '200':
          description: Matching code pairs and their distance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        code_1: { type: string, example: "99501" }
                        code_2: { type: string, example: "99502" }
                        distance: { type: number, example: 5.23 }
                required: [query, results]
              example:
                query: { codes: ["99501", "90210"], distance: 100, unit: "km", country: "us" }
                results:
                  - { code_1: "99501", code_2: "99502", distance: 5.23 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { description: "`country` sent as a list (`country[]=`): a validation error (`message`, the reason under `errors.country`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/code/city:
    get:
      tags: [Lookup]
      summary: Codes for a city
      operationId: codesByCity
      description: >-
        Returns the postal codes for a city, optionally filtered by state, in
        pages of up to 10,000 codes (`page`, `limit`) in the order the codes
        were added; `total` counts the codes on all pages and `has_more` says
        whether another page follows. A page costs one request per 10 codes on it.
      parameters:
        - { name: city, in: query, required: true, description: "City name.", schema: { type: string }, example: "Anchorage" }
        - { name: state_name, in: query, required: false, description: "State name to disambiguate the city.", schema: { type: string }, example: "Alaska" }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: page, in: query, required: false, description: "Page of the results, from 1. `has_more` says whether another page follows.", schema: { type: integer, minimum: 1, default: 1 }, example: 1 }
        - { name: limit, in: query, required: false, description: "Codes per page, at most 10000 (a larger limit reads as 10000, and 0 or less, which was no limit before the paging, as a full page).", schema: { type: integer, maximum: 10000, default: 10000 }, example: 10 }
      responses:
        '200':
          description: Postal codes for the city.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PagedCodeListResponse' }
              example:
                query: { city: "Anchorage", state: "Alaska", country: "us" }
                results: ["99501", "99502", "99503"]
                total: 3
                page: 1
                limit: 10000
                has_more: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`page` is no whole number from 1: a validation error (`message`, the reason under `errors.page`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/code/state:
    get:
      tags: [Lookup]
      summary: Codes for a state
      operationId: codesByState
      description: >-
        Returns the postal codes for a state/province, in pages of up to
        10,000 codes (`page`, `limit`) in the order the codes were added;
        `total` counts the codes on all pages and `has_more` says whether
        another page follows. A page costs one request per 10 codes on it.
      parameters:
        - { name: state_name, in: query, required: true, description: "State/province name.", schema: { type: string }, example: "Alaska" }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: page, in: query, required: false, description: "Page of the results, from 1. `has_more` says whether another page follows.", schema: { type: integer, minimum: 1, default: 1 }, example: 1 }
        - { name: limit, in: query, required: false, description: "Codes per page, at most 10000 (a larger limit reads as 10000, and 0 or less, which was no limit before the paging, as a full page).", schema: { type: integer, maximum: 10000, default: 10000 }, example: 10 }
      responses:
        '200':
          description: Postal codes for the state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PagedCodeListResponse' }
              example:
                query: { state: "Alaska", country: "us" }
                results: ["99501", "99502", "99503"]
                total: 3
                page: 1
                limit: 10000
                has_more: false
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`page` is no whole number from 1: a validation error (`message`, the reason under `errors.page`, `info`)." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/country/province:
    get:
      tags: [Reference]
      summary: Provinces for a country
      operationId: provinces
      description: Returns the list of provinces/states for a country.
      parameters:
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
      responses:
        '200':
          description: Provinces/states for the country.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CodeListResponse' }
              example:
                query: { country: "us" }
                results: ["Alaska", "California", "New York", "Texas"]
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/reverse:
    get:
      tags: [Lookup]
      summary: Postal codes nearest to a coordinate
      operationId: reverse
      description: >-
        Returns the postal codes closest to a latitude/longitude, nearest
        first, with their distance. Use it to turn a GPS position into a
        postal code. Searches within `radius` (default 10 km, at most 50 km
        or 31 miles).
      parameters:
        - { name: latitude, in: query, required: true, description: "Latitude (-90 to 90).", schema: { type: number }, example: 61.2175 }
        - { name: longitude, in: query, required: true, description: "Longitude (-180 to 180).", schema: { type: number }, example: -149.8997 }
        - { name: country, in: query, required: false, description: "Only codes of this two-letter ISO country.", schema: { type: string }, example: "us" }
        - { name: radius, in: query, required: false, description: "Search radius in `unit`. Default 10 km.", schema: { type: number } }
        - { name: unit, in: query, required: false, description: "Unit of `radius` and of the returned distances.", schema: { type: string, enum: [km, mi, miles], default: km } }
        - { name: limit, in: query, required: false, description: "Maximum number of codes (1-100).", schema: { type: integer, minimum: 1, maximum: 100, default: 10 } }
      responses:
        '200':
          description: The nearest codes, closest first. Empty when none lie within the radius.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/PostalCodeWithDistance' }
                required: [query, results]
              example:
                query: { latitude: 61.2175, longitude: -149.8997, country: "US", radius: 10, unit: "km", limit: 1 }
                results:
                  - distance: 0.07
                    postal_code: "99501"
                    country_code: "US"
                    latitude: 61.2181
                    longitude: -149.9003
                    city: "Anchorage"
                    state: "Alaska"
                    city_en: "Anchorage"
                    state_en: "Alaska"
                    state_code: "AK"
                    province: "Anchorage"
                    province_code: "020"
                    community: null
                    community_code: null
                    accuracy: 4
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "A parameter is missing or out of range, e.g. a radius above 50 km." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/validate:
    get:
      tags: [Lookup]
      summary: Validate a postal code
      operationId: validateCode
      description: >-
        Checks a postal code against a country. `valid` says whether the code
        exists in our data; `format_valid` whether it has the country's postal
        code format (null for countries without postal codes, e.g. HK), so a
        well-formed but unassigned code is `valid: false, format_valid: true`.
        `normalized` is the code as stored (upper case, canonical spacing).
      parameters:
        - { name: code, in: query, required: true, description: "The postal code to check.", schema: { type: string }, example: "99501" }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
      responses:
        '200':
          description: Validation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  result:
                    type: object
                    properties:
                      valid: { type: boolean }
                      format_valid: { type: [boolean, "null"] }
                      uses_postal_codes: { type: boolean }
                      normalized: { type: [string, "null"] }
                      matches:
                        type: array
                        items: { $ref: '#/components/schemas/PostalCode' }
                    required: [valid, format_valid, uses_postal_codes, normalized, matches]
                required: [query, result]
              example:
                query: { code: "99501", country: "US" }
                result:
                  valid: true
                  format_valid: true
                  uses_postal_codes: true
                  normalized: "99501"
                  matches:
                    - postal_code: "99501"
                      country_code: "US"
                      latitude: 61.2181
                      longitude: -149.9003
                      city: "Anchorage"
                      state: "Alaska"
                      city_en: "Anchorage"
                      state_en: "Alaska"
                      state_code: "AK"
                      province: "Anchorage"
                      province_code: "020"
                      community: null
                      community_code: null
                      accuracy: 4
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`code` or `country` is missing or malformed." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/autocomplete:
    get:
      tags: [Lookup]
      summary: Postal codes starting with a prefix
      operationId: autocomplete
      description: >-
        Returns distinct postal codes of a country that start with `q`, in
        order, each with its first place. Built for address form autocomplete.
      parameters:
        - { name: q, in: query, required: true, description: "The beginning of the postal code.", schema: { type: string }, example: "995" }
        - { name: country, in: query, required: true, description: "Two-letter ISO country code.", schema: { type: string }, example: "us" }
        - { name: limit, in: query, required: false, description: "Maximum number of codes (1-50).", schema: { type: integer, minimum: 1, maximum: 50, default: 10 } }
      responses:
        '200':
          description: Matching codes in order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: object, additionalProperties: true }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        postal_code: { type: string }
                        city: { type: [string, "null"] }
                        state: { type: [string, "null"] }
                        state_code: { type: [string, "null"] }
                required: [query, results]
              example:
                query: { q: "995", country: "US", limit: 2 }
                results:
                  - { postal_code: "99501", city: "Anchorage", state: "Alaska", state_code: "AK" }
                  - { postal_code: "99502", city: "Anchorage", state: "Alaska", state_code: "AK" }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { description: "`q` or `country` is missing, or `limit` is out of range." }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/coverage:
    get:
      tags: [Reference]
      summary: Coverage by country
      operationId: coverage
      description: >-
        Returns the number of postal codes available per country. Does not count
        against your quota.
      responses:
        '200':
          description: Postal code counts keyed by ISO country code.
          content:
            application/json:
              schema:
                type: object
                properties:
                  coverage:
                    type: object
                    additionalProperties: { type: integer }
                required: [coverage]
              example:
                coverage: { US: 41000, DE: 16000, AT: 3000 }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /v1/status:
    get:
      tags: [Metadata]
      summary: Account quota status
      operationId: getStatus
      description: >-
        Returns your current monthly and grace quota usage and the plan of your
        API key's account. Requests to this endpoint do not count against your
        quota or rate limit.
      responses:
        '200':
          description: Current quota usage for the authenticated account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusResponse' }
              example:
                account_id: 313373133731337
                account_id_str: "313373133731337"
                quotas:
                  month: { total: 300, used: 72, remaining: 228 }
                  grace: { total: 0, used: 0, remaining: 0 }
                plan:
                  name: "free"
                  interval: "month"
                  price_id: "price_1AbCdEfGhIjKlMnOpQrStUvW"
                  free: true
                  custom: false
                  status: "active"
                  renews_at: "2026-11-01T00:00:00+00:00"
                  scheduled: null
                  upgrade_url: "https://app.zipcodestack.com/upgrade?utm_source=agent&utm_campaign=status&team=313373133731337"
                  plans_url: "https://api.zipcodestack.com/v1/agent/plans"
        '401': { $ref: '#/components/responses/Unauthorized' }
components:
  securitySchemes:
    ApiKeyQuery: { type: apiKey, in: query, name: apikey, description: "API key as the apikey query parameter." }
    ApiKeyHeader: { type: apiKey, in: header, name: apikey, description: "API key as the apikey request header (recommended)." }
  headers:
    RateLimitRemainingQuotaMonth:
      description: Remaining requests in your monthly quota.
      schema: { type: integer }
      example: 228
    Cost:
      description: Requests this call counted against your quota.
      schema: { type: integer }
      example: 1
  responses:
    Unauthorized:
      description: >-
        The API key is missing or invalid: every request needs one, in the
        `apikey` header or query parameter. Without a key the message is "No
        API key found in request" (`error.code` `missing_api_key`), with an
        unknown key "Unauthorized" (`invalid_api_key`). `actions.sign_up`
        creates an account with a free API key. `actions` can also carry
        `agent_setup` (setup steps for AI coding agents), `pricing` (the plans
        as JSON) and `openapi`; `docs` then points to the docs' llms.txt
        (example `missing_api_key_agent_links`).
      headers:
        WWW-Authenticate:
          description: Always `Key`.
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            missing_api_key:
              summary: No API key sent
              value:
                message: "No API key found in request"
                error: { code: "missing_api_key", message: "No API key found in request" }
                actions:
                  sign_up: "https://app.zipcodestack.com/register?utm_source=api_error&utm_campaign=missing_api_key"
                  docs: "https://zipcodestack.com/docs/openapi.yaml"
            invalid_api_key:
              summary: Unknown API key
              value:
                message: "Unauthorized"
                error: { code: "invalid_api_key", message: "Unauthorized" }
                actions:
                  sign_up: "https://app.zipcodestack.com/register?utm_source=api_error&utm_campaign=invalid_api_key"
                  docs: "https://zipcodestack.com/docs/openapi.yaml"
            missing_api_key_agent_links:
              summary: No API key sent, with the links for AI coding agents
              value:
                message: "No API key found in request"
                error: { code: "missing_api_key", message: "No API key found in request" }
                actions:
                  agent_setup: "https://zipcodestack.com/agents.md"
                  sign_up: "https://app.zipcodestack.com/register?utm_source=api_error&utm_campaign=missing_api_key"
                  pricing: "https://api.zipcodestack.com/v1/agent/plans"
                  docs: "https://zipcodestack.com/docs/llms.txt"
                  openapi: "https://zipcodestack.com/docs/openapi.yaml"
    NotFound:
      description: >-
        A requested postal code could not be found. `/v1/match` returns the
        error under a `codes` key; `/v1/radius` returns it under an `error`
        key with "postalcode ... not found" wording.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            codes: "99999 could not be found in our database."
            info: "For more information, see documentation: https://zipcodestack.com/docs/status-codes#_404"
    RateLimited:
      description: >-
        A limit is used up. `error.code` says which: `quota_exceeded` (the
        month's requests; `quota.resets_at` says when they come back),
        `grace_quota_exceeded` (the grace quota of an unpaid invoice:
        `actions.complete_payment` pays it), `overage_quota_exceeded` (the
        overage budget) or `rate_limit_exceeded` (the free plan's 10 requests
        a minute). Quota errors carry `quota`. `actions.upgrade` opens the
        plans of the API key's account. `actions` can also carry `pricing`
        (the plans as JSON) and `openapi`; `docs` then points to the docs'
        llms.txt (the examples ending in `_agent_links`).
      headers:
        Retry-After:
          description: >-
            Seconds until the next full minute, with the requests-per-minute
            `rate_limit_exceeded` of an API key. A used-up quota has none: it
            lasts until `quota.resets_at`.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            quota_exceeded:
              summary: The month's requests are used up
              value:
                message: "You used all your monthly requests. Please upgrade your plan at https://app.zipcodestack.com/upgrade"
                error: { code: "quota_exceeded", message: "You used all your monthly requests. Please upgrade your plan at https://app.zipcodestack.com/upgrade" }
                quota: { limit: 300, used: 300, remaining: 0, resets_at: "2026-11-01T00:00:00+00:00" }
                actions:
                  upgrade: "https://app.zipcodestack.com/upgrade?utm_source=api_error&utm_campaign=quota_exceeded&team=7123456789012345678"
                  docs: "https://zipcodestack.com/docs/openapi.yaml"
            rate_limit_exceeded:
              summary: The requests per minute are used up
              value:
                message: "API rate limit exceeded"
                error: { code: "rate_limit_exceeded", message: "API rate limit exceeded" }
                actions:
                  upgrade: "https://app.zipcodestack.com/upgrade?utm_source=api_error&utm_campaign=rate_limit_exceeded&team=7123456789012345678"
                  docs: "https://zipcodestack.com/docs/openapi.yaml"
            quota_exceeded_agent_links:
              summary: The month's requests are used up, with the links for AI coding agents
              value:
                message: "You used all your monthly requests. Please upgrade your plan at https://app.zipcodestack.com/upgrade"
                error: { code: "quota_exceeded", message: "You used all your monthly requests. Please upgrade your plan at https://app.zipcodestack.com/upgrade" }
                quota: { limit: 300, used: 300, remaining: 0, resets_at: "2026-11-01T00:00:00+00:00" }
                actions:
                  upgrade: "https://app.zipcodestack.com/upgrade?utm_source=api_error&utm_campaign=quota_exceeded&team=7123456789012345678"
                  pricing: "https://api.zipcodestack.com/v1/agent/plans"
                  docs: "https://zipcodestack.com/docs/llms.txt"
                  openapi: "https://zipcodestack.com/docs/openapi.yaml"
            rate_limit_exceeded_agent_links:
              summary: The requests per minute are used up, with the links for AI coding agents
              value:
                message: "API rate limit exceeded"
                error: { code: "rate_limit_exceeded", message: "API rate limit exceeded" }
                actions:
                  upgrade: "https://app.zipcodestack.com/upgrade?utm_source=api_error&utm_campaign=rate_limit_exceeded&team=7123456789012345678"
                  pricing: "https://api.zipcodestack.com/v1/agent/plans"
                  docs: "https://zipcodestack.com/docs/llms.txt"
                  openapi: "https://zipcodestack.com/docs/openapi.yaml"
  schemas:
    PostalCode:
      type: object
      properties:
        postal_code: { type: string, example: "99501" }
        country_code: { type: string, example: "US" }
        latitude: { type: number, example: 61.2181 }
        longitude: { type: number, example: -149.9003 }
        city: { type: string, example: "Anchorage" }
        state: { type: string, example: "Alaska" }
        city_en: { type: string, example: "Anchorage" }
        state_en: { type: string, example: "Alaska" }
        state_code: { type: [string, "null"], example: "AK" }
        province: { type: [string, "null"], description: "Second administrative level below the state (a US county, a German Landkreis).", example: "Anchorage" }
        province_code: { type: [string, "null"], example: "020" }
        community: { type: [string, "null"], description: "Third administrative level, where the country has one.", example: null }
        community_code: { type: [string, "null"], example: null }
        accuracy: { type: [integer, "null"], description: "Precision of the coordinates: 1 estimated, 4 place, 6 centroid of addresses.", example: 4 }
    PostalCodeWithDistance:
      allOf:
        - $ref: '#/components/schemas/PostalCode'
        - type: object
          properties:
            distance: { type: number, description: "Distance from the requested point in the requested unit.", example: 0.41 }
    RadiusCode:
      type: object
      properties:
        code: { type: string, example: "99502" }
        city: { type: string, example: "Anchorage" }
        state: { type: string, example: "AK" }
        city_en: { type: string, example: "Anchorage" }
        state_en: { type: string, example: "Alaska" }
        distance: { type: number, example: 5.23 }
    PolygonCode:
      type: object
      properties:
        code: { type: string, example: "99501" }
        city: { type: string, example: "Anchorage" }
        state: { type: string, example: "AK" }
        city_en: { type: string, example: "Anchorage" }
        state_en: { type: string, example: "Alaska" }
        latitude: { type: number, example: 61.2181 }
        longitude: { type: number, example: -149.9003 }
        distance: { type: number, example: 0 }
    CodeListResponse:
      type: object
      properties:
        query: { type: object, additionalProperties: true }
        results:
          type: array
          items: { type: string }
      required: [query, results]
    PagedCodeListResponse:
      type: object
      properties:
        query: { type: object, additionalProperties: true }
        results:
          type: array
          items: { type: string }
        total: { $ref: '#/components/schemas/PagingTotal' }
        page: { $ref: '#/components/schemas/PagingPage' }
        limit: { $ref: '#/components/schemas/PagingLimit' }
        has_more: { $ref: '#/components/schemas/PagingHasMore' }
      required: [query, results, total, page, limit, has_more]
    PagingTotal: { type: integer, description: "Number of postal codes that match, on all pages.", example: 3 }
    PagingPage: { type: integer, description: "This page, from 1.", example: 1 }
    PagingLimit: { type: integer, description: "Codes per page (at most 10000).", example: 10000 }
    PagingHasMore: { type: boolean, description: "Whether another page follows: request `page` + 1.", example: false }
    StatusResponse:
      type: object
      properties:
        account_id: { type: integer, example: 313373133731337 }
        account_id_str: { type: string, description: "The same id as `account_id`, as a string for JavaScript clients, whose numbers lose precision above 2^53.", example: "313373133731337" }
        quotas:
          type: object
          properties:
            month: { $ref: '#/components/schemas/QuotaBucket' }
            grace: { $ref: '#/components/schemas/QuotaBucket' }
          required: [month]
        plan:
          type: object
          description: >-
            The plan of the API key's account, so you can check that an upgrade
            took effect.
          properties:
            name: { type: [string, "null"], example: "free" }
            interval: { type: [string, "null"], example: "month" }
            price_id: { type: [string, "null"] }
            free: { type: [boolean, "null"], example: true }
            custom:
              type: boolean
              description: A plan made for this account only; it has no `upgrade_url`.
              example: false
            status:
              type: [string, "null"]
              description: The status of the subscription, e.g. `active` or `past_due`.
              example: "active"
            renews_at: { type: [string, "null"], format: date-time, example: "2026-11-01T00:00:00+00:00" }
            scheduled:
              type: [object, "null"]
              description: A plan change that starts later, e.g. a downgrade at the end of the period.
              properties:
                name: { type: [string, "null"] }
                interval: { type: [string, "null"], example: "month" }
                starts_at: { type: string, format: date-time }
            upgrade_url:
              type: [string, "null"]
              format: uri
              description: Opens the plans of this account. `null` for a custom plan.
            plans_url:
              type: string
              format: uri
              description: Every plan as JSON.
              example: "https://api.zipcodestack.com/v1/agent/plans"
      required: [quotas]
    QuotaBucket:
      type: object
      properties:
        total: { type: integer, example: 300 }
        used: { type: integer, example: 72 }
        remaining: { type: integer, example: 228 }
    Error:
      type: object
      description: >-
        Error body. Errors raised as exceptions (including gateway-generated
        401/429s) carry `message` plus a machine-readable `error` envelope
        with a stable `error.code` and actionable next-step URLs in `actions`;
        quota-related 429 errors additionally include `quota`. Postal-code
        not-found errors on `/v1/match` and `/v1/radius` instead map a
        parameter name (`codes` / `error`) to the message and include an
        `info` documentation link.
      properties:
        message:
          type: string
          description: Human-readable error message.
        info:
          type: string
          description: Documentation link. Present on parameter-mapped errors.
        error:
          type: object
          properties:
            code:
              type: string
              description: >-
                Stable error code. Common values: missing_api_key,
                invalid_api_key, key_not_allowed_for_product,
                referrer_not_allowed, quota_exceeded, grace_quota_exceeded,
                overage_quota_exceeded, rate_limit_exceeded, not_found,
                forbidden.
              example: quota_exceeded
            message:
              type: string
              description: Same human-readable message as the top-level `message`.
        quota:
          type: object
          description: Current quota state. Present on quota-related 429 errors only.
          properties:
            limit: { type: integer, example: 300 }
            used: { type: integer, example: 300 }
            remaining: { type: integer, example: 0 }
            resets_at:
              type: [string, 'null']
              format: date-time
              description: When the quota resets, as an ISO 8601 timestamp. Null if unknown.
        actions:
          type: object
          description: >-
            Next steps as absolute URLs, keyed by action. A key is only
            present when it applies to this error and is available for this
            product right now, so read the keys you know and ignore the
            others. `sign_up`: create an account with a free API key.
            `upgrade`: the plans of the API key's account (quota and plan
            errors). `complete_payment`: pay the open invoice (grace quota).
            `verify_email`: confirm the account's email address. `docs`: the
            documentation (always present). `openapi`: this specification.
            `pricing`: the plans as JSON. `agent_setup`: setup steps for AI
            coding agents. `get_free_api_key`, `get_trial_key`, `get_api_key`:
            endpoints that issue API keys to agents; `agent_setup` explains
            them.
            `enable_overage`: turn on overage for the account, so requests past
            the monthly quota are served and billed per request up to a budget
            (quota_exceeded of a paid plan that has overage, while it is off).
          additionalProperties: { type: string, format: uri }
      additionalProperties: true
