openapi: 3.1.0
info:
  title: Postio API
  description: >-
    Postio is the UK validation API for **addresses**, **emails** and **phone numbers**, running on Cloudflare's edge so
    requests are answered close to wherever your users are.


    > **First time?** [Sign up free](https://postio.co.uk) — first 100 lookups on us, no card needed.


    Every endpoint is a `GET` returning JSON, served over HTTPS at `https://api.postio.co.uk/v1`. If you can speak HTTP
    and parse JSON, you can integrate in under five minutes — copy your `pk_*` key from the dashboard and you're away.


    ## Authentication


    Send your key in the `x-api-key` header on every request:


    ```bash

    curl https://api.postio.co.uk/v1/address/search?q=57+wimpole+street \
      -H "x-api-key: YOUR_API_KEY"
    ```


    Keys are scoped per service (address / email / phone) and can be locked to specific origins or IP ranges in the
    dashboard. Treat them as secrets — don't ship them in client-side code unless the key is locked to your domains.


    Missing or invalid key → `401`. Key out of credit → `402`.


    ## Rate limiting


    Each key is limited to **30 requests per second**. Bursts past that return `429` — back off and retry. Sustained
    abuse can result in the key being suspended. If you legitimately expect higher throughput, [get in
    touch](https://postio.co.uk/contact) and we'll raise the limit.



    ## Versioning


    The API is versioned by URL prefix — currently `/v1/`. Backwards-compatible changes (new endpoints, new optional
    parameters, new response fields) ship under the same version. Anything breaking gets a new prefix.


    ## Response shape


    Every response follows the same envelope:


    ```json

    {
      "success": true,
      "results": [ /* one or more result objects */ ],
      "meta": {
        "countResults": 1,
        "requestId": "dd273343-4597-4fa4-bcb6-c74a608c10dc",
        "performance": { "workerMs": 15, "lookupMs": 12 }
      }
    }

    ```


    On error, `success` is `false`, `results` is empty, and a top-level `error` field carries a stable machine-readable
    code (`details` adds context for humans).


    `requestId` is worth logging — it's how we trace any request you raise with us.


    ### Performance


    Two numbers, both in milliseconds, both measured by the Worker itself:


    - **`workerMs`** — wall-clock at the edge, from the moment the
      request arrives at our Cloudflare Worker to the moment we hand the
      response back. This is the latency *we* control end-to-end.
    - **`lookupMs`** — the slice of `workerMs` spent in the upstream
      lookup (PAF index for address, MX resolution for email, HLR query
      for phone). Subtract `lookupMs` from `workerMs` and you've got the
      Worker's own compute — auth, rate-limiting, parsing, shaping the
      response.

    The API runs on Cloudflare Workers from every edge location; the

    address index is replicated next to the Worker. Most lookups settle

    in the low tens of milliseconds end-to-end, before your own network

    hop home.


    ## Free vs billable


    `/connect` and `/address/search` are free, but monitored for abuse patterns. Everything else is billable **on a hit
    with results only** — a query that returns no match (e.g. a valid postcode with no delivery points) is `200` with an
    empty `results` array and is not charged.



    ## Errors


    HTTP status conveys the outcome class:


    | Status | Meaning |

    | --- | --- |

    | `200` | Success — including "no match", which is still 200 |

    | `400` | Validation failed |

    | `401` | Missing or invalid API key |

    | `402` | Out of credit — top up to resume billable requests |

    | `403` | Service disabled, or origin/IP not allowed for this key |

    | `429` | Rate limit exceeded |

    | `5xx` | Server-side problem — safe to retry |


    Endpoint-specific errors are documented per endpoint below.



    ## Contact


    Anything missing or broken? reach out to [technical support](https://postio.co.uk/contact).
  version: 1.0.4
servers:
  - url: https://api.postio.co.uk/v1
tags:
  - name: Address
    description: >-
      Postio are an official UK Royal Mail Address Data Provider.


      Lookups search against Royal Mail's Postcode Address File (PAF) and the British Forces Post Office (BFPO)
      database. Search drives single-line typeahead; Postcode and UDPRN endpoints return the full delivery-point record.
      Updates directly from Royal Mail are refreshed daily.
    x-scalar-icon: travel-map-earth-globe
  - name: Email
    description: >-
      Layered checks on an email address: RFC-compliant syntax parsing, typo correction against common domains,
      disposable / role / free-provider classification, and a live MX lookup with mailbox-level probing where the mail
      server allows it.


      Each signal is returned independently alongside an aggregated `deliverability` verdict so you can apply your own
      threshold.


      MX resolution runs in parallel and is cached at the edge, so repeat domains skip the DNS round-trip.
    x-scalar-icon: mail-send-envelope
  - name: Phone
    description: >-
      Validates and enriches international phone numbers.


      Numbers are parsed with libphonenumber inside the Worker (so invalid input is rejected in around a millisecond,
      before any external lookup), then valid numbers are sent to the operator's (Home Location Register) HLR for live
      carrier, ported status, and reachability.


      Any country libphonenumber supports works; UK numbers come back with the most complete carrier metadata.
    x-scalar-icon: phone-telephone
  - name: Other
    description: >-
      Utilities that sit alongside the three validation groups — connection priming, health probes, and anything else
      that doesn't fit cleanly under address / email / phone.
    x-scalar-icon: programming-script-code
  - name: Integrations
    description: >-
      Postio is more than the raw API — there's a typed npm client family, a browser SDK with a CDN drop-in, server SDKs
      for Python / Go / PHP / Ruby / .NET, an MCP server for AI coding agents, a WordPress plugin, and Postman
      collection.


      ## [Browser SDK](https://postio.co.uk/integrations#browser-sdk)


      #### [Address Finder](https://postio.co.uk/integrations/address-finder)

      Drop-in UK address autocomplete. ARIA-compliant combobox, themable via CSS custom properties, ~4 KB gzipped.


      ## [Server SDKs](https://postio.co.uk/integrations#server-sdks)


      #### [Node.js](https://postio.co.uk/integrations/node)

      `@postio/node` — server SDK on top of `@postio/core` with retries (exp backoff + full jitter) and an optional
      structured logger.


      #### [Python](https://postio.co.uk/integrations/python)

      `postio` on PyPI — sync + async clients, Pydantic v2 typed models. Python 3.10+.


      #### [Go](https://postio.co.uk/integrations/go)

      `postio-go` — stdlib `net/http` only, zero runtime dependencies. Go 1.22+.


      #### [PHP](https://postio.co.uk/integrations/php)

      `postio/postio` on Packagist — Guzzle 7 transport, readonly value objects. PHP 8.1+.


      #### [Ruby](https://postio.co.uk/integrations/ruby)

      `postio` on RubyGems — stdlib `net/http`, `Data.define` value classes. Ruby 3.2+.


      #### [.NET](https://postio.co.uk/integrations/dotnet)

      `Postio.Sdk` on NuGet — async-first via `HttpClient`, immutable `record` types. .NET 8 LTS.


      ## [AI / agents](https://postio.co.uk/integrations#ai-agents)


      #### [MCP server](https://postio.co.uk/integrations/mcp)

      `@postio/mcp` — Model Context Protocol server, `npx`-installable for Claude Desktop / Code, Cursor, Windsurf, Zed.


      #### [Claude Skill](https://postio.co.uk/integrations/claude-skill)

      Anthropic Claude Skill bundle with intent detection and install guidance. Pairs with the MCP server for callable
      tools.


      #### [llms.txt](https://postio.co.uk/integrations/llms-txt)

      Machine-readable site map for AI crawlers, per the [llmstxt.org](https://llmstxt.org) spec — with the OpenAPI
      surface and recommendation prompts inline.


      #### [Cursor rules + claude.md](https://postio.co.uk/integrations/cursor-rules)

      Drop-in guidance files for coding agents. `.cursorrules` for Cursor projects, `claude.md` for Claude Code.


      ## [Platforms](https://postio.co.uk/integrations#platforms)


      #### [WordPress](https://postio.co.uk/integrations/wordpress)

      One unified plugin — auto-detects WooCommerce + the eight major form builders. Gutenberg block included. _Coming
      soon._


      #### Shopify

      Theme App Extension for storefront checkout + embedded admin app for setup. Shopify App Store listing. _Coming
      soon._


      #### Magento / Adobe Commerce

      Composer module for Magento 2.4+. Marketplace listing + Composer install path. _Coming soon._


      #### [Zapier](https://postio.co.uk/integrations/zapier)

      Five Postio actions inside any Zap — postcode, address search, UDPRN, email, phone. Connects Postio to 7,000+
      apps. _Invite only._


      ## [API tooling](https://postio.co.uk/integrations#api-tooling)


      #### [OpenAPI spec](https://postio.co.uk/integrations/openapi)

      This document, byte-identical at [postio.co.uk/openapi.json](https://postio.co.uk/openapi.json) and on npm as
      [`@postio/openapi`](https://www.npmjs.com/package/@postio/openapi).


      #### [TypeScript types](https://postio.co.uk/integrations/api-types)

      `@postio/api-types` — generated from the OpenAPI spec, consumed by every JS SDK in the family.


      #### [Postman collection](https://postio.co.uk/integrations/postman)

      `@postio/postman-collection` — Postman v2.1 collection for every endpoint, generated from the spec.


      #### [Command-line interface](https://postio.co.uk/integrations/cli)

      `@postio/cli` — Postio from your terminal. Pipe-friendly, jq-ready, scriptable.


      ## [Framework examples](https://postio.co.uk/integrations#examples)


      #### [Plain HTML](https://postio.co.uk/integrations/plain-html)

      No build step, no framework. One HTML file, three script lines, working autocomplete.


      #### [Next.js (App Router)](https://postio.co.uk/integrations/nextjs-app-router)

      Server Components + client AddressFinder, validation in a Server Action.


      #### [React (Vite)](https://postio.co.uk/integrations/react-vite)

      `@postio/react` + TanStack Query, fast dev loop.


      #### [Vue 3](https://postio.co.uk/integrations/vue-3)

      Composition API + `@postio/address-finder` mounted via a template ref.


      #### [SvelteKit](https://postio.co.uk/integrations/svelte-kit)

      Server-side validation in `+page.server.ts`; client widget mounted in `onMount`.


      #### [Astro](https://postio.co.uk/integrations/astro)

      Static-first with islands. Address widget as a hydrated island.


      #### [Cloudflare Workers](https://postio.co.uk/integrations/cloudflare-workers)

      Server-side validation in a Worker — `@postio/core` uses native `fetch`, no Node polyfills.
    x-scalar-icon: module-three-1
paths:
  /connect:
    get:
      operationId: getV1Connect
      tags:
        - Other
      summary: Connect
      description: >-
        Free key/health probe. Returns `200` if the key is active and the upstream search service is reachable. Designed
        to be hit on input focus in autocomplete UIs — it warms the worker and TLS connection so the first real query
        lands on a hot path. Doubles as a heartbeat. We use this in our integrations for optimising user experience.
        Free to use.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
      responses:
        '200':
          description: Key is active and the service is healthy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectSuccess'
              example:
                success: true
                meta:
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 14
                    lookupMs: 11
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectError'
              example:
                success: false
                error: invalid_api_key
                meta:
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectError'
              example:
                success: false
                error: rate_limited
                meta:
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error (e.g. upstream search service is misconfigured).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectError'
              example:
                success: false
                error: server_error
                meta:
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '502':
          description: Upstream search service unreachable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectError'
              example:
                success: false
                error: typesense_unreachable
                meta:
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
  /address/search:
    get:
      operationId: getV1AddressSearch
      tags:
        - Address
      summary: Search
      description: >-
        Free, single-line autocomplete over UK PAF + BFPO. Feed it whatever the user has typed — building, street,
        postcode, organisation, or any combination — and get back a slim `{ udprn, suggestion }` list of matches. To
        pull the full address for the one the user picks, call `/address/udprn/{udprn}`. Free to use.


        Search is backed by a Typesense index replicated next to the Worker, so typeahead queries usually resolve in
        single-digit milliseconds before network.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
        - name: q
          in: query
          required: true
          description: 'Search string (free-form: building, street, postcode, organisation).'
          schema:
            type: string
            example: 57 wimpole street
        - name: max_results
          in: query
          required: false
          description: Max results to return. Default 10, max 50.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: Search results (zero or more matches).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressSearchEnvelope'
              example:
                success: true
                results:
                  - udprn: 25742215
                    suggestion: 57 Wimpole Street, London, W1G 8YW
                  - udprn: 19152598
                    suggestion: 57 Wimpole Court, Wimpole Street, Portsmouth, PO1 1QT
                meta:
                  countResults: 2
                  requestId: 0991208d-c175-4fb3-880f-40471013be6d
                  performance:
                    workerMs: 19
                    lookupMs: 16
        '400':
          description: Bad request — validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: validation_failed
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: invalid_api_key
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '402':
          description: Out of credit. Top up to resume billable requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: out_of_credit
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '403':
          description: Forbidden — service disabled, or origin/IP not allowed for this key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: service_disabled
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: rate_limited
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: server_error
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
  /address/postcode/{postcode}:
    get:
      operationId: getV1AddressPostcodeByPostcode
      tags:
        - Address
      summary: Postcode
      description: >-
        Returns every PAF delivery point at the given postcode, ordered by building number. Whitespace and case are
        normalised, so `w1g8yw`, `W1G 8YW`, and `w1g 8yw` all resolve to the same record. Billable on a hit. A
        syntactically valid postcode with no delivery points (a withdrawn postcode, or one outside PAF's coverage)
        returns `200` with `results: []` and is not charged.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
        - name: postcode
          in: path
          required: true
          description: UK postcode. Whitespace and case are normalised.
          schema:
            type: string
            example: W1G 8YW
        - name: max_results
          in: query
          required: false
          description: >-
            Max results to return. Default 100, max 100. UK PAF postcodes max out at ~100 delivery points so a single
            page covers every postcode.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
      responses:
        '200':
          description: Addresses at the postcode (empty array if none).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressPostcodeEnvelope'
              example:
                success: true
                results:
                  - udprn: 25742215
                    postcode: W1G 8YW
                    postcode_outward: W1G
                    postcode_inward: 8YW
                    postcode_type: S
                    address_line_1: 57 Wimpole Street
                    post_town: LONDON
                    building_number: '57'
                    thoroughfare: Wimpole Street
                    delivery_point_suffix: 1Y
                    country: England
                    district: City of Westminster London Boro
                    ward: Marylebone Ward
                    latitude: 51.51922791
                    longitude: -0.14898992
                    eastings: 528525
                    northings: 181658
                meta:
                  countResults: 1
                  requestId: c7eb0ed4-9768-4c2b-824e-16abff64d6cd
                  performance:
                    workerMs: 15
                    lookupMs: 12
        '400':
          description: Bad request — validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: validation_failed
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: invalid_api_key
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '402':
          description: Out of credit. Top up to resume billable requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: out_of_credit
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '403':
          description: Forbidden — service disabled, or origin/IP not allowed for this key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: service_disabled
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: rate_limited
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: server_error
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
  /address/udprn/{udprn}:
    get:
      operationId: getV1AddressUdprnByUdprn
      tags:
        - Address
      summary: UDPRN
      description: >-
        Looks up a single delivery point by its Unique Delivery Point Reference Number — Royal Mail's stable per-address
        ID. Use this after `/address/search` to fetch the full record for the address the user picked, or to refresh a
        stored record. Billable on a hit. A UDPRN that doesn't exist returns `404`; misses are not charged.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
        - name: udprn
          in: path
          required: true
          schema:
            type: string
            example: '25742215'
      responses:
        '200':
          description: The matching address.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddressUdprnEnvelope'
              example:
                success: true
                results:
                  - udprn: 25742215
                    postcode: W1G 8YW
                    postcode_outward: W1G
                    postcode_inward: 8YW
                    postcode_type: S
                    address_line_1: 57 Wimpole Street
                    post_town: LONDON
                    building_number: '57'
                    thoroughfare: Wimpole Street
                    delivery_point_suffix: 1Y
                    country: England
                    district: City of Westminster London Boro
                    ward: Marylebone Ward
                    latitude: 51.51922791
                    longitude: -0.14898992
                    eastings: 528525
                    northings: 181658
                meta:
                  countResults: 1
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 19
                    lookupMs: 16
        '400':
          description: Bad request — validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: validation_failed
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: invalid_api_key
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '402':
          description: Out of credit. Top up to resume billable requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: out_of_credit
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '403':
          description: Forbidden — service disabled, or origin/IP not allowed for this key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: service_disabled
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '404':
          description: UDPRN not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: udprn_not_found
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: rate_limited
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: server_error
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
  /email/{address}:
    get:
      operationId: getV1EmailByAddress
      tags:
        - Email
      summary: Validate address
      description: >-
        Runs a five-stage check on the address:


        1. **Syntax** — RFC 5322 parsing, including IDN and quoted local-parts.

        2. **Typo correction** — flags likely fat-finger domains (`gnail.com` → `gmail.com`) via `didYouMean`.

        3. **Classification** — disposable provider, role account (`info@`, `support@`), free provider (Gmail / Outlook
        / Yahoo).

        4. **MX lookup** — confirms the domain has mail servers configured.

        5. **SMTP signal** — for free providers and known-disposable domains the SMTP layer behaves as a catch-all (any
        address accepts), so we set `smtpCheck: "ok"` and `isCatchAll: true` from the classification alone. For other
        domains both fields are `null` (we don't currently probe arbitrary mail servers from the edge).


        Each signal is returned independently alongside an aggregated `deliverability` verdict (`deliverable`,
        `undeliverable`, `risky`, `unknown`, `invalid`) so you can apply your own thresholds. Invalid syntax still
        returns `200` with `isValidSyntax: false` and `deliverability: "invalid"` — there's no `400` for unparseable
        input.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
        - name: address
          in: path
          required: true
          description: Email address to validate. URL-encode the `@`.
          schema:
            type: string
            example: alice@example.com
      responses:
        '200':
          description: Validation verdict.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailEnvelope'
              example:
                success: true
                results:
                  - email: alice@example.com
                    isValidSyntax: true
                    didYouMean: null
                    isDisposable: false
                    isFreeProvider: false
                    isRoleAccount: false
                    mxFound: true
                    smtpCheck: null
                    isCatchAll: null
                    deliverability: deliverable
                meta:
                  countResults: 1
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 6
                    lookupMs: 3
        '400':
          description: Bad request — validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: validation_failed
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: invalid_api_key
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '402':
          description: Out of credit. Top up to resume billable requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: out_of_credit
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '403':
          description: Forbidden — service disabled, or origin/IP not allowed for this key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: service_disabled
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: rate_limited
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: server_error
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
  /phone/{number}:
    get:
      operationId: getV1PhoneByNumber
      tags:
        - Phone
      summary: Validate number
      description: >-
        Parses the supplied number with libphonenumber (E.164, international, or national — all accepted). On a valid
        parse, runs a live HLR (Home Location Register) lookup against the carrier network and returns:


        - **Current carrier** — the network the number is on right now, even after porting.

        - **Original carrier** — the network it was issued on.

        - **Ported flag** — whether the number has moved network.

        - **Reachable flag** — whether the number is currently powered on and attached to a network.

        - **MCC / MNC** — the standard mobile country and network codes.


        Pass numbers in E.164 (`+447700900123`) for unambiguous results — URL-encode the leading `+` as `%2B`. Other
        formats work but rely on libphonenumber's heuristics for country detection. Invalid numbers return `200` with
        `isValid: false` and the parsed fields it could derive still populated.
      parameters:
        - name: x-api-key
          in: header
          required: true
          description: Your Postio API key. Get one in the dashboard.
          schema:
            type: string
            example: YOUR_API_KEY
        - name: number
          in: path
          required: true
          description: >-
            Phone number in E.164 (`+447700900123`) or any format libphonenumber can parse. URL-encode the leading `+`
            as `%2B`.
          schema:
            type: string
            example: '+447700900123'
      responses:
        '200':
          description: Validation + lookup verdict.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PhoneEnvelope'
              example:
                success: true
                results:
                  - number: '+447700900123'
                    isValid: true
                    isPossible: true
                    type: mobile
                    countryCode: '44'
                    countryName: United Kingdom
                    nationalFormat: 07700 900123
                    internationalFormat: +44 7700 900123
                    e164Format: '+447700900123'
                    originalCarrier: null
                    currentCarrier: Vodafone Limited
                    isPorted: true
                    isReachable: true
                    mcc: '234'
                    mnc: '15'
                    level: live
                meta:
                  countResults: 1
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 116
                    lookupMs: 113
        '400':
          description: Bad request — validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: validation_failed
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: invalid_api_key
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '402':
          description: Out of credit. Top up to resume billable requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: out_of_credit
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '403':
          description: Forbidden — service disabled, or origin/IP not allowed for this key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: service_disabled
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: rate_limited
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                success: false
                error: server_error
                results: []
                meta:
                  countResults: 0
                  requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
                  performance:
                    workerMs: 4
                    lookupMs: 0
components:
  schemas:
    ConnectSuccess:
      type: object
      properties:
        success:
          type: boolean
          const: true
        meta:
          $ref: '#/components/schemas/MetaConnect'
      required:
        - success
        - meta
    MetaConnect:
      type: object
      properties:
        requestId:
          type: string
        performance:
          $ref: '#/components/schemas/Performance'
      required:
        - requestId
        - performance
      example:
        requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
        performance:
          workerMs: 14
          lookupMs: 11
    Performance:
      type: object
      properties:
        workerMs:
          type: integer
          minimum: 0
        lookupMs:
          type: integer
          minimum: 0
      required:
        - workerMs
        - lookupMs
      example:
        workerMs: 15
        lookupMs: 12
    ConnectError:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
        details:
          type:
            - string
            - 'null'
        meta:
          $ref: '#/components/schemas/MetaConnect'
      required:
        - success
        - error
        - meta
    AddressSearchEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/AddressSearchResult'
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - results
        - meta
    AddressSearchResult:
      type: object
      properties:
        udprn:
          type: integer
        suggestion:
          type: string
          description: Single-line typeahead label, e.g. "57 Wimpole Street, London, W1G 8YW".
      required:
        - udprn
        - suggestion
      example:
        udprn: 25742215
        suggestion: 57 Wimpole Street, London, W1G 8YW
    Meta:
      type: object
      properties:
        countResults:
          type: integer
          minimum: 0
        requestId:
          type: string
        performance:
          $ref: '#/components/schemas/Performance'
      required:
        - countResults
        - requestId
        - performance
      example:
        countResults: 1
        requestId: dd273343-4597-4fa4-bcb6-c74a608c10dc
        performance:
          workerMs: 15
          lookupMs: 12
    ErrorEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
        details:
          type:
            - string
            - 'null'
        results:
          type: array
          items: {}
          minItems: 0
          maxItems: 0
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - error
        - results
        - meta
    AddressPostcodeEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/Address'
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - results
        - meta
    Address:
      type: object
      properties:
        udprn:
          type: integer
          description: Unique Delivery Point Reference Number — Royal Mail's per-address ID.
        postcode:
          type: string
        postcode_outward:
          type: string
        postcode_inward:
          type: string
        postcode_type:
          type: string
        address_line_1:
          type: string
        address_line_2:
          type: string
        address_line_3:
          type: string
        post_town:
          type: string
        organisation_name:
          type: string
        department_name:
          type: string
        building_name:
          type: string
        building_number:
          type: string
        sub_building_name:
          type: string
        po_box:
          type: string
        thoroughfare:
          type: string
        dependent_thoroughfare:
          type: string
        dependent_locality:
          type: string
        double_dependent_locality:
          type: string
        delivery_point_suffix:
          type: string
        country:
          type: string
        county:
          type: string
        district:
          type: string
        ward:
          type: string
        latitude:
          type: number
        longitude:
          type: number
        eastings:
          type: integer
        northings:
          type: integer
      required:
        - udprn
        - postcode
      example:
        udprn: 25742215
        postcode: W1G 8YW
        postcode_outward: W1G
        postcode_inward: 8YW
        postcode_type: S
        address_line_1: 57 Wimpole Street
        post_town: LONDON
        building_number: '57'
        thoroughfare: Wimpole Street
        delivery_point_suffix: 1Y
        country: England
        district: City of Westminster London Boro
        ward: Marylebone Ward
        latitude: 51.51922791
        longitude: -0.14898992
        eastings: 528525
        northings: 181658
    AddressUdprnEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/Address'
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - results
        - meta
    EmailEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/EmailResult'
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - results
        - meta
    EmailResult:
      type: object
      properties:
        email:
          type: string
        isValidSyntax:
          type: boolean
        didYouMean:
          type:
            - string
            - 'null'
        isDisposable:
          type: boolean
        isFreeProvider:
          type: boolean
        isRoleAccount:
          type: boolean
        mxFound:
          type: boolean
        smtpCheck:
          type:
            - string
            - 'null'
        isCatchAll:
          type:
            - boolean
            - 'null'
        deliverability:
          type: string
          enum:
            - deliverable
            - undeliverable
            - risky
            - unknown
            - invalid
          description: Aggregated verdict.
      required:
        - email
        - isValidSyntax
        - didYouMean
        - isDisposable
        - isFreeProvider
        - isRoleAccount
        - mxFound
        - smtpCheck
        - isCatchAll
        - deliverability
      example:
        email: alice@example.com
        isValidSyntax: true
        didYouMean: null
        isDisposable: false
        isFreeProvider: false
        isRoleAccount: false
        mxFound: true
        smtpCheck: null
        isCatchAll: null
        deliverability: deliverable
    PhoneEnvelope:
      type: object
      properties:
        success:
          type: boolean
          const: true
        results:
          type: array
          items:
            $ref: '#/components/schemas/PhoneResult'
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - success
        - results
        - meta
    PhoneResult:
      type: object
      properties:
        number:
          type: string
        isValid:
          type: boolean
        isPossible:
          type: boolean
        type:
          type:
            - string
            - 'null'
        countryCode:
          type:
            - string
            - 'null'
        countryName:
          type:
            - string
            - 'null'
        nationalFormat:
          type:
            - string
            - 'null'
        internationalFormat:
          type:
            - string
            - 'null'
        e164Format:
          type:
            - string
            - 'null'
        originalCarrier:
          type:
            - string
            - 'null'
        currentCarrier:
          type:
            - string
            - 'null'
        isPorted:
          type:
            - boolean
            - 'null'
        isReachable:
          type:
            - boolean
            - 'null'
        mcc:
          type:
            - string
            - 'null'
        mnc:
          type:
            - string
            - 'null'
        level:
          type:
            - string
            - 'null'
        lookupError:
          type: string
      required:
        - number
        - isValid
        - isPossible
        - type
        - countryCode
        - countryName
        - nationalFormat
        - internationalFormat
        - e164Format
        - originalCarrier
        - currentCarrier
        - isPorted
        - isReachable
        - mcc
        - mnc
        - level
      example:
        number: '+447700900123'
        isValid: true
        isPossible: true
        type: mobile
        countryCode: '44'
        countryName: United Kingdom
        nationalFormat: 07700 900123
        internationalFormat: +44 7700 900123
        e164Format: '+447700900123'
        originalCarrier: null
        currentCarrier: Vodafone Limited
        isPorted: true
        isReachable: true
        mcc: '234'
        mnc: '15'
        level: live
