Public beta

Latency Radar API

Run regional latency measurements from code, CI, or an AI agent. Two endpoints: create a test, read its results. Every number in the response is a real observation from a specific region. The docs below say exactly how to read them.

Base URLhttps://api.latencyradar.com
OpenAPI 3.1 spec ↓

Getting access

The API is in public beta and keys are issued by hand. Email support@latencyradar.com with a sentence about what you want to measure and you will get a key back, usually the same day.

Beta keys are free and come with a daily test quota. Pricing for higher volumes will arrive after the beta; the request and response contracts documented here are the ones that will carry forward.

Quick start

Tests are asynchronous: creating one returns an id immediately, and you poll for results. A preview test finishes in a few seconds; a full test takes roughly half a minute.

1. Create a test

curl -X POST https://api.latencyradar.com/api/v1/tests \
  -H "Authorization: Bearer $LATENCY_RADAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/health",
    "method": "GET",
    "regions": ["ams", "sfo", "sin"],
    "mode": "full"
  }'

2. The API accepts it with 202

{
  "id": "tst_2f6c1a8e",
  "status": "queued",
  "created_at": "2026-08-19T14:02:11.000Z",
  "queue": { "position": 0 }
}

3. Poll until status is terminal

curl "https://api.latencyradar.com/api/v1/tests/tst_2f6c1a8e?wait_seconds=15" \
  -H "Authorization: Bearer $LATENCY_RADAR_API_KEY"

wait_seconds long-polls: the server holds the request up to 15 seconds and returns as soon as there is news, so a completed full test usually needs two or three polls instead of thirty. Terminal statuses are completed, partial, and failed.

Authentication

Every request carries your API key as a bearer token:

Authorization: Bearer lr_beta_...

A missing or unknown key returns 401 UNAUTHORIZED; a revoked key returns 403 API_KEY_REVOKED. Keys are stored hashed on our side, so a lost key is replaced, not recovered.

POST/api/v1/tests

Creates one latency test for a single public HTTP or HTTPS URL and queues it. Returns 202 with the test id.

FieldTypeDescription
urlstring, requiredPublic HTTP or HTTPS URL to measure. Targets resolving to private or internal addresses are rejected.
methodstring, requiredOne of GET, POST, PUT, DELETE, PATCH.
regionsstring[], required1–5 region codes from the table below, e.g. ["ams", "sfo"].
modestring, optionalpreview (default, one sample per region) or full (20 samples per region with percentiles).
headersobject, optionalHeaders sent to the target. Only Accept and User-Agent are allowed on the public API — anything else is rejected with INVALID_HEADERS.
bodystring, optionalRequest body for non-GET methods.

An Idempotency-Key header is accepted but not enforced during beta, so a retried create may start a duplicate test.

GET/api/v1/tests/{id}

Returns the current state of a test: its status, per-region results as they land, a cross-region summary, and a routing verdict. Pass wait_seconds (1–15) to long-poll.

Completed full-mode response (one region shown)

{
  "id": "tst_2f6c1a8e",
  "status": "completed",
  "mode": "full",
  "target": { "url": "https://api.example.com/health", "method": "GET" },
  "created_at": "2026-08-19T14:02:11.000Z",
  "started_at": "2026-08-19T14:02:12.000Z",
  "completed_at": "2026-08-19T14:02:31.000Z",
  "queue": { "position": null },
  "regions": [
    {
      "region": "ams",
      "region_name": "Amsterdam, Netherlands",
      "status": "completed",
      "sample_count": 20,
      "success_count": 20,
      "failure_count": 0,
      "success": true,
      "http_status": 200,
      "resolved_ip": "203.0.113.10",
      "dns_resolve_ms": 12.4,
      "p50_total_ms": 84.1,
      "p95_total_ms": 112.7,
      "p99_total_ms": 168.3,
      "breakdown": {
        "from": "median_sample",
        "connect_ms": 9.2,
        "tls_ms": 21.5,
        "wait_ms": 48.6,
        "download_ms": 4.8,
        "total_ms": 84.1,
        "ttfb_ms": 79.3
      },
      "tls": {
        "version": "TLSv1.3",
        "cipher": "TLS_AES_256_GCM_SHA384",
        "cert_expires_at": "2026-11-02T09:00:00.000Z",
        "cert_days_remaining": 75,
        "cert_issuer": "Let's Encrypt",
        "cert_subject": "api.example.com",
        "cert_valid": true
      },
      "error_code": null,
      "error_message": null,
      "error_sample_count": 0
    }
  ],
  "summary": {
    "regions_requested": 3,
    "regions_ok": 3,
    "regions_degraded": 0,
    "regions_failed": 0,
    "regions_pending": 0,
    "fastest_region": "ams",
    "fastest_total_ms": 84.1,
    "slowest_region": "sin",
    "slowest_total_ms": 241.9,
    "worst_p95_region": "sin",
    "worst_p95_total_ms": 289.4
  },
  "routing": {
    "distinct_ips": 1,
    "ips_by_region": {
      "ams": "203.0.113.10",
      "sfo": "203.0.113.10",
      "sin": "203.0.113.10"
    },
    "latency_spread_ms": 157.8,
    "delivery": "single_origin",
    "note": "All 3 regions reached one address with a 157.8ms spread, consistent with a single origin server that distant regions pay to reach."
  }
}

Region status values

  • completed — every sample succeeded.
  • degraded — some samples failed; percentiles are computed from the successful ones and failure_count / error_code say what went wrong.
  • failed — zero samples succeeded; the target could not be measured from this region.
  • unavailable — the measurement worker itself was unreachable; nothing about the target was measured.

Regions

CodeLocation
amsAmsterdam, Netherlands
sfoSan Francisco, United States
yulMontreal, Canada
sinSingapore
tyoTokyo, Japan

Preview vs full mode

preview

One sample per region. Fast, and enough to answer "does this endpoint work from Singapore, and roughly how slow is it". Percentile fields are null — one sample cannot produce a percentile — and the region's authoritative total is breakdown.total_ms.

full

20 samples per region. Produces p50_total_ms, p95_total_ms, and p99_total_ms; the p50 is the region's authoritative total. Every sample opens its own connection and performs a full TLS handshake, so no sample reports a time another sample paid for.

Reading the numbers

  • Percentiles are real observations. They use nearest-rank selection with no interpolation, so every percentile is a value that was actually measured. With 20 samples, p99_total_ms is the maximum — a single slow sample becomes the p99.
  • Percentiles exist for the total only. "Where does the time go" is answered by breakdown instead: the four phases of one real request (connect_ms, tls_ms, wait_ms, download_ms), which sum to that request's total_ms exactly. breakdown.from names which request it was — the median sample in full mode, so breakdown.total_ms equals p50_total_ms by construction.
  • ttfb_ms is comparable with browser tooling. It is the cumulative connect + tls + wait, the sense Chrome DevTools and WebPageTest use, and sits outside the additive set — adding it to the phases would double-count.
  • DNS is reported separately. Workers pre-resolve the hostname (for SSRF safety) and pin the request to the resolved addresses, so no total includes resolution. dns_resolve_ms reports the cold-cache resolution itself; a first-time visitor pays roughly dns_resolve_ms + p50_total_ms.
  • Failed samples poison no statistic. They 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.
  • summary never averages. Every value in it is copied verbatim from a specific region, so it can always be traced back to the regions array. Under partial failure it names the broken region instead of reporting a healthy figure.
  • routing is the one conclusion. It pairs the number of distinct addresses the regions reached with the latency spread between them to give a hedged delivery verdict — single origin, anycast/edge, or geo-distributed. Its note is one sentence written to be quoted verbatim.

Errors

Errors are stable and machine-readable: { "error": { "code", "message", "request_id", "retry_after_seconds" } }.

CodeStatusMeaning
UNAUTHORIZED401Missing or unknown API key.
API_KEY_REVOKED403The key exists but has been revoked.
QUOTA_EXCEEDED429The key's daily test quota is used up. retry_after_seconds says when it resets.
RATE_LIMITED429Too many requests in a short window. Slow down and retry.
TARGET_NOT_ALLOWED400The target resolves to a private or internal address, or is otherwise blocked.
INVALID_REQUEST400The request body failed validation. The message says which field.
INVALID_HEADERS400A target header other than Accept or User-Agent was supplied.
CAPACITY_EXCEEDED429Too many tests are running concurrently. Wait for them to finish.
TEST_NOT_FOUND404No test with that id is visible to this key.
WORKER_UNAVAILABLE502A measurement region could not be reached.
INTERNAL_ERROR500Something failed on our side. Include request_id when reporting it.

Limits

  • Beta keys include a daily test quota (default 25 tests/day).
  • 1–5 regions per test; one URL per test.
  • Target headers are limited to Accept and User-Agent, so the public API cannot replay credentials against third-party endpoints.
  • Targets resolving to private, internal, or link-local addresses are rejected.
  • Concurrent tests are capped, and per-target rate limits protect measured hosts. This is a diagnostic tool, not a load-testing platform.
  • wait_seconds long-polling is capped at 15 seconds per request.

MCP server

Latency Radar ships an MCP server so agents like Claude can run measurements as a tool. It wraps this API with two tools — run a test and wait for results, or fetch an existing test by id — and authenticates with your own API key. During the beta it is distributed on request: mention MCP in your API key email and you will get the package alongside the key.

{
  "mcpServers": {
    "latency-radar": {
      "command": "node",
      "args": ["/path/to/latency-radar-mcp/src/server.js"],
      "env": {
        "LATENCY_RADAR_API_KEY": "lr_beta_..."
      }
    }
  }
}

The server needs LATENCY_RADAR_API_KEY set; it embeds no shared credentials.