openapi: 3.1.0
info:
  title: Latency Radar Public API
  version: 0.5.0
  description: |
    Public beta API for running regional HTTP latency measurements through
    Latency Radar. The API is intentionally small so MCP wrappers and agents can
    choose tools reliably.

    ## How to read the numbers

    - Percentiles use nearest-rank selection with no interpolation, so every
      percentile is a value that was actually observed. In full mode (20 samples
      per region) `p95_total_ms` is the 19th ordered sample and `p99_total_ms`
      is the maximum, which is why a single slow sample becomes the p99.
    - Percentiles are reported for the **total only**. They answer "how slow is
      this, and how bad is the tail", which is a question someone acts on. There
      is deliberately no `p95_connect_ms` or `p50_tls_ms`: a per-phase percentile
      is drawn from a different request than its neighbours, so a set of them sums
      to nothing, and with 20 samples it is the 19th observation wearing a
      statistic's name. It can even land below the p50 of the same phase, which
      looks like a bug and reads like one.
    - "Where does the time go" is answered by `regions[].breakdown` instead: the
      four phases of **one real request**, plus that request's own `total_ms`,
      which they sum to exactly - the worker rounds the cumulative phase
      boundaries and differences them, rather than rounding each phase on its own.
      `breakdown.from` names which request it was: `median_sample` in full mode
      (the sample at nearest-rank index `ceil(0.5 * n) - 1` when the successful
      samples are sorted by total - the 10th of 20 - so `breakdown.total_ms`
      equals `p50_total_ms` by construction), `single_sample` in preview.
    - The four phases are non-cumulative deltas: `connect_ms`, `tls_ms`,
      `wait_ms` (server think time plus one round trip, after the handshake
      completed) and `download_ms`. `breakdown.ttfb_ms` is the one cumulative
      figure: `connect_ms + tls_ms + wait_ms`, i.e. time to first byte in the
      sense Chrome DevTools and WebPageTest use it, so the two are directly
      comparable. It is derived, not measured, and sits outside the additive set -
      adding it to the phases would double-count. The discrete wait phase was
      called `ttfb_ms` before 0.5.0, which invited exactly that comparison against
      a number three times larger.
    - Every sample opens its own connection and performs a full TLS handshake.
      Measurement sockets are unpooled and TLS session caching is off, so no
      sample reports a `connect_ms` or `tls_ms` that a previous sample paid for,
      and the certificate is readable on every handshake.
    - Failed samples are excluded from every percentile. A region with 19 fast
      samples and one timeout reports clean percentiles plus
      `status: degraded` and `failure_count: 1`. Read the counts before the
      percentiles.
    - There is no cross-region aggregate anywhere in the response. Every value
      in `summary` is a value that also appears on a specific region.
    - Each region has exactly one authoritative total: `p50_total_ms` in full
      mode, `breakdown.total_ms` in preview. `summary` ranks regions on it. There
      is deliberately no second, mode-independent total field carrying the same
      value under another name at the top level of a region.
    - `routing` is the one field that states a conclusion rather than a
      measurement. It pairs the number of distinct addresses the regions reached
      with the latency spread between them, because neither alone is decisive: a
      single address fits both one origin server and a global anycast network,
      and only the spread separates them. Its `note` is a single sentence written
      to be quoted verbatim.

    - No in-request DNS phase is reported. Workers pre-resolve the hostname and
      pin the request's lookup to the resolved address set for SSRF safety, so
      the measured request performs no resolution and an in-request DNS timing
      would be structurally zero. `dns_resolve_ms` reports the pre-resolution
      itself instead, and it is **not** included in any total or percentile in
      this response. A first-time visitor with a cold cache pays roughly
      `dns_resolve_ms + p50_total_ms`; a visitor whose resolver is warm pays
      about `p50_total_ms`. The same exclusion applies to `breakdown.ttfb_ms`: a
      browser's TTFB includes resolution, so the comparable first-visit figure is
      `dns_resolve_ms + breakdown.ttfb_ms`. The window before the socket connects is counted in
      `connect`, because with the lookup pinned it is socket setup rather than
      resolution.

    ## Changelog

    ### 0.5.0 — `breakdown.ttfb_ms` renamed to `wait_ms`, cumulative `ttfb_ms` added

    `breakdown.ttfb_ms` held a discrete post-handshake wait, but TTFB
    conventionally means time from the start of the request, so the field read
    roughly 3x low against Chrome DevTools or WebPageTest for the same request.
    The discrete phase is now `breakdown.wait_ms`, and `breakdown.ttfb_ms` is the
    conventional cumulative value: `connect_ms + tls_ms + wait_ms`, derived and
    excluded from the additive set. `connect_ms + tls_ms + wait_ms + download_ms`
    still sums to `total_ms` exactly. Neither figure includes DNS resolution; a
    first-time visitor pays `dns_resolve_ms` on top of `ttfb_ms`.

    ### 0.4.0 — phase fields replaced by `breakdown`, certificate regression fixed

    Removed: the ten `p50_*` / `p95_*` phase fields and the five `sample_*`
    fields. Added: `regions[].breakdown`, one nested object in both modes, with
    `from`, `connect_ms`, `tls_ms`, `ttfb_ms`, `download_ms` and `total_ms`.

    Fifteen phase fields become six. The `p95_*` phase fields were the breakdown
    of the single sample whose *total* sat at p95, which meant `p95_connect_ms`
    could come in below `p50_connect_ms` - impossible for a real percentile pair,
    and an accurate signal that the name was wrong for the value. Rather than
    convert them into independent per-phase percentiles, which no one optimises
    against and which sum to no total in the response, the phase breakdown is now
    stated once, for one identified request, with its provenance in the payload.

    Preview's authoritative total moved from `sample_total_ms` to
    `breakdown.total_ms`; full mode's is still `p50_total_ms`, and
    `breakdown.total_ms` equals it by construction.

    Also fixed, in the worker rather than the contract: `tls.cert_expires_at`,
    `cert_days_remaining`, `cert_issuer` and `cert_subject` had gone null on
    targets measured more than once. The agent cached the TLS session, later
    handshakes resumed it, and a resumed handshake carries no certificate -
    `getPeerCertificate()` returns an empty object while protocol and cipher still
    populate. Measurement sockets now disable session caching and connection
    pooling, so every sample performs a full handshake. `cert_valid` also no
    longer reports `true` when no certificate was read: with nothing behind it, it
    is `null`.

    ### 0.3.0 — `routing` added

    Additive only: no removals, no renames, nothing to migrate. A `routing` block
    at the top level reports how many distinct addresses the requested regions
    reached, which address each one reached, the latency spread between the
    fastest and slowest region, and a one-sentence verdict on delivery topology.

    Every input is a value already present elsewhere in the same response
    (`regions[].resolved_ip` and each region's authoritative total), so the block
    adds no measurement and can be checked by hand.

    ### 0.2.0 — response contract corrected in place

    v1 was corrected in place rather than versioned, because no external
    consumer existed at the time of the change. There is no compatibility shim
    and no deprecation window.

    Removed:

    - `aggregate` — it was a median of per-region medians labelled with a pooled
      sample count, described no real user, and reported healthy figures while a
      region was timing out. Replaced by `summary`.
    - `dns_ms` and `dns_p95_ms` — always zero by construction (see above).
      Replaced by `dns_resolve_ms`. The sub-millisecond window they described is
      now counted in the connect phase, so the phases account for the whole
      total with nothing left over.
    - `total_ms` — it duplicated the region's authoritative total exactly in both
      modes. Two names for one number invite being read as two different
      measurements, and once the phases close out against a specific total there
      has to be a single authoritative one. Use `p50_total_ms` in full mode and
      `breakdown.total_ms` in preview (0.4.0; in 0.2.0 this was
      `sample_total_ms`).
    - The bare `connect_ms` / `tls_ms` / `ttfb_ms` / `download_ms` phase fields,
      which read as one request's breakdown. Renamed by statistic prefix to
      `p50_*` / `p95_*` (full mode) and `sample_*` (preview mode), all of which
      0.4.0 then replaced with the nested `breakdown` object.
    - `connect_p95_ms` / `tls_p95_ms` / `ttfb_p95_ms` / `download_p95_ms`,
      renamed to `p95_connect_ms` etc. and taken from the p95 sample rather than
      being independent per-phase percentiles. Removed entirely in 0.4.0.

    Added:

    - `summary` with per-region-traceable values and region state counts.
    - `region_name`, `resolved_ip`, `dns_resolve_ms`, `tls`.
    - `success_count`, `failure_count`, `error_sample_count`, and the
      `degraded` region status, so a region with mixed sample outcomes can no
      longer report `status: failed` together with `success: true` and a full
      set of percentiles.
    - Phase fields now sum to their total exactly rather than within a
      millisecond or two of it, which matters on fast targets where a 1ms
      rounding gap is a visible fraction of an 11ms total.
servers:
  - url: https://api.latencyradar.com
security:
  - bearerApiKey: []
paths:
  /api/v1/tests:
    post:
      operationId: createLatencyTest
      summary: Create a regional latency test
      description: >
        Creates one asynchronous latency test for a single HTTP or HTTPS URL.
        Use this when you need latency measurements from one or more Latency
        Radar regions. Creation always returns a job id; poll the test endpoint
        for results. The Idempotency-Key header is accepted during beta but is
        not enforced yet, so retries may create duplicate tests.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
          description: Accepted but ignored in beta; reserved for future retry deduplication.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTestRequest"
      responses:
        "202":
          description: Test accepted and queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTestResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /api/v1/tests/{id}:
    get:
      operationId: getLatencyTest
      summary: Get latency test status or results
      description: >
        Returns the current state of a latency test. When wait_seconds is
        provided, the server may hold the request briefly before returning the
        latest state. This is useful for MCP tools and batch clients because it
        reduces polling chatter while preserving the asynchronous API contract.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Test id returned by createLatencyTest.
        - name: wait_seconds
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 15
          description: Long-poll for up to this many seconds, capped at 15.
      responses:
        "200":
          description: Current test state and any available results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Test"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      description: Admin-issued Latency Radar beta API key.
  responses:
    Error:
      description: Stable machine-readable error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    CreateTestRequest:
      type: object
      additionalProperties: false
      required:
        - url
        - method
        - regions
      properties:
        url:
          type: string
          format: uri
          description: Public HTTP or HTTPS URL to measure.
        method:
          type: string
          enum: [GET, POST, PUT, DELETE, PATCH]
        regions:
          type: array
          minItems: 1
          maxItems: 5
          items:
            type: string
            enum: [ams, sfo, yul, sin, tyo]
        mode:
          type: string
          enum: [preview, full]
          default: preview
          description: Preview is one sample per region; full provides percentile fields.
        headers:
          type: object
          additionalProperties:
            type: string
          description: Only Accept and User-Agent are allowed in the public API.
        body:
          type: string
    CreateTestResponse:
      type: object
      required: [id, status, created_at, queue]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [queued]
        created_at:
          type: [string, "null"]
          format: date-time
        queue:
          type: object
          required: [position]
          properties:
            position:
              type: [integer, "null"]
    Test:
      type: object
      required: [id, status, mode, target, created_at, started_at, completed_at, queue, regions, summary, routing]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [pending, queued, running, completed, failed, partial]
          description: >
            partial means at least one region produced measurements and at least
            one region is degraded, failed, or unavailable. failed means no
            region produced any measurement. Terminal values are completed,
            failed, and partial.
        mode:
          type: string
          enum: [preview, full]
        target:
          type: object
          required: [url, method]
          properties:
            url:
              type: [string, "null"]
            method:
              type: string
        created_at:
          type: [string, "null"]
          format: date-time
        started_at:
          type: [string, "null"]
          format: date-time
        completed_at:
          type: [string, "null"]
          format: date-time
        queue:
          type: object
          required: [position]
          properties:
            position:
              type: [integer, "null"]
        regions:
          type: array
          items:
            $ref: "#/components/schemas/RegionResult"
        summary:
          $ref: "#/components/schemas/TestSummary"
        routing:
          $ref: "#/components/schemas/Routing"
    RegionResult:
      type: object
      required:
        - region
        - region_name
        - status
        - sample_count
        - success_count
        - failure_count
        - success
        - http_status
        - resolved_ip
        - dns_resolve_ms
        - p50_total_ms
        - p95_total_ms
        - p99_total_ms
        - breakdown
        - tls
        - error_code
        - error_message
        - error_sample_count
      properties:
        region:
          type: string
          description: Region code.
          enum: [ams, sfo, yul, sin, tyo]
        region_name:
          type: [string, "null"]
          description: >
            Human-readable location, for example "Montreal, Canada". Null only
            for a region code this deployment does not know.
        status:
          type: string
          enum: [queued, running, completed, degraded, failed, unavailable]
          description: >
            completed means every sample succeeded (failure_count is 0).
            degraded means some samples succeeded and some failed; percentile
            fields are present and are computed from the successful samples
            only. failed means zero samples succeeded; every percentile,
            breakdown and http_status field is null. unavailable means the worker itself
            could not be reached, so nothing about the target was measured -
            distinct from failed, which describes the target. queued and running
            are non-terminal.
        sample_count:
          type: integer
          description: >
            Samples attempted in this region. Equals success_count +
            failure_count. Full mode attempts 20; preview mode attempts 1, which
            is why preview leaves every percentile field null. A batch can stop
            early (for example on an oversized response), so this can be lower
            than the mode's nominal sample count.
        success_count:
          type: integer
          description: Samples that completed and produced a timing.
        failure_count:
          type: integer
          description: >
            Samples that failed. These contribute to no percentile; see
            error_code and error_sample_count for what went wrong.
        success:
          type: [boolean, "null"]
          description: >
            Coarse: true when success_count > 0, so it is true for a degraded
            region as well. Kept for backward compatibility. Use status and the
            sample counts instead. Null while the region is queued or running.
        http_status:
          type: [integer, "null"]
          description: >
            HTTP status from the first successful sample. Null when status is
            failed or unavailable, so it is never a stale value from before the
            failure.
        resolved_ip:
          type: [string, "null"]
          description: >
            The address the region actually connected to, taken from the socket
            rather than from the resolver. Reveals GeoDNS and anycast routing,
            which explains regional latency differences. Null for a region that
            never connected, and for worker builds that predate this field.
        dns_resolve_ms:
          type: [number, "null"]
          description: >
            Hostname resolution time, measured once per region during the SSRF
            safety pre-resolution that runs before the first sample, with a cold
            resolver cache. This is not a per-sample average: later samples in
            the batch hit the resolver cache, and the measured requests do no
            resolution at all because their lookup is pinned to the
            pre-resolved address set. Null for worker builds that predate this
            field.
        p50_total_ms:
          type: [number, "null"]
          description: >
            The region's authoritative total in full mode, and what summary ranks
            on. Null in preview mode because one sample cannot produce a
            percentile.
        p95_total_ms:
          type: [number, "null"]
          description: Null in preview mode because one sample cannot produce a percentile.
        p99_total_ms:
          type: [number, "null"]
          description: Null in preview mode because one sample cannot produce a percentile.
        breakdown:
          oneOf:
            - $ref: "#/components/schemas/PhaseBreakdown"
            - type: "null"
          description: >
            Where the time went, for one real request in this region. Null when
            the region produced no successful sample, and for worker builds that
            send no representative sample.
        tls:
          oneOf:
            - $ref: "#/components/schemas/TlsInfo"
            - type: "null"
          description: >
            Negotiated TLS parameters and leaf certificate for the first hop, from
            the handshake the measurement already performs. Null for plain HTTP
            targets, for regions that never completed a handshake, and for worker
            builds that predate this field.
        error_code:
          type: [string, "null"]
          description: >
            The failure that affected the most samples in this region. Present on
            a degraded region as well as a failed one.
        error_message:
          type: [string, "null"]
        error_sample_count:
          type: [integer, "null"]
          description: >
            How many samples hit error_code. 0 when there was no error. Null when
            the region came from a worker build that does not report the per-code
            breakdown.
    PhaseBreakdown:
      type: object
      description: >
        Where the time went, for one real request. Not four per-phase
        percentiles: those come from up to four different requests, so they sum
        to no total the response reports, and with 20 samples a per-phase p95 is
        the 19th observation wearing a statistic's name - it can even land below
        the p50 of the same phase. The four phases here - connect, tls, wait,
        download - are the actual phases of one HTTP call and sum to total_ms
        exactly, so a stacked bar matches its own label. ttfb_ms is derived from
        the first three and sits outside that sum. Percentiles are reported for
        the total only, where they answer a question someone acts on.
      required:
        - from
        - connect_ms
        - tls_ms
        - wait_ms
        - download_ms
        - total_ms
        - ttfb_ms
      properties:
        from:
          type: string
          enum: [median_sample, single_sample]
          description: >
            Which request the breakdown describes. median_sample in full mode:
            the successful samples are sorted by total and the sample at
            nearest-rank index ceil(0.5 * n) - 1 is taken, the 10th of 20, which
            is the same selection p50_total_ms makes - so total_ms equals
            p50_total_ms by construction. single_sample in preview mode, where
            the region ran one sample and that sample is the breakdown.
        connect_ms:
          type: [number, "null"]
          description: >
            Connect time, measured from the start of the request, so it includes
            socket setup. The measured request performs no resolution: the lookup
            is pinned to the pre-resolved address set, and dns_resolve_ms reports
            resolution separately.
        tls_ms:
          type: [number, "null"]
          description: >
            TLS handshake time. Always a full handshake: measurement sockets
            disable TLS session caching, so no sample resumes a session another
            sample established, and the phase is comparable across samples and
            regions. 0 for plain HTTP targets.
        wait_ms:
          type: [number, "null"]
          description: >
            Wait between the completed handshake and the first response byte:
            server think time plus one round trip. A delta, not time measured from
            the start of the request - see ttfb_ms for that. Called ttfb_ms before
            0.5.0, which collided with the established meaning of the term.
        download_ms:
          type: [number, "null"]
          description: Response body transfer time.
        total_ms:
          type: [number, "null"]
          description: >
            This request's total, which connect_ms + tls_ms + wait_ms +
            download_ms sum to exactly - the worker rounds the cumulative phase
            boundaries and differences them, rather than rounding each phase on
            its own. In full mode this equals p50_total_ms; in preview mode it is
            the region's authoritative total and what summary ranks on. Excludes
            DNS resolution, which is reported separately as dns_resolve_ms.
        ttfb_ms:
          type: [number, "null"]
          description: >
            Time to first byte in the conventional sense, measured from the start
            of the request: connect_ms + tls_ms + wait_ms. Derived rather than
            measured, and deliberately outside the additive set, so it is directly
            comparable with what Chrome DevTools and WebPageTest label TTFB while
            the four phases still close out against total_ms. Equals total_ms
            minus download_ms. Null if any phase it needs is unknown - never a
            partial sum. Excludes DNS resolution, like every figure in this
            object: a first-time visitor with a cold resolver cache pays
            dns_resolve_ms on top.
    TlsInfo:
      type: object
      required:
        - version
        - cipher
        - cert_expires_at
        - cert_days_remaining
        - cert_issuer
        - cert_subject
        - cert_valid
      properties:
        version:
          type: [string, "null"]
          description: Negotiated protocol, for example TLSv1.3.
        cipher:
          type: [string, "null"]
          description: Negotiated cipher suite name.
        cert_expires_at:
          type: [string, "null"]
          format: date-time
          description: Leaf certificate notAfter.
        cert_days_remaining:
          type: [integer, "null"]
          description: >
            Whole days from measurement time to cert_expires_at, computed when
            the measurement ran rather than when the result is read, so re-reading
            a stored test returns the same number. Can be negative in principle.
        cert_issuer:
          type: [string, "null"]
          description: Issuer organisation, falling back to issuer common name.
        cert_subject:
          type: [string, "null"]
          description: Leaf certificate subject common name.
        cert_valid:
          type: [boolean, "null"]
          description: >
            Whether the chain and hostname verified. Note that certificate
            verification is enforced during measurement, so an already-expired or
            mismatched certificate fails the request outright and surfaces as a
            failed region with error_code CERT_HAS_EXPIRED or
            ERR_TLS_CERT_ALTNAME_INVALID and tls null, rather than as
            cert_valid false. The useful signal here is cert_days_remaining
            before expiry, not detection after it.
    TestSummary:
      type: object
      description: >
        One-line verdict for agent and single-row export use. Every value is
        taken from a specific region and appears in the regions array; nothing
        here is averaged across regions and no percentile is computed from other
        percentiles. Under partial failure this block names the broken region
        instead of reporting a healthy figure.
      required:
        - regions_requested
        - regions_ok
        - regions_degraded
        - regions_failed
        - regions_pending
        - fastest_region
        - fastest_total_ms
        - slowest_region
        - slowest_total_ms
        - worst_p95_region
        - worst_p95_total_ms
      properties:
        regions_requested:
          type: integer
        regions_ok:
          type: integer
          description: Regions with status completed.
        regions_degraded:
          type: integer
          description: Regions with status degraded.
        regions_failed:
          type: integer
          description: Regions with status failed or unavailable.
        regions_pending:
          type: integer
          description: >
            Regions still queued or running. The four state counts always sum to
            regions_requested, so they stay consistent while polling.
        fastest_region:
          type: [string, "null"]
          description: >
            Region code with the lowest authoritative total among regions that
            produced measurements - p50_total_ms in full mode,
            breakdown.total_ms in
            preview. Null when no region has data.
        fastest_total_ms:
          type: [number, "null"]
          description: >
            That region's own total, copied verbatim, so it can always be found
            in the regions array.
        slowest_region:
          type: [string, "null"]
          description: Region code with the highest authoritative total among regions that produced measurements.
        slowest_total_ms:
          type: [number, "null"]
        worst_p95_region:
          type: [string, "null"]
          description: >
            Region code with the highest p95_total_ms. Null in preview mode,
            which has no percentiles.
        worst_p95_total_ms:
          type: [number, "null"]
    Routing:
      type: object
      description: >
        Delivery topology, derived entirely from values that appear elsewhere in
        this response: the address each region reached and each region's
        authoritative total. Nothing here is measured separately, so every field
        can be checked by hand.


        The verdict pairs two inputs because neither is decisive alone. One
        address fits both a single origin server and a global anycast network -
        Cloudflare and Fastly both present one address worldwide - and what
        separates them is that anycast is fast everywhere, so a wide spread rules
        it out. The threshold is 150ms, fixed and not configurable; a spread of
        exactly 150ms counts as wide.


        Known limits, both of which produce a null verdict or a hedged note
        rather than a wrong answer. A target whose regions resolve over different
        address families cannot be compared, so delivery is null. Several
        addresses within one datacenter (DNS round-robin) are indistinguishable
        from real geographic distribution here, which is why the geo_distributed
        note says "consistent with" rather than asserting it. And a genuinely
        anycast target with one region far from any edge can exceed the threshold
        and read as single_origin, which is why every note is hedged.


        No vendor identification is attempted, now or later: mapping addresses to
        CDN ranges is wrong often enough to mislead.
      required:
        - distinct_ips
        - ips_by_region
        - latency_spread_ms
        - delivery
        - note
      properties:
        distinct_ips:
          type: integer
          description: >
            Count of distinct addresses across the regions that resolved one.
            Zero for a test that has not run, and for results measured before
            per-region IP capture existed. A region that resolved an address but
            failed every sample still counts here: the address is a DNS fact
            independent of the request outcome.
        ips_by_region:
          type: object
          additionalProperties:
            type: [string, "null"]
          description: >
            One entry per requested region, null when that region never resolved
            an address. Every requested region is present, so this can be diffed
            against the regions array without a join.
        latency_spread_ms:
          type: [number, "null"]
          description: >
            slowest_total_ms minus fastest_total_ms from the summary block - the
            difference between two observed region values, and the only
            cross-region arithmetic in this response. Null when fewer than two
            regions produced measurements. In preview mode it derives from one
            sample per region.
        delivery:
          type: [string, "null"]
          enum: [single_origin, anycast_or_edge, geo_distributed, geo_distributed_uneven, null]
          description: >
            single_origin - one address, spread >= 150ms: traffic appears to reach
            one server, and distant regions pay for it.
            anycast_or_edge - one address, spread < 150ms: fast everywhere, so an
            anycast or edge network.
            geo_distributed - several addresses, spread < 150ms: consistent with
            edge or geo-distributed delivery.
            geo_distributed_uneven - several addresses, spread >= 150ms: several
            locations, but some regions are still served from far away.
            Null when the verdict cannot be supported: fewer than two regions with
            data, no resolved addresses, or mixed address families. The note
            always says which.
        note:
          type: string
          description: >
            One sentence stating the two facts and then a hedged conclusion,
            written to be quoted verbatim in an MCP response, a CSV cell or a
            client report. Descriptive, never prescriptive - it reports what the
            measurement shows and never recommends a change.
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - UNAUTHORIZED
                - API_KEY_REVOKED
                - QUOTA_EXCEEDED
                - RATE_LIMITED
                - TARGET_NOT_ALLOWED
                - INVALID_REQUEST
                - INVALID_HEADERS
                - CAPACITY_EXCEEDED
                - TEST_NOT_FOUND
                - WORKER_UNAVAILABLE
                - INTERNAL_ERROR
            message:
              type: string
            request_id:
              type: [string, "null"]
            retry_after_seconds:
              type: [integer, "null"]
