texas2stepdocs Source on GitHubGitHub

API

Interactive reference: /api/docs (OpenAPI explorer) and /api/openapi.json on any server. The explorer is served by texas2step itself, so it works without internet access. The API tab in the web UI has copy-ready examples against that server.

Paths below are relative to the server's base URL. That is https://www.example.net/texas2step for a server published at /texas2step/ (see Install), or just the origin, such as http://127.0.0.1:8080, when reaching the service directly.

All responses carry Cache-Control: no-store, Timing-Allow-Origin: * and CORS headers. Measurement endpoints add Server-Timing so clients can subtract server time.

Measurement primitives#

Method Path Purpose
GET /api/v1/ping Minimal response for round-trip timing. Server-Timing: app;dur=…
GET /api/v1/down?bytes=N Exactly N random bytes (≤ max_download_bytes) with Content-Length
GET /api/v1/down?seconds=S Stream random bytes for S seconds (chunked, ≤ max_stream_seconds)
POST /api/v1/up Read and discard the body; returns {"bytes", "rx_ms", "mbps"} measured server-side
WS /api/v1/ws Echo every message unchanged
POST /api/v1/rtc/offer WebRTC signalling for the UDP probe (see below)
# 1 GB download at full speed
curl -o /dev/null -w '%{speed_download} B/s\n' 'https://www.example.net/texas2step/api/v1/down?bytes=1000000000'

# Saturate for 30 s (useful with -P in a loop, or alongside other traffic)
curl -o /dev/null 'https://www.example.net/texas2step/api/v1/down?seconds=30'

# 500 MB upload; the server reports what it measured
head -c 500000000 /dev/urandom | curl -s -X POST --data-binary @- https://www.example.net/texas2step/api/v1/up

UDP probe protocol#

POST /api/v1/rtc/offer with {"sdp": "<offer>", "type": "offer"} returns {"id", "sdp", "type": "answer"}. ICE is not trickled, so gather all candidates before sending the offer. The offer must include data channels:

  • probe: create it with ordered: false, maxRetransmits: 0. Every message is echoed verbatim. texas2step clients send u32 sequence (LE) | f64 send time ms (LE) | padding.
  • control (reliable, optional): send {"op":"stats"} to receive {"op":"stats","rx":…,"tx":…,"rx_bytes":…}, or {"op":"reset"} to zero the counters. {"op":"addr"} returns both ends of the UDP path ICE chose: {"op":"addr","ip":"192.0.2.7","port":50123,"server_ip":"192.0.2.1","server_port":61042}. ip and port are the client as the server sees it, after any NAT. All four are null until the path is up.

Sessions close when the peer disconnects or after rtc_session_timeout seconds idle. The server answers 503 when it already holds max_rtc_sessions sessions, or when every port in its rtc_port_min–rtc_port_max range is taken.

Discovery#

Method Path Returns
GET /api/v1/info name, version, server identity, capabilities, limits, ICE servers, UI defaults
GET /api/v1/whoami client IP, port, family, scheme, HTTP version, user agent
GET /api/v1/diag/http both ends of this HTTP connection, its HTTP version and TLS, the server's TCP RTT and clock (below)
GET /api/v1/locations servers offered in the picker (this one first)
GET /api/v1/health {"status": "ok", "version": …}

GET /api/v1/diag/http answers once per request, uncached, for the Diag tab:

{
  "client": {"ip": "2001:db8::7", "port": 51234, "family": "IPv6"},
  "server": {"ip": "2001:db8::1", "port": 443, "family": "IPv6"},
  "http_version": "HTTP/2.0",
  "tls": {"protocol": "TLSv1.3", "cipher": "TLS_AES_256_GCM_SHA384"},
  "tcp_rtt_ms": 1.234,
  "tcp_rttvar_ms": 0.567,
  "time_ms": 1792385060799.1
}

Behind nginx, the client port, server address and port, TLS and the kernel's TCP round-trip time ($tcpinfo_rtt) come from headers the package's snippet sets. They're believed only from trusted_proxies, and a proxy that doesn't send them leaves those fields null. Without a proxy, server is the socket's own address, and tls and the TCP times are null. time_ms is the server's clock, in milliseconds since the epoch.

Server-side measurements#

The server runs the Python engine toward another texas2step server and returns the result. target must be a configured location URL unless allow_any_target is set. At most two run at once per worker; extra requests get 429.

Method Path Body (defaults)
POST /api/v1/measure/latency {"target", "count": 20}
POST /api/v1/measure/download {"target", "seconds": 10, "streams": 4, "loaded_latency": true}
POST /api/v1/measure/upload same as download
POST /api/v1/measure/loss {"target", "packets": 500, "interval_ms": 10, "size": 64}
POST /api/v1/measure/full {"target", "seconds", "streams", "packets", "interval_ms", "loss": true}
curl -s -X POST https://dallas.example.net/texas2step/api/v1/measure/full \
  -H 'content-type: application/json' \
  -d '{"target": "https://houston.example.net/texas2step", "seconds": 10}'

Results#

Method Path Purpose
POST /api/v1/results Store a result object ("version": 1, ≤ 2 MB). Returns its summary and id
GET /api/v1/results?limit=50&before=<epoch> Summaries, newest first
GET /api/v1/results/{id} One result in full; also viewable at /r/{id}

Result object#

Browser runs, texas2step run --json and /measure/full share one shape (snake_case):

{
  "version": 1,
  "tool": "texas2step-web/<version>",
  "started_at": "2026-10-18T07:24:00Z",
  "server": {"id": "dallas", "name": "dallas.example.net", "location": "Dallas, TX", "url": "https://…", "version": "<version>"},
  "client": {"ip": "10.1.2.3", "family": "IPv4", "protocol": "h2", "user_agent": "…"},
  "config": {"streams": 4, "seconds": 10, "packets": 500, "interval_ms": 10},
  "latency": {"count": 20, "lost": 0, "min": 2.9, "median": 3.2, "p90": 3.8, "jitter": 0.4, "samples": [ … ], …},
  "download": {"mbps": 941.2, "peak_mbps": 948.0, "bytes": …, "timeline": [ …per 100 ms… ],
               "per_stream_mbps": [ … ], "loaded_latency": { …latency stats… }, …},
  "upload": { …same as download… },
  "loss": {"transport": "webrtc", "sent": 500, "received": 499, "loss_pct": 0.2,
           "up_loss_pct": 0.0, "down_loss_pct": 0.2, "out_of_order": 0,
           "rtt": { …latency stats… }, "packets": [3.1, 3.0, null, …]},
  "bufferbloat": {"added_download_ms": 4.1, "added_upload_ms": 11.0, "grade": "A"},
  "readiness": [{"id": "calls", "label": "Video calls", "level": "great", "detail": "…"}, …]
}

readiness is computed by the browser client only.