Skip to content
speedtest.doctor
Developer documentation

API Documentation

REST API reference for api.speedtest.doctor — speed test, ping, DNS, SSL, HTTP headers, port scan, WHOIS and full connection diagnosis. JSON in, JSON out.

Getting started

The API is REST over HTTPS. Requests and responses are JSON. There are three things to know:

1 · Get a key

Create one in your dashboard. Keys look like sk_live_… and are shown once.

2 · Base URL

All endpoints live under https://api.speedtest.doctor.

3 · Auth header

Send Authorization: Bearer YOUR_API_KEY on every call.

Your first call (health, no key required) bash
# bash — the URL has no '?' so quoting is optional, but harmless
curl -sS https://api.speedtest.doctor/health \
  -H "Authorization: Bearer YOUR_API_KEY"

Authentication

Every /v1/* tool and diagnose endpoint requires a Bearer key. Pass it in the Authorization header.

zsh / bash tip: any URL containing ?, & or * must be wrapped in quotes, otherwise zsh treats them as glob patterns and the request fails. When in doubt, always quote the URL.

Quote URLs with query strings bash
# zsh / bash — ALWAYS quote URLs that contain '?', '&' or '*'
# (zsh treats them as glob characters and the request will fail otherwise).
curl -sS -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.speedtest.doctor/down?bytes=0" \
  -o /dev/null -w "HTTP %{http_code}\n"
See the HTTP status with -w bash
# Append the HTTP status to any call so you can see 200 / 401 / 429 at a glance:
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/ssl" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}' \
  -w "\nHTTP %{http_code}\n"

Endpoints

All tool endpoints are POST with a JSON body and require a Bearer key. The target is SSRF-guarded: private, loopback and reserved addresses are rejected with 400 invalid_target.

POST /v1/tools/ssl

SSL / TLS check

Open a TLS connection to the target and return the negotiated handshake plus the parsed leaf certificate (subject, issuer, validity, SHA-1 fingerprint).

Param Type Required Default Description
target string yes Host to check (hostname or IP). Alias: host.
port integer no 443 TLS port, 1–65535.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/ssl" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}'
Response (200) json
{
  "tool": "ssl",
  "target": "example.com",
  "port": 443,
  "timed_out": false,
  "duration_ms": 564,
  "handshake": "CONNECTION ESTABLISHED\nProtocol version: TLSv1.3\nCiphersuite: TLS_AES_256_GCM_SHA384\nPeer certificate: CN = example.com\nVerification: OK\nDONE",
  "certificate": {
    "subject": "CN = example.com",
    "issuer": "C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3",
    "not_before": "May 31 21:39:12 2026 GMT",
    "not_after": "Aug 29 21:41:26 2026 GMT",
    "fingerprint_sha1": "E7:F6:0D:1A:FE:CD:FF:DF:16:4B:74:79:38:6B:BE:67:CD:D8:E5:1E"
  }
}
POST /v1/tools/ping

Ping / latency

ICMP ping the target and return a parsed latency summary (min/avg/max/mdev and packet loss) plus the raw output.

Param Type Required Default Description
target string yes Host to ping (hostname or IP). Alias: host.
count integer no 4 Number of echo requests, 1–10.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/ping" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com","count":4}'
Response (200) json
{
  "tool": "ping",
  "target": "example.com",
  "timed_out": false,
  "duration_ms": 1047,
  "summary": {
    "packet_loss_pct": 0,
    "rtt_min_ms": 3.558,
    "rtt_avg_ms": 3.706,
    "rtt_max_ms": 3.855,
    "rtt_mdev_ms": 0.148
  },
  "output": "PING example.com (104.20.23.154) 56(84) bytes of data.\n..."
}
POST /v1/tools/dns

DNS benchmark

Resolve the target against several public resolvers (Cloudflare, Google, Quad9) plus the system resolver, and report each one's response time and addresses.

Param Type Required Default Description
target string yes Hostname to resolve. Alias: host.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/dns" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}'
Response (200) json
{
  "tool": "dns",
  "target": "example.com",
  "fastest_ms": 2,
  "resolvers": {
    "cloudflare": { "ms": 11, "addresses": ["172.66.147.243", "104.20.23.154"] },
    "google":     { "ms": 12, "addresses": ["104.20.23.154", "172.66.147.243"] },
    "quad9":      { "ms": 15, "addresses": ["172.66.147.243", "104.20.23.154"] },
    "system":     { "ms": 2,  "addresses": ["172.66.147.243", "104.20.23.154"] }
  }
}
POST /v1/tools/headers

HTTP headers

Issue a HEAD request to the target and return the response status and headers (redirects are not followed).

Param Type Required Default Description
target string yes Host to fetch (hostname or IP). Alias: host.
scheme string no https "https" or "http".
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/headers" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}'
Response (200) json
{
  "tool": "headers",
  "target": "https://example.com/",
  "status": 200,
  "duration_ms": 205,
  "headers": {
    "content-type": "text/html",
    "server": "cloudflare",
    "cf-cache-status": "HIT",
    "content-encoding": "br"
  },
  "error": null
}
POST /v1/tools/portscan

TCP port scan

Check whether a set of well-known TCP ports accept connections on the target. Only an allow-list of common service ports may be scanned.

Param Type Required Default Description
target string yes Host to scan (hostname or IP). Alias: host.
ports integer[] yes 1–16 ports from the allow-list (see note).

Allowed ports: 20, 21, 22, 23, 25, 53, 80, 110, 143, 443, 465, 587, 993, 995, 3306, 3389, 5432, 6379, 8080, 8443.

Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/portscan" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com","ports":[80,443,22]}'
Response (200) json
{
  "tool": "portscan",
  "target": "example.com",
  "resolved_ip": "104.20.23.154",
  "results": [
    { "port": 80,  "open": true },
    { "port": 443, "open": true },
    { "port": 22,  "open": false }
  ]
}
POST /v1/tools/whois

WHOIS lookup

Run a WHOIS query for the target domain/IP and return the raw registry output (truncated to a safe size).

Param Type Required Default Description
target string yes Domain or IP to look up. Alias: host.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/whois" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}'
Response (200) json
{
  "tool": "whois",
  "target": "example.com",
  "timed_out": false,
  "duration_ms": 412,
  "output": "Domain Name: EXAMPLE.COM\nRegistry Domain ID: ...\nRegistrar: ...\n..."
}
POST /v1/tools/traceroute

Traceroute

Trace the network path to the target and return the raw hop list. This call can take up to ~20s.

Param Type Required Default Description
target string yes Host to trace (hostname or IP). Alias: host.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/tools/traceroute" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"example.com"}'
Response (200) json
{
  "tool": "traceroute",
  "target": "example.com",
  "timed_out": false,
  "duration_ms": 8431,
  "output": "traceroute to example.com (104.20.23.154), 20 hops max\n 1  10.0.0.1  1.2 ms\n 2  ...\n"
}
POST /v1/diagnose

Connection diagnosis

Submit client-measured metrics and get back a plain-language verdict, a list of likely causes and a shareable report URL. Bandwidth and latency numbers are measured in the browser (see the speed-test engine below) and posted here.

Param Type Required Default Description
metrics object yes Client-measured metrics. Flat fields are also accepted.
metrics.download_mbps number no null Download throughput in Mbps.
metrics.upload_mbps number no null Upload throughput in Mbps.
metrics.ping_ms number no null Idle latency in ms.
metrics.jitter_ms number no null Latency variation in ms.
metrics.packet_loss_pct number no null Packet loss percentage.
metrics.bufferbloat_ms number no null Latency increase under load.
metrics.dns_ms number no null DNS lookup time. If omitted, dns_target is measured server-side.
dns_target string no Hostname measured server-side when dns_ms is absent.
panel string no "full" Echoed into meta for your own bookkeeping.
region string no "auto" Echoed into meta.
Request bash
curl -sS -X POST "https://api.speedtest.doctor/v1/diagnose" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metrics": {
      "download_mbps": 482.6, "upload_mbps": 38.2, "ping_ms": 14,
      "jitter_ms": 11, "packet_loss_pct": 0.4, "bufferbloat_ms": 162, "dns_ms": 41
    },
    "panel": "full"
  }'
Response (200) json
{
  "id": "diag_12b0639051d6",
  "verdict": "healthy_throughput_high_bufferbloat",
  "metrics": {
    "download_mbps": 482.6, "upload_mbps": 38.2, "ping_ms": 14,
    "jitter_ms": 11, "packet_loss_pct": 0.4, "bufferbloat_ms": 162, "dns_ms": 41
  },
  "causes": ["bufferbloat"],
  "report_url": "https://speedtest.doctor/r/diag_12b0639051d6",
  "meta": {
    "panel": "full", "region": "auto",
    "bandwidth_source": "client", "dns_source": "client", "plan": "starter"
  }
}

Speed-test engine endpoints

These power the in-browser speed tester. They are not behind a Bearer key (the browser calls them cross-origin) and are protected by per-IP rate limits, a concurrency cap and a per-IP run limit. Metering happens when the client submits measurements to /v1/diagnose.

Method Path Purpose
GET /down?bytes=N Streams N bytes of incompressible data (download probe). bytes=0 is a latency probe.
POST /up Sinks uploaded bytes and reports throughput (upload probe).
GET /v1/meta Returns the caller's IP, e.g. { "ip": "…" }.
POST /v1/test/start Reserves a speed-test run against the per-IP window; returns allowed / remaining.
GET /health Liveness + capacity snapshot. No auth.

Quotas & errors

Each plan includes a number of prepaid API calls. One successful /v1/* request = one call.

Plan Included calls API keys Price
Free 100 1 €0
Starter 10,000 5 €9 / mo
Pro 50,000 Unlimited €29 / mo

Rate limiting

Requests are also rate-limited per key and per IP (short rolling windows; currently ~120 req/min per key). Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (epoch seconds). On a 429 you also get a retry-after header.

Error responses

Errors return a non-2xx status and a small JSON body with an error code and a human message.

401 Unauthorized json
// 401 — missing or invalid key
{ "error": "unauthorized", "message": "Missing Bearer API key" }
{ "error": "unauthorized", "message": "Invalid API key", "reason": "rejected by App A" }
429 Too Many Requests json
// 429 — rate limit hit (per-IP or per-key). Also sent as response headers:
//   x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, retry-after
{ "error": "rate_limited", "scope": "key", "retry_after": 42 }
400 Bad Request json
// 400 — bad/again-blocked target (SSRF guard) or malformed request
{ "error": "invalid_target", "message": "target resolves to a private/reserved address" }
{ "error": "request_error", "message": "ports must be a non-empty array" }

Code samples

Calling the SSL endpoint from a couple of common stacks. Swap YOUR_API_KEY and the target as needed.

JavaScript (fetch) javascript
const res = await fetch("https://api.speedtest.doctor/v1/tools/ssl", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ target: "example.com" }),
});

if (!res.ok) throw new Error(`API error ${res.status}`);
const data = await res.json();
console.log(data.certificate.not_after); // -> "Aug 29 21:41:26 2026 GMT"
Python (requests) python
import requests

resp = requests.post(
    "https://api.speedtest.doctor/v1/tools/ssl",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"target": "example.com"},
    timeout=15,
)
resp.raise_for_status()
data = resp.json()
print(data["certificate"]["not_after"])  # -> "Aug 29 21:41:26 2026 GMT"

Embed the widget

Drop a live diagnostics tool into your own site with one <iframe>. Configure it below and copy the generated snippet — the preview updates as you change options.

How auth works: the widget calls the public API with your key. Pass it as the key URL parameter (simplest), or deliver it at runtime via postMessage so it never appears in your HTML (recommended). Either way the key is client-visible, so use a dedicated, low-quota key for embeds. Never put a Pro key in a public page.

Configure

Live preview

Open in new tab ↗
Embed snippet html

Replace YOUR_API_KEY with a real key. Prefer the postMessage method below if you'd rather keep the key out of your markup.

Advanced: deliver the key with postMessage (key stays out of the URL)
postMessage embed html
<iframe id="std-widget"
        src="https://speedtest.doctor/embed/tool?tool=ssl&theme=dark"
        style="width:100%;border:0;" height="320"></iframe>
<script>
  const f = document.getElementById("std-widget");
  f.addEventListener("load", () => {
    // Key is sent at runtime; it never appears in the iframe URL or your HTML source.
    f.contentWindow.postMessage(
      { type: "std:config", key: "YOUR_API_KEY", target: "example.com" }, "*");
  });
  // Optional: auto-resize the iframe to the widget's content.
  window.addEventListener("message", (e) => {
    if (e.data && e.data.type === "std:height") f.style.height = e.data.height + "px";
  });
</script>

Start building

Grab a free key and make your first call in under a minute.

API plans & pricing · Widget gallery · Free speed test