openapi: 3.1.0
info:
  title: Fake Addresses API
  version: "1.0.0"
  summary: Random address generator API — postal-format-valid, non-deliverable test data.
  description: |
    Random Address Generator API for QA and development. Every record is
    assembled from one real TIGER street inside one real ZCTA, with a
    street number drawn from a range with no registered delivery point —
    postal-format-valid, non-deliverable by construction. No real people,
    no real deliverable addresses.

    Free, keyless, CORS-enabled. The anonymous rate limit is 60 requests
    per minute per client, published here and in every response's
    `RateLimit-*` headers (IETF draft-ietf-httpapi-ratelimit-headers) and
    the legacy `X-RateLimit-*` form, kept side by side for back-compat.

    `seed` + `dataset` determinism is permanent: the same `seed` and the
    same `dataset` version return byte-identical records forever, across
    deploys (docs/accuracy.md §3). Omit `seed` for fresh output on every
    call — the response always echoes the seed it used, so you can pin it
    afterwards.

    Output: free to use, no attribution required. Terms in prose at /terms/.
  license:
    name: No attribution required (API output)
    url: https://creativecommons.org/publicdomain/zero/1.0/
  contact:
    name: Fake Addresses
    url: https://fakeaddresses.com/contact/
servers:
  - url: https://fakeaddresses.com/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development
tags:
  - name: addresses
    description: Generate and export random, postal-format-valid US test addresses.
  - name: meta
    description: Service metadata — rate limit, dataset versions, export formats, licence.
  - name: coverage
    description: Street-edge data coverage by US state.
paths:
  /addresses:
    get:
      operationId: getAddresses
      tags: [addresses]
      summary: Generate or export random test addresses
      description: |
        Generates `count` records starting at `index` within the sequence
        determined by `seed` + `dataset`, optionally narrowed by
        `stateUsps` / `city` / `zip`. `format=json` (the default) is
        buffered as a single streamed JSON object; every other format
        streams a downloadable file body.
      parameters:
        - name: seed
          in: query
          description: Public seed. Omit for a fresh, randomly-chosen seed (echoed in the response so you can pin it).
          schema: { type: string, maxLength: 200 }
        - name: dataset
          in: query
          description: Dataset version to pin against. Defaults to the current version.
          schema: { type: string, default: "2026.09" }
        - name: count
          in: query
          description: Number of records to generate (1-10000).
          schema: { type: integer, minimum: 1, maximum: 10000, default: 1 }
        - name: index
          in: query
          description: Starting index within the seed's sequence (0-based). Useful for paginating a large pinned sequence.
          schema: { type: integer, minimum: 0, default: 0 }
        - name: stateUsps
          in: query
          description: Two-letter USPS state abbreviation to narrow generation to, e.g. "TX".
          schema: { type: string, minLength: 2, maxLength: 2 }
        - name: city
          in: query
          description: Census place name to narrow generation to. Requires stateUsps.
          schema: { type: string }
        - name: zip
          in: query
          description: Exact 5-digit ZIP/ZCTA to generate within.
          schema: { type: string, pattern: "^[0-9]{5}$" }
        - name: includeEmail
          in: query
          description: Opt-in. Adds a reserved-domain (RFC 2606) email per record.
          schema: { type: boolean, default: false }
        - name: includeSsn
          in: query
          description: Opt-in. Adds a reserved-range (area 000/666, group 00) SSN-shaped value per record. Omitted by default (CLAUDE.md §2 Phase 0).
          schema: { type: boolean, default: false }
        - name: includeCreditCard
          in: query
          description: Opt-in. Adds a labelled, processor-published sandbox card number per record — never an arbitrary Luhn-valid number.
          schema: { type: boolean, default: false }
        - name: includeMap
          in: query
          description: Adds a pre-computed, ≤2 KB simplified ZCTA boundary SVG path + pin position per record (format=json only in practice).
          schema: { type: boolean, default: false }
        - name: format
          in: query
          description: Output format.
          schema: { type: string, enum: [json, ndjson, csv, sql, xlsx], default: json }
        - name: sqlDialect
          in: query
          description: Required interpretation when format=sql.
          schema: { type: string, enum: [postgres, mysql, sqlite, mssql, oracle, bigquery, snowflake], default: postgres }
        - name: table
          in: query
          description: Table name used by format=sql. Must be a valid SQL identifier.
          schema: { type: string, pattern: "^[A-Za-z_][A-Za-z0-9_]{0,62}$", default: fake_addresses }
      responses:
        "200":
          description: Records generated successfully (or, for a bulk export format, a streamed file).
          headers:
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer }, description: Unix seconds when the current window resets. }
            RateLimit-Limit: { schema: { type: integer }, description: "Standards-track form (IETF draft-ietf-httpapi-ratelimit-headers) of X-RateLimit-Limit." }
            RateLimit-Remaining: { schema: { type: integer } }
            RateLimit-Reset: { schema: { type: integer } }
            RateLimit-Policy: { schema: { type: string }, description: "e.g. '60;w=60' — 60 requests per 60-second window." }
            X-Licence: { schema: { type: string } }
            X-Licence-Url: { schema: { type: string } }
            X-Fake-Addresses-Dataset: { schema: { type: string } }
            X-Fake-Addresses-Seed: { schema: { type: string } }
            X-Fake-Addresses-Requested: { schema: { type: integer } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AddressesResponse" }
            application/x-ndjson:
              schema: { type: string, description: "Newline-delimited GeneratedRecord objects, one per line, plus a final {\"_meta\":true,...} summary line." }
            text/csv:
              schema: { type: string }
            application/sql:
              schema: { type: string }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
        "400":
          description: Invalid request (bad parameter, unknown state, malformed filter).
          content:
            application/json: { schema: { $ref: "#/components/schemas/ApiError" } }
        "422":
          description: Well-formed request, but no street-edge coverage exists yet for the given filter.
          content:
            application/json: { schema: { $ref: "#/components/schemas/ApiError" } }
        "429":
          description: Anonymous rate limit exceeded.
          headers:
            Retry-After: { schema: { type: integer } }
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer } }
            RateLimit-Limit: { schema: { type: integer }, description: "Standards-track form (IETF draft-ietf-httpapi-ratelimit-headers) of X-RateLimit-Limit." }
            RateLimit-Remaining: { schema: { type: integer } }
            RateLimit-Reset: { schema: { type: integer } }
            RateLimit-Policy: { schema: { type: string }, description: "e.g. '60;w=60' — 60 requests per 60-second window." }
          content:
            application/json: { schema: { $ref: "#/components/schemas/ApiError" } }
      x-codeSamples:
        - lang: curl
          source: 'curl "https://fakeaddresses.com/api/v1/addresses?seed=ci-fixture&stateUsps=TX&count=5"'
  /meta:
    get:
      operationId: getMeta
      tags: [meta]
      summary: Service metadata — rate limit, dataset versions, formats, licence
      responses:
        "200":
          description: Service metadata.
          content:
            application/json: { schema: { $ref: "#/components/schemas/MetaResponse" } }
  /coverage:
    get:
      operationId: getCoverage
      tags: [coverage]
      summary: Street-edge ingest coverage by US state
      responses:
        "200":
          description: Per-state coverage summary.
          headers:
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer } }
            RateLimit-Limit: { schema: { type: integer }, description: "Standards-track form (IETF draft-ietf-httpapi-ratelimit-headers) of X-RateLimit-Limit." }
            RateLimit-Remaining: { schema: { type: integer } }
            RateLimit-Reset: { schema: { type: integer } }
            RateLimit-Policy: { schema: { type: string }, description: "e.g. '60;w=60' — 60 requests per 60-second window." }
          content:
            application/json: { schema: { $ref: "#/components/schemas/CoverageResponse" } }
        "429":
          description: Anonymous rate limit exceeded.
          content:
            application/json: { schema: { $ref: "#/components/schemas/ApiError" } }
components:
  schemas:
    Licence:
      type: object
      required: [name, statement, url]
      properties:
        name: { type: string, const: no-attribution-required }
        statement: { type: string }
        url: { type: string }
    Street:
      type: object
      required: [number, name, side, line1, line1Uppercase]
      properties:
        number: { type: integer }
        name: { type: string }
        side: { type: string, enum: [L, R] }
        line1: { type: string }
        line1Uppercase: { type: string }
    City:
      type: object
      required: [name, source, matchType]
      properties:
        name: { type: string }
        source: { type: string, enum: [usps_zip_city, census_place] }
        matchType: { type: string, enum: [exact_zip, contained, nearest] }
    County:
      type: object
      required: [fips, name, namelsad, matchType]
      properties:
        fips: { type: string }
        name: { type: string }
        namelsad: { type: string }
        matchType: { type: string, enum: [contained, nearest] }
    State:
      type: object
      required: [usps, name, fips]
      properties:
        usps: { type: string }
        name: { type: string }
        fips: { type: string }
    Zcta:
      type: object
      required: [zcta5]
      properties:
        zcta5: { type: string }
        centroidLat: { type: [number, "null"] }
        centroidLon: { type: [number, "null"] }
    AreaCode:
      type: object
      required: [npa, precision]
      properties:
        npa: { type: string }
        precision: { type: string, enum: [state] }
    NonDeliverable:
      type: object
      required: [byConstruction, method]
      properties:
        byConstruction: { type: boolean, const: true }
        method: { type: string }
    CreditCard:
      type: object
      required: [brand, number, label, source]
      properties:
        brand: { type: string }
        number: { type: string }
        label: { type: string, const: "publisher test value" }
        source: { type: string }
    GeneratedRecord:
      type: object
      required:
        - seed
        - index
        - dataset
        - street
        - city
        - county
        - state
        - zip
        - zcta
        - lastLine
        - lastLineUppercase
        - coordinates
        - timeZone
        - phone
        - areaCode
        - coherenceClaimExcluded
        - nonDeliverable
        - licence
      properties:
        seed: { type: string }
        index: { type: integer }
        dataset: { type: string }
        street: { $ref: "#/components/schemas/Street" }
        city: { $ref: "#/components/schemas/City" }
        county: { $ref: "#/components/schemas/County" }
        state: { $ref: "#/components/schemas/State" }
        zip: { type: string }
        zcta: { $ref: "#/components/schemas/Zcta" }
        lastLine: { type: string }
        lastLineUppercase: { type: string }
        coordinates:
          type: object
          required: [latitude, longitude]
          properties:
            latitude: { type: number }
            longitude: { type: number }
        timeZone: { type: [string, "null"] }
        phone: { type: string, pattern: "^\\([0-9]{3}\\) 555-01[0-9]{2}$" }
        areaCode: { $ref: "#/components/schemas/AreaCode" }
        email: { type: string }
        ssn: { type: string }
        creditCard: { $ref: "#/components/schemas/CreditCard" }
        coherenceClaimExcluded: { type: boolean }
        nonDeliverable: { $ref: "#/components/schemas/NonDeliverable" }
        licence: { $ref: "#/components/schemas/Licence" }
    SkipEntry:
      type: object
      required: [index, reason]
      properties:
        index: { type: integer }
        reason: { type: string }
    GenerationSummary:
      type: object
      required: [requested, returned, skippedCount, skipped]
      properties:
        requested: { type: integer }
        returned: { type: integer }
        skippedCount:
          type: integer
          description: >-
            Always 0. Records are drawn from an immutable, pre-validated corpus,
            so no index can fail at request time.
        skipped:
          type: array
          items: { $ref: "#/components/schemas/SkipEntry" }
        poolSize:
          type: integer
          description: >-
            Distinct records the request's filter matches in this dataset version.
        poolExhausted:
          type: boolean
          description: >-
            True when startIndex + requested exceeds poolSize, so the seeded
            sequence wraps and rows repeat. Still fully deterministic; reported
            rather than hidden.
    AddressesResponse:
      type: object
      required: [dataset, seed, seedWasGenerated, licence, filter, requested, startIndex, records, generation]
      properties:
        dataset: { type: string }
        seed: { type: string }
        seedWasGenerated: { type: boolean }
        licence: { $ref: "#/components/schemas/Licence" }
        filter:
          type: object
          properties:
            stateUsps: { type: [string, "null"] }
            city: { type: [string, "null"] }
            zip: { type: [string, "null"] }
        requested: { type: integer }
        startIndex: { type: integer }
        records:
          type: array
          items: { $ref: "#/components/schemas/GeneratedRecord" }
        generation: { $ref: "#/components/schemas/GenerationSummary" }
    ApiError:
      type: object
      required: [error, message]
      properties:
        error: { type: string }
        message: { type: string }
        coverageHint: { type: string }
    MetaResponse:
      type: object
      required: [api, rateLimit, dataset, exportFormats, sqlDialects, maxRowsPerRequest, licence, endpoints]
      properties:
        api: { type: string }
        rateLimit:
          type: object
          required: [limit, windowSeconds, anonymous, enforcement]
          properties:
            limit: { type: integer }
            windowSeconds: { type: integer }
            anonymous: { type: boolean }
            enforcement: { type: string }
        dataset:
          type: object
          required: [current, known, note]
          properties:
            current: { type: string }
            known: { type: array, items: { type: string } }
            note: { type: string }
        exportFormats: { type: array, items: { type: string } }
        sqlDialects: { type: array, items: { type: string } }
        maxRowsPerRequest: { type: integer }
        licence: { $ref: "#/components/schemas/Licence" }
        endpoints:
          type: object
          properties:
            addresses: { type: string }
            coverage: { type: string }
            meta: { type: string }
            openapi: { type: string }
    CoverageResponse:
      type: object
      description: >-
        What the PUBLISHED dataset can generate, measured from its manifest.
        The dataset is not nationwide and does not claim to be: a filter
        selecting a locality outside it returns 422, never an invented address.
      required: [approximation, states, totals]
      properties:
        approximation: { type: string }
        dataset: { type: string }
        recordCount: { type: integer }
        zctasRepresented: { type: integer }
        zctasNational: { type: integer }
        countiesRepresented: { type: integer }
        countiesNational: { type: integer }
        states:
          type: array
          items:
            type: object
            required: [stateUsps, countiesQueued, countiesDone, countiesPending, countiesFailed]
            properties:
              stateUsps: { type: string }
              countiesQueued:
                type: integer
                description: Counties in the state per the US Census TIGER county layer.
              countiesDone:
                type: integer
                description: Counties that contribute records to the published dataset.
              countiesPending: { type: integer }
              countiesFailed:
                type: integer
                description: Always 0 — an unresolvable locality is simply absent from the dataset.
              countiesRepresented: { type: integer }
              countiesInState: { type: integer }
              zctasRepresented: { type: integer }
              recordCount: { type: integer }
        totals:
          type: object
          required: [countiesQueued, countiesDone, countiesPending, countiesFailed]
          properties:
            countiesQueued: { type: integer }
            countiesDone: { type: integer }
            countiesPending: { type: integer }
            countiesFailed: { type: integer }
