Qartas API Reference
Base URL: https://navigation.wslt.app — all endpoints below are relative to
it. Verified against the service source (internal/api/handlers.go,
internal/api/tiles.go, cmd/chafmaps-api/main.go, internal/engine/*) and
against live responses on 2026-07-31.
Conventions
Auth — every endpoint requires authentication; there is no anonymous access. Two credential forms:
| Endpoints | Accepted credentials |
|---|---|
JSON endpoints + staticmap (/autocomplete, /geocode, /reverse, /directions, /prefetch-directions, /place/{id}, /place-photos, /tile-token, /staticmap) |
X-API-Key: cmk_live_... header (or a platform JWT Authorization: Bearer ... — operator-internal) |
Tile family (/tiles, /vector, /style, /glyphs, /sprites) |
X-API-Key header or ?t=<tile token> query parameter (token minted from your key via POST /maps/v1/tile-token) |
See auth.md for details.
Envelope — JSON endpoints wrap results:
{"status":"success","data":{...},"message":"OK"}
{"status":"error","error":{"code":"CODE","message":"text","details":{...}?},"message":"text"}
Binary endpoints (tiles, glyphs, sprites .png, staticmap) return raw
bytes on success; their parameter/upstream errors are a bare HTTP status
with no body (auth-layer errors on those routes do use the JSON envelope).
Request correlation — every response carries X-Request-ID (either your
well-formed UUID X-Request-ID request header echoed back, or a generated
one). Quote it in support requests.
Limits — request bodies are capped at 1 MB; the whole request times out at
30 s (504 REQUEST_TIMEOUT) — both operator-configurable
(CHAFMAPS_API_REQUEST_TIMEOUT_SECONDS, default 30). Per-minute rate limits
and daily quotas: rate-limits-and-plans.md.
Zero coordinates — JSON body fields marked required below use binding
validation that treats an absent field and a literal 0 identically: a
coordinate of exactly 0 is rejected (400 VALIDATION_ERROR). Coordinates
(0,0) ("null island") are never valid input anywhere on this API.
Service off-switch — if the engine is disabled on the server, every
/maps/v1 route answers 503 MAPS_UNAVAILABLE.
GET /maps/v1/autocomplete
Search-as-you-type place suggestions.
Query parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
q |
string | no* | — | The search text. *Empty/whitespace q is not an error: instant 200 with empty predictions and source: "empty_query" (so per-keystroke callers can fire on deletes). |
lat |
float | no | 0 | Bias center latitude (user location). |
lng |
float | no | 0 | Bias center longitude. If lat and lng are both 0/absent, a per-country fallback city center is used from the caller's IP country (QA→Doha, SA→Riyadh, AE→Dubai, EG→Cairo, otherwise Amman). |
country |
string | no | IP country, else "jo" |
ISO-3166 alpha-2 ranking bias (e.g. qa). A soft bias, never a hard filter — explicit other-country names still match. Invalid values fall back. |
locale |
string | no | from Accept-Language (ar* → ar, else en) |
Result-language hint: en or ar. |
max_wait_ms |
int | no | 2000 (server env CHAFMAPS_API_AUTOCOMPLETE_MAX_WAIT_MS) |
Foreground wait budget, clamped to [200, 10000]. If the gather isn't done in time you get budget_expired: true and the gather keeps warming the cache for your next call. |
Response — 200
{
"predictions": [
{
"place_id": "g_0x151b5fb85d7981af:0x631c30c0f8dc65e8",
"name": "Amman",
"address": "Al-Matar Street, Amman",
"distance_meters": 1200,
"lat": 31.9543786,
"lng": 35.9105776,
"ownership_tier": "seed",
"driver_verifications": 0
}
],
"source": "redis",
"served_in_ms": 0,
"budget_expired": false
}
| Field | Type | Description |
|---|---|---|
predictions[] |
array | Suggested places, ranked. |
predictions[].place_id |
string | Source-prefixed place ID (g_ = Google-sourced, w_ = alternate source). Pass to /place/{id} unchanged — the server strips the prefix itself. |
predictions[].name |
string | Display name. |
predictions[].address |
string | Display address (may be empty). |
predictions[].distance_meters |
int | Distance from the bias center. Omitted when 0/unknown. |
predictions[].lat / lng |
float | Coordinates. Omitted when unknown. |
predictions[].ownership_tier |
string | Data-quality tier: seed | enriched | verified | owned. |
predictions[].driver_verifications |
int | Count of real-world confirmations. |
source |
string | Where the answer came from: a cache/gather tier (redis, postgres, ...), or empty_query (blank q), budget_expired (see below), empty (engine returned nothing). |
served_in_ms |
int | Server-side time to produce the answer. |
budget_expired |
bool | true = the wait budget elapsed before the gather finished — the empty result is not authoritative; retry shortly. false + empty predictions = a genuine no-match. |
Errors
401 (auth), 429 RATE_LIMIT_EXCEEDED/MAPS_QUOTA_EXCEEDED,
503 AUTOCOMPLETE_BACKPRESSURE / AUTOCOMPLETE_UNAVAILABLE / AUTOCOMPLETE_DISABLED,
502 AUTOCOMPLETE_FAILED. See errors.md.
POST /maps/v1/geocode
Forward geocoding: free-text address → coordinates. (Implemented as autocomplete → top hit → place-details enrichment; if enrichment fails you still get the autocomplete hit's coordinates.)
Body
| Field | Type | Required | Description |
|---|---|---|---|
address |
string | yes (non-empty) | The address/free text to resolve. |
Response — 200
{
"matched": true,
"formatted_address": "Desert Highway, Amman",
"place_id": "g_0x151b5402e149d31b:0x1b38522aafb395fc",
"name": "Queen Alia International Airport (AMM)",
"lat": 31.7216982,
"lng": 35.9964563,
"source": "chafmaps_autocomplete"
}
| Field | Type | Description |
|---|---|---|
matched |
bool | false = no match found; every other field is then omitted. |
formatted_address |
string | Best-known address. |
place_id |
string | Source-prefixed place ID (may be absent). |
name |
string | Place display name. |
lat / lng |
float | Resolved coordinates. |
source |
string | chafmaps_place_details (details-enriched, most precise) or chafmaps_autocomplete (top autocomplete hit's own coordinates). |
Errors
400 VALIDATION_ERROR (missing/empty address), 401, 429,
503 GEOCODE_BACKPRESSURE / GEOCODE_UNAVAILABLE / GEOCODE_DISABLED, 502 GEOCODE_FAILED.
POST /maps/v1/reverse
Reverse geocoding: coordinates → address.
Body
| Field | Type | Required | Description |
|---|---|---|---|
lat |
float | yes (non-zero) | Latitude. |
lng |
float | yes (non-zero) | Longitude. |
Response — 200
{
"lat": 31.9539,
"lng": 35.9106,
"formatted_address": "CAC - المركز الزراعي الشامل، عمّان",
"place_id": "g_...",
"source": "fresh"
}
| Field | Type | Description |
|---|---|---|
lat / lng |
float | Echo of the request coordinates. |
formatted_address |
string | One flat display address (no street/city/postal component breakdown exists on this API). Empty when source is no_address. |
place_id |
string | Rarely present on reverse results; omitted when unknown. |
source |
string | redis | postgres | fresh (live gather) | approximate | no_address. |
raw |
object | Optional raw provider payload; usually omitted. |
source: "no_address" is a real answer ("nothing is here" — open water,
unmapped area), served 200. Do not treat it as a failure or retry it.
Errors
400 VALIDATION_ERROR, 401, 429,
503 REVERSE_BACKPRESSURE (Retry-After: 2) — momentary saturation, retry;
503 REVERSE_UNAVAILABLE (Retry-After: 5) — all sources failed, retry later;
502 REVERSE_FAILED.
POST /maps/v1/directions
Driving directions between two points, optionally via up to 4 waypoints.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
origin_lat |
float | yes (non-zero) | — | Origin latitude. |
origin_lng |
float | yes (non-zero) | — | Origin longitude. |
dest_lat |
float | yes (non-zero) | — | Destination latitude. |
dest_lng |
float | yes (non-zero) | — | Destination longitude. |
coarse_origin |
bool | no | false |
Use a coarser (~110 m vs ~11 m) cache cell for the origin — improves cache hits when the origin is a moving vehicle. Destination stays full-resolution. |
enrich_maneuvers |
bool | no | false |
Must stay false. true is rejected with 400 ENRICH_MANEUVERS_UNSUPPORTED (the OSRM enrichment pass doesn't exist on this service). maneuvers[] still appear whenever the routing tier natively returns them. |
max_wait_ms |
int | no | none (server's 30 s request timeout applies) | Wait budget. Honored when 0 < max_wait_ms < 30000. On expiry you get the degraded straight-line 200, not an error. |
waypoints |
array | no | [] |
Up to 4 intermediate stops, each {"lat": .., "lng": ..}. Each must be in-range and not (0,0). Presence of any waypoint switches to the multi-leg response shape below. |
Origin ≈ destination (and no waypoints) is rejected: 400 SAME_POINT.
Response — 200, three shapes
1. Normal route (no waypoints, routing succeeded):
{
"routes": [
{
"label": "Fastest",
"distance_meters": 10562,
"duration_seconds": 1043,
"live_traffic_duration_seconds": 1043,
"polyline": "{\"coordinates\":[[35.9106,31.9539],...],\"type\":\"LineString\"}",
"maneuvers": [ {"type":"left","instruction":"Turn left onto ...","distance_m":230.0,"duration_s":25.0,"street_name":"..."} ]
}
],
"distance_meters": 10562,
"duration_seconds": 1043,
"polyline": "<primary route's GeoJSON LineString, as a JSON string>",
"source": "redis",
"engine": "google",
"ownership_tier": "seed",
"served_at": "2026-07-31T22:24:07Z",
"served_in_ms": 3
}
| Field | Type | Description |
|---|---|---|
routes[] |
array | One or more route alternatives; routes[0] is primary. |
routes[].label |
string | Human label (e.g. "Fastest"). |
routes[].distance_meters / duration_seconds |
int | Per-route totals. |
routes[].live_traffic_duration_seconds |
int | Traffic-aware ETA (omitted when unavailable). |
routes[].polyline |
string | A GeoJSON LineString serialized as a JSON string ([lng,lat] coordinate order). Parse it before drawing. |
routes[].maneuvers[] |
array | Turn-by-turn steps when the routing tier natively produced them: {type, instruction, distance_m, duration_s, street_name?}. Omitted otherwise. |
routes[].free_flow_duration_seconds / typical_duration_seconds |
int | Optional traffic context (best-case / typical-for-time-of-day). |
routes[].traffic_segments[] |
array | Optional per-segment traffic for polyline coloring: {"d": <meters from start>, "t": <seconds from start>, "l": <level>, "s": <speed km/h>}. |
distance_meters, duration_seconds, polyline |
— | Primary route's values, duplicated top-level for simple callers. |
source |
string | Cache tier that served this: redis | postgres | fresh. |
engine |
string | Underlying routing engine (osrm | valhalla | graphhopper | waze | google), preserved across cache reads. Omitted when unknown. |
ownership_tier |
string | Data-quality tier of the cached route. |
served_at / served_in_ms |
— | Serve timestamp / duration. |
2. Degraded straight line (timeout or all-sources failure — still 200):
{
"polyline": "{\"type\":\"LineString\",\"coordinates\":[[35.9106,31.9539],[35.8340,31.9720]]}",
"distance_meters": 7423,
"duration_seconds": 1856,
"routes": [],
"partial": true,
"reason": "degraded",
"source": "server_straight",
"elapsed_ms": 8000
}
distance_meters is the straight-line (haversine) distance;
duration_seconds is a rough estimate (distance ÷ 4 m/s, minimum 30 s).
🔴 Never render a
partial: truepolyline as a road route. Show distance/ETA if useful and re-fetch shortly — the gather keeps running server-side, so the retry is usually a cache hit. Detect this shape bypartial == trueorsource == "server_straight".
3. Multi-leg (any waypoints present):
{
"polyline": "{\"type\":\"MultiLineString\",\"coordinates\":[[[lng,lat],...],[[lng,lat],...]]}",
"distance_meters": 14210,
"duration_seconds": 1580,
"legs": 2,
"partial": false,
"source": "maps_v1_multileg",
"elapsed_ms": 950
}
polyline is a GeoJSON MultiLineString (one member per leg, serialized as
a JSON string). partial: true + source: "maps_v1_multileg_partial" means
at least one leg degraded to a straight line — the same "don't draw as a
road" rule applies to that response. No routes[] array on this shape.
Multi-leg totals are a route preview — do not use them as a pricing/fare
anchor.
Errors
400 VALIDATION_ERROR / SAME_POINT / TOO_MANY_WAYPOINTS / BAD_WAYPOINT
/ ENRICH_MANEUVERS_UNSUPPORTED, 401, 422 DIRECTIONS_UNROUTABLE (no route
exists between the coordinates — don't retry unchanged), 429,
503 DIRECTIONS_BACKPRESSURE (Retry-After: 5). Note: generic routing
failures do not produce a 5xx — they produce the degraded 200 above.
POST /maps/v1/prefetch-directions
Fire-and-forget cache warm-up. Same body and validation as /directions
(including the 4-waypoint cap and enrich_maneuvers rejection). The server
validates, queues the gather(s) in the background, and answers immediately —
you never wait for routing.
Response — 200
{"queued": true, "legs": 1}
or, when the server's bounded prefetch pool is saturated (not an error — the prefetch is a hint, not a promise):
{"queued": false, "reason": "prefetch_saturated"}
legs = 1 + number of waypoints. Background gathers are capped server-side
(default 16 concurrent, 20 s timeout each; env
CHAFMAPS_API_PREFETCH_MAX_CONCURRENT / CHAFMAPS_API_PREFETCH_TIMEOUT_MS).
Errors
Same validation errors as /directions; 401; 429.
GET /maps/v1/place/{id}
Full details for a place ID (from autocomplete/geocode).
Path & query parameters
| Param | In | Required | Default | Description |
|---|---|---|---|---|
id |
path | yes | — | Place ID. g_/w_ source prefixes are stripped server-side — pass IDs from autocomplete/geocode unchanged. After stripping, must match a known shape: ChIJ... (Google Place ID), 0x<hex>:0x<hex> (feature ID), or 12–30 hex chars (CID); otherwise 400 BAD_PLACE_ID. |
lat |
query | no | 0 | Coordinate hint (helps disambiguation). |
lng |
query | no | 0 | Coordinate hint. |
q |
query | no | — | Display-name hint; used to fill name on a total cache miss in cache_only mode. |
cache_only |
query | no | false |
true/1: answer from cache only within 400 ms — never triggers a live gather. On a miss you get a minimal record (place_id, name from q, lat/lng from hints) instead of an error. |
provider |
query | no | — | Accepted for forward-compatibility; currently has no effect. |
Response — 200
{
"place_id": "0x151b5f96539bcc23:0x83d61bcf8e637e6d",
"name": "Amman Citadel",
"address": "Amman Citadel, K. Ali Ben Al-Hussein St. 146, Amman",
"lat": 31.9543163,
"lng": 35.9365046,
"phone": "+962 6 463 8795",
"category": "Amman Citadel",
"rating": 4.5,
"ownership_tier": "enriched",
"driver_verifications": 0
}
| Field | Type | Description |
|---|---|---|
place_id |
string | The unprefixed ID (the g_/w_ marker is not echoed back). |
cid |
string | Optional numeric CID. |
name |
string | Display name. |
name_localized |
object | Optional {"en": ..., "ar": ...} map. |
address |
string | Formatted address. |
lat / lng |
float | Coordinates (always populated on a full lookup). |
phone, website, category, hours |
string | Optional; omitted when unknown. |
rating |
float | Optional star rating. |
rating_count |
int | Optional review count. |
photos[] |
array of string | Optional direct photo URLs. |
ownership_tier, driver_verifications |
— | Data-quality signals (see overview). |
Errors
400 MISSING_ID / BAD_PLACE_ID, 401, 429,
503 PLACE_UNAVAILABLE (Retry-After: 5), 503 PLACE_DISABLED / PLACE_BACKPRESSURE, 502 PLACE_FAILED.
GET /maps/v1/place-photos
Photos for whatever place is at a coordinate (reverse-geocode → place-resolve → photos, composed server-side, best-effort).
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
lat |
float | yes | Latitude (numeric; missing/non-numeric → 400 MISSING_COORDS). |
lng |
float | yes | Longitude. |
Response — 200
{"place_id": "g_0x151b5fe8b12bd0c7:0x974306aae2775390", "photos": ["https://lh3.googleusercontent.com/...", "..."]}
| Field | Type | Description |
|---|---|---|
place_id |
string | The resolved place (may be "" when nothing resolved). |
photos[] |
array of string | Direct, fetchable image URLs (googleusercontent.com/ggpht.com). Empty is normal and common — most residential coordinates have no photos. Any miss along the chain answers 200 with photos: [], never an error. |
Errors
400 MISSING_COORDS, 401, 429.
POST /maps/v1/tile-token
Mints the short-lived HMAC tile token that authorizes URL-parameter tile access. No request body.
Response — 200
{
"token": "bWFwc2tleTpkYTRkMWU3Ni0...",
"expires_at": "2026-07-31T22:24:16.837974336Z"
}
| Field | Type | Description |
|---|---|---|
token |
string | Opaque base64url token. Append to tile-family URLs as ?t=<token>. Bound to the API key that minted it — revoking the key kills the token. |
expires_at |
RFC3339 | Expiry — 24 h after minting by default (operator-tunable via CHAFMAPS_TILE_TOKEN_TTL). Mint a fresh token before this (the Dart SDK refreshes at 80% of lifetime automatically). |
Errors
401, 429, 500 TILE_TOKEN_FAILED (server misconfiguration),
503 MAPS_UNAVAILABLE.
GET /maps/v1/tiles/{z}/{x}/{y}[@2x|@3x].png
Raster basemap tiles (Web-Mercator XYZ scheme).
Path parameters & auth
| Param | Description |
|---|---|
z |
Zoom, 0–20 (z19/z20 rendered on demand). Out of range → bare 400. |
x, y |
Tile column/row; must lie within the 2^z grid → else bare 400. |
@2x / @3x |
Optional DPR suffix on the y segment (e.g. /tiles/12/2456/1608@2x.png): @2x = 512 px, @3x = 768 px, none = 256 px. |
| extension | Optional; default png. Accepted: png, jpg, jpeg, gif, webp (anything else → bare 400). The origin serves PNG in practice. |
Auth: X-API-Key header or ?t=<tile token>. Anonymous → 401 AUTH_REQUIRED (JSON envelope).
Optional request header X-Chaf-Prefetch: <any value> marks speculative
(pre-warming) traffic: cache misses are routed to a free prefetch pool, and a
degraded stand-in tile becomes a 503 instead of an image (a prefetcher has
no impatient viewer). Always send it from prefetch code paths.
Response — 200
Raw image bytes. Notable headers:
| Header | Meaning |
|---|---|
Content-Type |
Sniffed real image MIME (e.g. image/png). |
X-Tile-Source |
Where the tile came from: cache / memory / fresh / fresh-prefetch = verified content; ancestor-* etc. = degraded stand-in. |
Cache-Control |
public, max-age=3600, stale-while-revalidate=86400 for verified tiles; no-store (plus CDN variants) for degraded stand-ins — never persist those. |
Errors (bare status, no JSON body — except auth)
| Status | Meaning | Retry? |
|---|---|---|
400 |
Bad z/x/y or unsupported format. | No — fix the URL. |
401 |
Missing/invalid credential (JSON envelope: AUTH_REQUIRED, INVALID_TOKEN, TOKEN_EXPIRED, TOKEN_NOT_KEY_BOUND, API_KEY_REVOKED, INVALID_API_KEY). |
Re-auth / re-mint. |
404 |
Origin has no such tile. | No. |
429 |
Per-IP tile rate cap (JSON envelope RATE_LIMIT_EXCEEDED, Retry-After). |
After Retry-After. |
502 / 504 |
Transient upstream failure/timeout (Retry-After: 1 on 5xx). |
Yes, briefly deferred. |
503 |
Service disabled, or degraded-tile-on-prefetch. | Yes, later. |
GET /maps/v1/vector/{source}/{z}/{x}/{y} — NOT OFFERED
Registered but not part of the product. On the live deployment it answers:
501 Not Implemented
{"status":"error","error":{"code":"VECTOR_TILES_NOT_SUPPORTED","message":"vector tiles are not offered by this service — the standalone Qartas API serves raster tiles only (tiles/@2x/@3x, style, glyphs, sprites, staticmap). ..."},"message":"..."}
Do not retry a 501. (The route exists so that a self-hosted operator could
point CHAFMAPS_VECTOR_TILE_ORIGIN at their own tileserver-gl; the hosted
service does not.)
GET /maps/v1/style/{name}
MapLibre GL JSON styles (version 8) whose tile/glyph URLs point back at this API.
Parameters & auth
| Param | In | Required | Description |
|---|---|---|---|
name |
path | yes | raster-nav (generated raster style, @2x tile template), chafmaps (bundled on-disk style, @2x + ?t= template), or maplibre (alias for chafmaps). A .json suffix is accepted and stripped. Unknown names → bare 404. |
token |
query | no | A tile token to embed into the returned style's tile URLs (raster-nav: appended as ?t=<token>; disk styles: substituted for the __TOKEN__ placeholder). Without it the style's tile URLs carry no token and tile fetches will 401 unless your renderer adds X-API-Key itself. |
Note the two different query parameters: ?token= is what gets embedded
in the style body; auth for fetching the style itself is X-API-Key or
?t= — same as the rest of the tile family. A typical map-widget call is:
GET /maps/v1/style/raster-nav.json?t=<tile token>&token=<same tile token>
Response — 200
Content-Type: application/json, Cache-Control: private, max-age=60.
raster-nav additionally sets Access-Control-Allow-Origin: * (usable from
MapLibre GL JS in a browser). Example (raster-nav):
{"version":8,"name":"Qartas SDK Raster Nav",
"glyphs":"/maps/v1/glyphs/{fontstack}/{range}.pbf",
"sources":{"g":{"type":"raster","tiles":["/maps/v1/tiles/{z}/{x}/{y}@2x.png"],"tileSize":256,"attribution":""}},
"layers":[{"id":"g","type":"raster","source":"g"}]}
URLs are relative — they resolve against whatever host served the style
(the operator can switch them to absolute via CHAFMAPS_API_PUBLIC_BASE_URL).
GET /maps/v1/glyphs/{fontstack}/{range}
SDF font glyphs for MapLibre text rendering.
| Param | In | Description |
|---|---|---|
fontstack |
path | Font name; comma-separated candidates are tried in order (MapLibre sends "Font A,Font B"). Bundled: Noto Sans Regular. / or \ in either param → bare 400. |
range |
path | The glyph range including the .pbf suffix (matches the style template {range}.pbf), e.g. 0-255.pbf. Bundled ranges: 0-255, 256-511 (Latin), 1536-1791 (Arabic). Anything else → bare 404. |
Auth: X-API-Key or ?t=. Response: application/x-protobuf,
Cache-Control: public, max-age=2592000, immutable.
GET /maps/v1/sprites/{name}
Sprite stub for MapLibre's automatic sprite requests (the bundled styles use no icon layers, so a valid-empty answer is correct).
| Request path ends in | Response |
|---|---|
.json (e.g. /sprites/sprite.json, /sprites/sprite@2x.json) |
200 {} (application/json) |
.png |
200 67-byte transparent 1×1 PNG |
| anything else | bare 404 |
Auth: X-API-Key or ?t=. Cache-Control: public, max-age=3600.
GET /maps/v1/staticmap
Server-rendered PNG of a route path over the basemap (stitched from real
tiles + drawn polyline). This is a genuine per-call render, so it is
credentialed like the JSON endpoints (X-API-Key header; a ?t= tile
token is not accepted here) and billed to its own "tiles" daily quota
bucket (see rate-limits-and-plans.md).
Query parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | yes | — | Flat comma list lat,lng,lat,lng,... — never semicolons. Non-numeric pairs are skipped; fewer than 2 valid points → bare 400. |
w |
int | no | 600 | Image width px; values ≤0 → default; capped at 1024. |
h |
int | no | 260 | Image height px; same clamping. |
Response — 200
image/png bytes, Cache-Control: public, max-age=86400.
Errors
Bare 400 (bad path), 401 (envelope), 429, bare 502 (render failed —
there is no degraded fallback shape for an image).
Health endpoints (no auth)
| Endpoint | Response |
|---|---|
GET /livez |
200 {"status":"ok"} — process is up (dependency-free). |
GET /healthz |
200/503 {"postgres":"ok","redis":"ok","chafmaps_wired":true} — dependency health; string fields carry an error message instead of "ok" on failure. |