openapi: 3.1.0
info:
  title: posty.ro API
  description: >
    Developer-first Romanian address verification, autocomplete and

    postal-code lookup API.


    ## Error codes

    Every error body is `{"error": {"code", "message", "request_id"}}`. Codes
    are stable; match on `code`, not `message`.


    | Status | Codes |

    |---|---|

    | 400 | `invalid_body`, `invalid_parameter`, `invalid_postal_code`,
    `invalid_query`, `missing_address`, `ambiguous_input`, `missing_locality`,
    `house_number_requires_street`, `invalid_house_number`, `missing_file_url`,
    `invalid_callback_url`, `invalid_idempotency_key`, `field_too_long` |

    | 401 | `missing_credentials`, `invalid_api_key` |

    | 402 | `quota_exceeded`, `payment_required` |

    | 403 | `forbidden`, `subscription_suspended` |

    | 404 | `not_found`, `postal_code_not_found`, `locality_not_found` |

    | 405 | `method_not_allowed` |

    | 409 | `job_not_complete` |

    | 410 | `results_expired` |

    | 413 | `payload_too_large` |

    | 422 | `idempotency_key_reused` |

    | 429 | `rate_limit_exceeded` |

    | 500 | `internal_error` |

    | 504 | `request_timeout` |


    ## Quota vs rate limit

    - **402 Payment Required**: `quota_exceeded` (monthly quota used up on a
    plan without overage)
      or `payment_required` (invoice past due beyond grace). Fixed with money.
    - **429 Too Many Requests**: `rate_limit_exceeded`; wait `Retry-After`
    seconds.

    - Only billable product calls (lookup, search, autocomplete, verify, batch
    submit, reference data) count toward
      quota and are gated by 402; reference data (`/v1/counties`, `/v1/localities/{siruta}`) counts as a lookup like
      postal-code lookup. Usage and batch polling/listing are not counted.

    ## Pagination

    List endpoints use opaque cursors: `limit` (1-200, default 50) and `cursor`;
    the response carries

    `data` and `next_cursor` (null on the last page). Pass `next_cursor` back
    unchanged as `cursor`;

    a malformed cursor or out-of-range `limit` is `400 invalid_parameter`. Lists
    are scoped to the

    caller's organisation and ordered newest first. `/v1/orgs/{slug}/usage` is a
    bounded time series

    (at most 90 days) and is not paginated.


    ## Request bodies

    JSON request bodies are strict: a field that is not documented (for example
    a typo such as

    `housenumber`), a value of the wrong type, or any data after the JSON value
    returns

    400 `invalid_body`. Responses may gain fields; requests may not carry
    unknown ones.

    A repeated object key is not an error; the last value wins, so do not send
    duplicates.


    ## Versioning & deprecation

    - URL major version (`/v1`). Within `/v1`: adding endpoints, optional
    request fields,
      response fields, enum values and error codes is non-breaking; clients must ignore unknown fields.
    - Breaking changes ship only in a new major path (`/v2`) after GA (1.0.0).
    Before 1.0.0
      they may ship in `/v1` and are listed under "Changed (breaking)" in `apispec/CHANGELOG.md`.
    - Deprecated operations are marked `deprecated: true` and respond with
    `Deprecation`
      (RFC 9745) and `Sunset` (RFC 8594) headers at least 6 months before removal.
  version: 0.2.0
  contact:
    name: posty.ro
    url: https://posty.ro
  termsOfService: https://posty.ro/legal/terms
  license:
    name: Proprietary; API access under the posty.ro Terms of Service
    url: https://posty.ro/legal/terms
  x-data-sources:
    - name: Coduri poștale România (Poșta Română, via data.gov.ro)
      license: OGL-ROU-1.0
      url: https://data.gov.ro/dataset/coduri-postale-romania
      version: 2016-05
    - name: OpenStreetMap
      license: ODbL-1.0
      attribution: © OpenStreetMap contributors
      url: https://www.openstreetmap.org/copyright
      note: Only when the OSM source is enabled.
servers:
  - url: http://localhost:8080
    description: Local development
  - url: https://api.sandbox.posty.ro
    description: Sandbox
  - url: https://api.posty.ro
    description: Production
security:
  - BearerAuth: []
tags:
  - name: system
    description: Health and operational endpoints (no auth)
  - name: public
    description: Public API — address lookup and verification (Bearer API key)
  - name: billing
    description: Organization usage and billing read models (Bearer API key of the
      organization; no session cookies in the Go API)
components:
  parameters:
    IfNoneMatch:
      name: If-None-Match
      in: header
      required: false
      description: ETag from a previous 200. A match returns `304` with no body. A
        `304` is a served request and is billed exactly like a `200`.
      schema:
        type: string
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: Optional client correlation ID (8-64 chars `[A-Za-z0-9._-]`). When
        valid it is echoed as `X-Posty-Request-Id` and used in logs and
        `error.request_id`; invalid values are ignored and replaced by a
        generated ID. It is a correlation label only, not a trace ID.
      schema:
        type: string
        pattern: ^[A-Za-z0-9._-]{8,64}$
  headers:
    XDataAttribution:
      description: Licence attribution for the data sources used (OGL-ROU-1.0;
        ODbL-1.0 when OSM is enabled). Customers displaying results must show
        it. On autocomplete the text follows the enabled sources (OSM text only
        when OSM is enabled); all other endpoints always send the
        government-data text. See https://posty.ro/legal/data-sources.
      schema:
        type: string
      example: Posta Romana via data.gov.ro (OGL-ROU-1.0), infocod 2016-05
    LinkLicense:
      description: Link to the data-sources page, rel="license". Always sent together
        with X-Data-Attribution (product routes only, not usage reads).
      schema:
        type: string
      example: <https://posty.ro/legal/data-sources>; rel="license"
    RequestId:
      description: Unique request identifier, present on every response. Quote it when
        contacting support (it is also `error.request_id`).
      required: true
      schema:
        type: string
      example: a1b2c3d4e5f6a7b8
    RateLimitLimit:
      description: Requests allowed in the current rate-limit window (the per-key
        limit that applied to this request).
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the current rate-limit window.
      schema:
        type: integer
    RateLimitReset:
      description: Unix time (seconds) when the rate-limit window resets.
      schema:
        type: integer
    ContentDisposition:
      description: Suggested file name, `attachment; filename="batch-results.csv"`.
      schema:
        type: string
    RetryAfter:
      description: Seconds to wait before retrying.
      required: true
      schema:
        type: integer
        minimum: 1
    CacheControl:
      description: Always `private` (responses are authenticated and metered; shared
        caches must not store them). `max-age` equals the server-side cache TTL
        of the endpoint (3600 for postal-code lookup, 300 for autocomplete).
      schema:
        type: string
      example: private, max-age=300
    ETag:
      description: "Weak validator of the response body. Send it back as
        `If-None-Match` to get an empty `304`. Autocomplete bodies are per
        organization; the validator is only meaningful for the same API key
        (`Vary: Authorization`)."
      schema:
        type: string
      example: W/"1a2b3c4d5e6f7a8b"
    VaryAuthorization:
      description: Always `Authorization` on 200 and 304 lookup responses.
      schema:
        type: string
        enum:
          - Authorization
    ContentLanguage:
      description: Language of the error `message` (`ro` or `en`), chosen from
        `Accept-Language` (default `ro`). Sent on error responses only; success
        bodies are not localised.
      schema:
        type: string
        enum:
          - ro
          - en
    VaryAcceptLanguage:
      description: Always `Accept-Language` on error responses, because the message
        language depends on it.
      schema:
        type: string
        enum:
          - Accept-Language
    Cache:
      description: "`HIT` when the response was served from the API cache, `MISS` when
        the cache was consulted and the response was computed. Absent when the
        cache was bypassed. Sent on 200 responses only."
      schema:
        type: string
        enum:
          - HIT
          - MISS
    PeriodResetsAt:
      description: RFC 3339 time at which the monthly quota resets (sent with
        `quota_exceeded`).
      schema:
        type: string
        format: date-time
    GracePeriodEnds:
      description: RFC 3339 end of the past-due grace period (sent with `payment_required`).
      schema:
        type: string
        format: date-time
  responses:
    IdempotencyKeyReused:
      description: The `Idempotency-Key` was already used (within 24 hours, same
        organisation) with a different request body (`idempotency_key_reused`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Invalid input (see `error.code`, e.g. `invalid_body`,
        `invalid_postal_code`, `missing_file_url`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing (`missing_credentials`) or invalid/revoked
        (`invalid_api_key`) API key
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PaymentRequired:
      description: Monthly quota exhausted on a plan without overage
        (`quota_exceeded`, with `X-Posty-Period-Resets-At`) or payment past due
        beyond the grace period (`payment_required`, with
        `X-Posty-Grace-Period-Ends`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
        X-Posty-Period-Resets-At:
          $ref: "#/components/headers/PeriodResetsAt"
        X-Posty-Grace-Period-Ends:
          $ref: "#/components/headers/GracePeriodEnds"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Resource belongs to another organization (`forbidden`) or the
        subscription is suspended for non-payment (`subscription_suspended`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ForbiddenOrg:
      description: Resource belongs to another organization (`forbidden`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found (`not_found`, `postal_code_not_found`,
        `locality_not_found`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Resource not in the required state (`job_not_complete`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-Data-Attribution:
          $ref: "#/components/headers/XDataAttribution"
        Link:
          $ref: "#/components/headers/LinkLicense"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Gone:
      description: Results expired and were deleted (`results_expired`; retention
        period after completion, 30 days by default)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PayloadTooLarge:
      description: Request body larger than 64 KiB (`payload_too_large`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Rate limit exceeded (`rate_limit_exceeded`; per-key RPS/RPM from
        the plan, or the per-IP daily limit of the demo key)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Unexpected server error (`internal_error`)
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    GatewayTimeout:
      description: The request exceeded the server-side processing deadline
        (`request_timeout`). Retry idempotent requests; retry `POST
        /v1/addresses/batch` only with an `Idempotency-Key`, otherwise a retry
        may create a duplicate job
      headers:
        X-Posty-Request-Id:
          $ref: "#/components/headers/RequestId"
        Content-Language:
          $ref: "#/components/headers/ContentLanguage"
        Vary:
          $ref: "#/components/headers/VaryAcceptLanguage"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key (psk_live_… / psk_test_…)
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable machine-readable code. Messages may change; codes do not
                (see `## Error codes` in info.description).
              enum:
                - ambiguous_input
                - field_too_long
                - forbidden
                - house_number_requires_street
                - idempotency_key_reused
                - internal_error
                - invalid_api_key
                - invalid_body
                - invalid_callback_url
                - invalid_house_number
                - invalid_idempotency_key
                - invalid_parameter
                - invalid_postal_code
                - invalid_query
                - job_not_complete
                - locality_not_found
                - method_not_allowed
                - missing_address
                - missing_credentials
                - missing_file_url
                - missing_locality
                - not_found
                - payload_too_large
                - payment_required
                - postal_code_not_found
                - quota_exceeded
                - rate_limit_exceeded
                - request_timeout
                - results_expired
                - subscription_suspended
              example: invalid_postal_code
            message:
              type: string
              example: postal code must be exactly 6 digits
            request_id:
              type: string
              example: a1b2c3d4e5f6a7b8
    AddressInput:
      type: object
      additionalProperties: false
      properties:
        postal_code:
          type: string
          maxLength: 256
          example: "400001"
        locality:
          type: string
          maxLength: 256
          example: Cluj-Napoca
        county:
          type: string
          maxLength: 256
          example: Cluj
        county_code:
          type: string
          maxLength: 256
          example: CJ
        street:
          type: string
          maxLength: 256
          example: Mihai Viteazu
        house_number:
          type: string
          maxLength: 256
          example: 12A
    VerifyRequest:
      type: object
      additionalProperties: false
      description: Provide exactly one of `address` (structured) or `freeform` (text).
        Both gives 400 `ambiguous_input`, neither gives 400 `missing_address`.
        Unknown fields give 400 `invalid_body`.
      oneOf:
        - required:
            - address
        - required:
            - freeform
      properties:
        address:
          $ref: "#/components/schemas/AddressInput"
        freeform:
          type: string
          maxLength: 256
          example: Str. Mihai Viteazu 12A, 400001 Cluj-Napoca
        options:
          type: object
          additionalProperties: false
          description: Reserved for future options.
    Correction:
      type: object
      required:
        - field
        - from
        - to
        - reason
      properties:
        field:
          type: string
          example: street
        from:
          type: string
          example: Mihai Vitezu
        to:
          type: string
          example: Mihai Viteazu
        reason:
          type: string
          enum:
            - typo
            - casing
            - diacritic
            - locality_mismatch
            - unknown
    VerifyResponse:
      type: object
      required:
        - verdict
        - confidence
        - input
        - normalized
        - corrections
        - metadata
      properties:
        verdict:
          type: string
          enum:
            - verified
            - corrected
            - partial
            - unverifiable
          example: verified
        confidence:
          type: number
          format: float
          example: 0.95
        input:
          $ref: "#/components/schemas/AddressInput"
        normalized:
          $ref: "#/components/schemas/AddressInput"
        corrections:
          type: array
          items:
            $ref: "#/components/schemas/Correction"
        metadata:
          type: object
          required:
            - source_versions
          properties:
            source_versions:
              type: object
              additionalProperties:
                type: string
              example:
                baseline: infocod-2016-05
    AutocompleteSuggestion:
      type: object
      required:
        - display
        - components
        - highlight
        - score
        - place_id
      properties:
        display:
          type: string
          example: Mihai Viteazu, Cluj-Napoca, Cluj, 400001
        components:
          type: object
          required:
            - street
            - locality
            - county
            - county_code
          properties:
            street:
              type: string
              example: Mihai Viteazu
            locality:
              type: string
              example: Cluj-Napoca
            county:
              type: string
              example: Cluj
            county_code:
              type: string
              pattern: ^[A-Z]{1,2}$
              example: CJ
            postal_code:
              type: string
              example: "400001"
              description: Street-level postal code. Omitted when no postal code is linked to
                this street.
            house_number:
              type: string
              example: "12"
              description: >
                Either the house number taken from the query (a whole number
                1-9999, e.g.

                "mihai viteazu 12" gives "12") or, when the query has none, the
                street's raw

                house ranges joined with "; " (e.g. "nr. 1-T; nr. 2-20"). Do not
                parse it as a

                single number. Omitted when neither is known.
        highlight:
          type: string
          example: Mihai Viteazu, <em>Cluj-Napoca</em>, Cluj, 400001
          description: >
            `display` HTML-escaped (`&`, `<`, `>`, quotes) with each matched
            query term

            wrapped in `<em>`...`</em>`. It is safe to render as HTML as-is.
            `display`

            itself is RAW text: escape it yourself before putting it in HTML,
            and never

            render `display` as HTML unescaped.
        score:
          type: number
          format: float
          example: 0.85
        place_id:
          type: string
          example: psl_MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAxfDAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMg
    AutocompleteResponse:
      type: object
      required:
        - suggestions
        - query
      properties:
        suggestions:
          type: array
          items:
            $ref: "#/components/schemas/AutocompleteSuggestion"
        query:
          type: object
          required:
            - q
            - normalized
          properties:
            q:
              type: string
              example: cluj mihai
            normalized:
              type: string
              example: cluj mihai
    PostalCodeLookup:
      type: object
      required:
        - code
        - localities
        - streets
      properties:
        code:
          type: string
          pattern: ^[0-9]{6}$
          example: "400000"
        localities:
          type: array
          items:
            type: object
            required:
              - name
              - type
              - county_code
            properties:
              name:
                type: string
                example: Cluj-Napoca
              type:
                type: string
                enum:
                  - municipiu
                  - oras
                  - comuna
                  - sat
                  - sector
              siruta:
                type: integer
                example: 54975
              county_code:
                type: string
                pattern: ^[A-Z]{1,2}$
                example: CJ
        streets:
          type: array
          items:
            type: object
            required:
              - name
            properties:
              name:
                type: string
                example: Strada Memorandumului
    PostalCodeSearchRequest:
      type: object
      additionalProperties: false
      required:
        - locality
      properties:
        locality:
          type: string
          maxLength: 256
          example: Târgu Mureș
        county_code:
          type: string
          maxLength: 256
          example: MS
        street:
          type: string
          maxLength: 256
          example: bd. 1 Decembrie 1918
        house_number:
          type: string
          maxLength: 256
          example: 12A
          description: House number as written on the building, alphanumerics allowed
            (e.g. "12A", "255-T"). Requires `street`.
        limit:
          type: integer
          default: 100
          minimum: 1
          maximum: 500
          example: 50
    PostalCodeSearchResponse:
      type: object
      required:
        - postal_codes
      properties:
        postal_codes:
          type: array
          items:
            $ref: "#/components/schemas/PostalCodeSearchItem"
    PostalCodeSearchItem:
      type: object
      required:
        - street
        - locality
        - county
        - county_code
        - postal_code
        - house_number
      properties:
        street:
          type: string
          example: bd. 1 Decembrie 1918
        locality:
          type: string
          example: Târgu Mureș
        county:
          type: string
          example: Mureș
        county_code:
          type: string
          pattern: ^[A-Z]{1,2}$
          example: MS
        postal_code:
          type: string
          example: "540510"
        house_number:
          type: string
          example: nr. 255-T
    UsageSeriesRow:
      type: object
      required:
        - date
        - count
      properties:
        date:
          type: string
          format: date
          example: 2026-06-01
        endpoint:
          type: string
          example: postal_lookup
          description: Populated only when grouped by endpoint.
        api_key_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
          description: Populated only when grouped by key.
        count:
          type: integer
          example: 1234
    UsageResponse:
      type: object
      required:
        - series
        - totals
      properties:
        series:
          type: array
          items:
            $ref: "#/components/schemas/UsageSeriesRow"
        totals:
          type: object
          required:
            - total
          properties:
            total:
              type: integer
              example: 5000
    UsageSummary:
      type: object
      required:
        - period_start
        - period_end
        - used
        - included
        - overage_units
        - overage_cents
        - plan_id
        - projected_invoice_cents
      properties:
        period_start:
          type: string
          format: date-time
          example: 2026-06-01T00:00:00Z
        period_end:
          type: string
          format: date-time
          example: 2026-07-01T00:00:00Z
        used:
          type: integer
          description: Total billable API calls in the current period.
          example: 60000
        included:
          type: integer
          description: Calls included in the plan (monthly quota).
          example: 50000
        overage_units:
          type: integer
          description: Number of 1000-call overage units incurred so far.
          example: 10
        overage_cents:
          type: integer
          description: Overage cost in EUR cents.
          example: 500
        plan_id:
          type: string
          example: starter
        projected_invoice_cents:
          type: integer
          description: Projected overage charge on the next invoice, in EUR cents.
          example: 500
    BatchSubmitRequest:
      type: object
      additionalProperties: false
      required:
        - file_url
      properties:
        file_url:
          type: string
          format: uri
        options:
          type: object
          additionalProperties: false
          properties:
            callback_url:
              type: string
              format: uri
              maxLength: 2048
              description: https URL on a public host; private, loopback and link-local
                targets are rejected (400 invalid_callback_url).
    BatchStatus:
      type: string
      enum:
        - queued
        - processing
        - done
        - failed
        - expired
      description: "expired: results were deleted after the retention period (30 days
        after completion by default); the status endpoint returns it with 200,
        the results endpoint 410 results_expired."
    BatchSubmitResponse:
      type: object
      required:
        - id
        - status
        - row_count
        - webhook
        - created_at
      properties:
        id:
          type: string
          example: bat_550e8400e29b4123
        status:
          $ref: "#/components/schemas/BatchStatus"
        row_count:
          type:
            - integer
            - "null"
        webhook:
          type: string
          enum:
            - queued
            - not_available_on_plan
            - none
        created_at:
          type: string
          format: date-time
    BatchJob:
      type: object
      required:
        - id
        - status
        - row_count
        - processed
        - created_at
        - started_at
        - finished_at
        - error_message
      properties:
        id:
          type: string
          example: bat_550e8400e29b4123
        status:
          $ref: "#/components/schemas/BatchStatus"
        row_count:
          type:
            - integer
            - "null"
        processed:
          type: integer
        created_at:
          type: string
          format: date-time
        started_at:
          type:
            - string
            - "null"
          format: date-time
        finished_at:
          type:
            - string
            - "null"
          format: date-time
        error_message:
          type:
            - string
            - "null"
    County:
      type: object
      required:
        - code
        - name
        - siruta
      properties:
        code:
          type: string
          example: CJ
        name:
          type: string
          example: Cluj
        siruta:
          type:
            - integer
            - "null"
          example: 12
    CountyList:
      type: object
      required:
        - counties
      properties:
        counties:
          type: array
          items:
            $ref: "#/components/schemas/County"
    Locality:
      type: object
      required:
        - siruta
        - name
        - type
        - county_code
        - county
        - parent
      properties:
        siruta:
          type: integer
          example: 54975
        name:
          type: string
          example: Cluj-Napoca
        type:
          type: string
          enum:
            - municipiu
            - oras
            - comuna
            - sat
            - sector
        county_code:
          type: string
          example: CJ
        county:
          type: string
          example: Cluj
        parent:
          type:
            - string
            - "null"
          description: Containing locality, for example `Bucuresti` for a sector; null
            otherwise.
    BatchJobList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BatchJob"
        next_cursor:
          type:
            - string
            - "null"
          description: Pass as `cursor` to get the next page; null on the last page.
    HealthStatus:
      type: string
      enum:
        - ok
        - degraded
        - disabled
    Health:
      type: object
      required:
        - status
        - platform_db
        - geodata_db
        - redis
      properties:
        status:
          type: string
          enum:
            - ok
            - degraded
        redis:
          $ref: "#/components/schemas/HealthStatus"
        platform_db:
          $ref: "#/components/schemas/HealthStatus"
        geodata_db:
          $ref: "#/components/schemas/HealthStatus"
paths:
  /v1/addresses/autocomplete:
    get:
      operationId: getAddressAutocomplete
      summary: Address autocomplete
      description: Returns fuzzy-matched address suggestions for a partial query string.
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IfNoneMatch"
        - name: q
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 256
          example: cluj mihai
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 10
            minimum: 1
            maximum: 25
          example: 10
        - name: context
          in: query
          required: false
          description: Optional locality context filter in the form locality:<siruta_int>.
            Reserved for future use.
          schema:
            type: string
            maxLength: 256
          example: locality:54975
      responses:
        "200":
          description: Autocomplete suggestions returned
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AutocompleteResponse"
        "304":
          description: Not modified (`If-None-Match` matched). No body; billed like a 200.
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/addresses/verify:
    post:
      operationId: postAddressVerify
      summary: Verify a Romanian address
      description: >
        Accepts either a structured address object or a freeform string.

        Returns a verdict, confidence score, normalized components, and
        per-field corrections.
      tags:
        - public
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
            examples:
              structured:
                summary: Structured input
                value:
                  address:
                    postal_code: "400001"
                    locality: Cluj-Napoca
                    county_code: CJ
                    street: Mihai Viteazu
                    house_number: 12A
              freeform:
                summary: Freeform string
                value:
                  freeform: Str. Mihai Viteazu 12A, 400001 Cluj-Napoca
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Verification result
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/postal-codes/search:
    post:
      operationId: searchPostalCodes
      summary: Postal code search
      description: Resolves postal codes from address fields (locality, street, house
        number). Inverse of GET /v1/postal-codes/{code}.
      tags:
        - public
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PostalCodeSearchRequest"
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Search results (empty array if no data found)
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostalCodeSearchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/postal-codes/{code}:
    get:
      operationId: getPostalCodeLookup
      summary: Postal code lookup
      description: Returns all localities and streets associated with a 6-digit
        Romanian postal code.
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IfNoneMatch"
        - name: code
          in: path
          required: true
          schema:
            type: string
            pattern: ^[0-9]{6}$
          example: "400000"
      responses:
        "200":
          description: Postal code found
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostalCodeLookup"
        "304":
          description: Not modified (`If-None-Match` matched). No body; billed like a 200.
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/counties:
    get:
      operationId: listCounties
      summary: List counties
      description: All 42 counties (including Bucuresti, code `B`), ordered by name.
        Reference data from the government baseline only. A billed lookup like
        postal-code lookup; cached for 1 hour (`ETag`, private `Cache-Control`).
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IfNoneMatch"
      responses:
        "200":
          description: OK
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CountyList"
        "304":
          description: Not modified (`If-None-Match` matched). No body; billed like a 200.
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/localities/{siruta}:
    get:
      operationId: getLocalityBySiruta
      summary: Get a locality by SIRUTA code
      description: Resolves a SIRUTA code to its locality (government baseline only;
        OSM data is never used). `parent` is the containing locality, for
        example `Bucuresti` for a sector. A billed lookup like postal-code
        lookup; cached for 1 hour.
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IfNoneMatch"
        - name: siruta
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 2147483647
          example: 54975
      responses:
        "200":
          description: OK
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Locality"
        "304":
          description: Not modified (`If-None-Match` matched). No body; billed like a 200.
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Cache:
              $ref: "#/components/headers/Cache"
            Cache-Control:
              $ref: "#/components/headers/CacheControl"
            ETag:
              $ref: "#/components/headers/ETag"
            Vary:
              $ref: "#/components/headers/VaryAuthorization"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/addresses/batch:
    get:
      operationId: listBatchJobs
      summary: List batch jobs
      description: >
        The calling organisation's batch jobs, newest first (`created_at`
        descending, ties by `id`).

        Cursor pagination; see `## Pagination`. Like batch status polling this
        is a management read:

        not counted toward quota and not blocked by the quota or billing gates.
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: cursor
          in: query
          required: false
          description: Opaque value from a previous response's `next_cursor`.
          schema:
            type: string
      responses:
        "200":
          description: One page of batch jobs
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJobList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
    post:
      operationId: submitBatch
      summary: Submit a batch address verification job
      description: >
        Send an `Idempotency-Key` header (1-255 visible ASCII characters, e.g. a
        UUID) to make retries safe.

        Within 24 hours, the same key from the same organisation with the same
        `file_url` and

        `options.callback_url` returns the original job (`202`, header
        `Idempotent-Replayed: true`, no new job,

        not billed again); the same key with a different body is rejected with
        `422 idempotency_key_reused`.

        A replay is not blocked by the batch credit pre-check and repeats the
        original `webhook` value; the route-wide quota and billing gates still
        apply, so a hard over-quota or past-due organisation gets `402` on a
        replay too. A malformed key is `400 invalid_idempotency_key`. Without
        the header every call creates a new job.
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
            pattern: ^[\x21-\x7e]+$
          example: 0b9f3c1e-6c1a-4a53-9d0e-2f1d5b7a9c11
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchSubmitRequest"
      responses:
        "202":
          description: Job accepted
          headers:
            Idempotent-Replayed:
              description: "`true` when this response replays the job created by an earlier
                request with the same `Idempotency-Key`"
              schema:
                type: string
                enum:
                  - "true"
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchSubmitResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/addresses/batch/{id}:
    get:
      operationId: getBatchStatus
      summary: Get batch job status
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job status
          headers:
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ForbiddenOrg"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
  /v1/addresses/batch/{id}/results:
    get:
      operationId: getBatchResults
      summary: Download batch results as CSV
      tags:
        - public
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: CSV results stream
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Posty-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-Data-Attribution:
              $ref: "#/components/headers/XDataAttribution"
            Link:
              $ref: "#/components/headers/LinkLicense"
          content:
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ForbiddenOrg"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
