Qartas Quickstart — running in 5 minutes
Base URL for all examples:
https://navigation.wslt.app
1. Get an API key
API keys are issued by the Qartas operator — there is no self-serve signup. Request one (tell us your app/company name and expected volume) and you will receive a key of the form:
cmk_live_uvgADMdlIfH4GPLJ8YtQqOQkAMEo_wgR (example — 41 chars, base64url charset)
The key is shown to you exactly once — only its SHA-256 hash is stored
server-side, so it cannot be recovered later, only replaced. Store it in a
secret manager. It is sent as the X-API-Key header on every call.
export CHAFMAPS_KEY="cmk_live_..." # your key
export B="https://navigation.wslt.app"
2. One curl per surface
Autocomplete (search-as-you-type)
curl -H "X-API-Key: $CHAFMAPS_KEY" \
"$B/maps/v1/autocomplete?q=amman&lat=31.95&lng=35.91&country=jo"
{"status":"success","data":{
"predictions":[{"place_id":"g_0x151b5fb85d7981af:0x631c30c0f8dc65e8",
"name":"Amman","address":"","lat":31.9543786,"lng":35.9105776,
"ownership_tier":"seed","driver_verifications":0}, ...],
"source":"redis","served_in_ms":0,"budget_expired":false},"message":"OK"}
An empty predictions array with "budget_expired": true means "still
gathering — retry shortly", not "no results". Only
"budget_expired": false + empty is an authoritative no-match.
Forward geocode (address → coordinates)
curl -X POST -H "X-API-Key: $CHAFMAPS_KEY" -H "Content-Type: application/json" \
-d '{"address":"Queen Alia International Airport"}' \
"$B/maps/v1/geocode"
{"status":"success","data":{"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"},"message":"OK"}
"matched": false (with the other fields absent) means no match.
Reverse geocode (coordinates → address)
curl -X POST -H "X-API-Key: $CHAFMAPS_KEY" -H "Content-Type: application/json" \
-d '{"lat":31.9539,"lng":35.9106}' \
"$B/maps/v1/reverse"
{"status":"success","data":{"lat":31.9539,"lng":35.9106,
"formatted_address":"CAC - المركز الزراعي الشامل، عمّان","source":"fresh"},"message":"OK"}
"source": "no_address" with an empty formatted_address is a real answer
("nothing is here" — open water, unmapped land), not an error.
Directions
curl -X POST -H "X-API-Key: $CHAFMAPS_KEY" -H "Content-Type: application/json" \
-d '{"origin_lat":31.9539,"origin_lng":35.9106,"dest_lat":31.9720,"dest_lng":35.8340}' \
"$B/maps/v1/directions"
{"status":"success","data":{
"routes":[{"label":"Fastest","distance_meters":10562,"duration_seconds":1043,
"live_traffic_duration_seconds":1043,"polyline":"{\"coordinates\":[[35.9106,31.9539],...],\"type\":\"LineString\"}"}],
"distance_meters":10562,"duration_seconds":1043,
"polyline":"...GeoJSON LineString...","source":"redis","ownership_tier":"seed",
"served_at":"2026-07-31T...","served_in_ms":3}, "message":"OK"}
🔴 Degraded-route caveat — read this before you draw anything. On a timeout or upstream failure this endpoint still answers HTTP 200, but with a synthesized straight line:
{"partial": true, "source": "server_straight", "reason": "degraded", "routes": [], "polyline": "<2-point LineString>", ...}. Never render apartial: truepolyline as a road route — show distance/ETA only and re-fetch after a few seconds (the gather keeps running server-side and warms the cache for your retry). Checkpartial/sourceon every response. Multi-waypoint requests can also return"partial": truewhen a single leg degraded.
Waypoints (up to 4):
curl -X POST -H "X-API-Key: $CHAFMAPS_KEY" -H "Content-Type: application/json" \
-d '{"origin_lat":31.9539,"origin_lng":35.9106,"dest_lat":31.9720,"dest_lng":35.8340,
"waypoints":[{"lat":31.9600,"lng":35.9000}]}' \
"$B/maps/v1/directions"
# → {"polyline":"<GeoJSON MultiLineString>","distance_meters":...,"duration_seconds":...,
# "legs":2,"partial":false,"source":"maps_v1_multileg","elapsed_ms":...}
Prefetch directions (cache warm-up, fire-and-forget)
curl -X POST -H "X-API-Key: $CHAFMAPS_KEY" -H "Content-Type: application/json" \
-d '{"origin_lat":31.9539,"origin_lng":35.9106,"dest_lat":31.9600,"dest_lng":35.9000}' \
"$B/maps/v1/prefetch-directions"
# → 200 {"status":"success","data":{"legs":1,"queued":true},"message":"OK"}
Call it the moment a user picks a destination; by the time they confirm, the
real /directions call is usually a cache hit.
Place details
Pass place IDs from autocomplete/geocode through unchanged (the g_/w_
source prefix is stripped server-side):
curl -H "X-API-Key: $CHAFMAPS_KEY" \
"$B/maps/v1/place/g_0x151b5f96539bcc23:0x83d61bcf8e637e6d?lat=31.9543&lng=35.9365"
{"status":"success","data":{"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},"message":"OK"}
Place photos (for a coordinate)
curl -H "X-API-Key: $CHAFMAPS_KEY" "$B/maps/v1/place-photos?lat=31.9543&lng=35.9365"
# → {"status":"success","data":{"place_id":"g_...","photos":["https://lh3.googleusercontent.com/...", ...]},"message":"OK"}
An empty photos array is normal (most residential coordinates have none).
Photo URLs are direct — load them straight into an image widget.
Tiles (map rendering)
Mint a tile token once per session, then embed it in tile URLs:
TOKEN=$(curl -s -X POST -H "X-API-Key: $CHAFMAPS_KEY" "$B/maps/v1/tile-token" \
| grep -oE '"token":"[^"]+"' | cut -d'"' -f4) # valid 24h
curl -o tile.png "$B/maps/v1/tiles/12/2456/1608@2x.png?t=$TOKEN" # 512px HiDPI
curl -o tile.png "$B/maps/v1/tiles/12/2456/1608.png?t=$TOKEN" # 256px @1x
curl -o tile.png "$B/maps/v1/tiles/12/2456/1608@3x.png?t=$TOKEN" # 768px @3x
(Server-side callers that can set headers may instead send X-API-Key on the
tile request itself and skip the token.)
Style, glyphs, sprites (MapLibre GL)
curl -H "X-API-Key: $CHAFMAPS_KEY" "$B/maps/v1/style/raster-nav.json?token=$TOKEN"
# → a version-8 MapLibre style whose tile/glyph URLs point back at this API,
# with your tile token already embedded (?token= is what gets embedded;
# the request itself authenticates via X-API-Key or ?t=).
curl -o glyphs.pbf "$B/maps/v1/glyphs/Noto%20Sans%20Regular/0-255.pbf?t=$TOKEN"
curl "$B/maps/v1/sprites/sprite.json?t=$TOKEN" # {} — valid empty stub
Bundled glyph ranges for Noto Sans Regular: 0-255, 256-511, 1536-1791
(Arabic). Other ranges 404.
Static map (server-rendered PNG of a route)
curl -o route.png -H "X-API-Key: $CHAFMAPS_KEY" \
"$B/maps/v1/staticmap?path=31.9539,35.9106,31.9720,35.8340&w=400&h=200"
path is a flat lat,lng,lat,lng,... comma list (≥ 2 points; no
semicolons).
3. Dart SDK quickstart
The chafmaps_sdk package is pure Dart (http + meta only — works in
Flutter apps, Dart servers, and CLIs). It is distributed by the operator
(publish_to: none) — depend on it by path:
dependencies:
chafmaps_sdk:
path: ../../packages/chafmaps_sdk
import 'package:chafmaps_sdk/chafmaps_sdk.dart';
final client = QartasClient(
baseUrl: 'https://navigation.wslt.app',
apiKey: 'cmk_live_...', // one key covers everything, tiles included
);
// Autocomplete — timedOut is the server's own budget_expired signal
final suggestions = await client.geocoding.autocomplete(
'Alfardan', lat: 25.2854, lng: 51.5310, country: 'qa');
// Forward geocode
final geocoded = await client.geocoding.geocode('Alfardan Gardens 2, Doha');
// Reverse geocode — isNoAddress is a real answer, not an error
final reverse = await client.geocoding.reverseGeocode(lat: 25.2854, lng: 51.5310);
// Directions — ALWAYS check isDegraded before drawing the polyline
final directions = await client.directions.getDirections(
originLat: 25.2854, originLng: 51.5310, destLat: 25.3548, destLng: 51.4326);
if (directions.isDegraded) {
// 🔴 straight-line estimate: show distance/ETA, do NOT draw as a road,
// re-fetch shortly.
} else {
drawRoute(directions.polyline);
}
// Prefetch (fire-and-forget, never throws on network failure)
await client.directions.prefetchDirections(
originLat: 25.2854, originLng: 51.5310, destLat: 25.3548, destLng: 51.4326);
// Place details + photos
final place = await client.places.getPlace(suggestions.predictions.first.placeId);
final photos = await client.places.listPlacePhotos(lat: 25.2854, lng: 51.5310);
// Tiles — the SDK mints/caches/auto-refreshes the tile token internally
// (24h TTL, refreshed at 80% of lifetime); you never touch it.
final tileUrl = await client.tiles.tileUrl(10, 634, 396, scale: TileScale.x2);
final styleUrl = await client.tiles.styleUrl('raster-nav');
final staticUrl = client.tiles.staticMapUrl(
path: '25.2854,51.5310,25.3548,51.4326', width: 400, height: 200);
client.close();
Error handling — every failure is a typed exception:
try {
await client.geocoding.geocode(address);
} on QartasRateLimitException catch (e) {
await Future.delayed(e.retryAfter); // 429 — back off, then retry
} on QartasBackpressureException catch (e) {
await Future.delayed(e.retryAfter); // 503 — transient, worth retrying
} on QartasBadRequestException catch (e) {
// 4xx — fix your input; e.code / e.message
} on QartasException catch (e) {
// anything else; e.requestId correlates with server logs
}
4. Practical tips
- Memoize aggressively. Cache hits don't consume your daily quota (see rate-limits-and-plans.md), and reusing a response you already hold costs nothing at all.
- Debounce autocomplete — one request per pause-in-typing, not per
keystroke. An empty
qreturns an instant empty answer without billing. - Use
prefetch-directionsat destination-pick time; it makes the real call feel instant. - Check
partial/isDegradedon every directions response — see the caveat above. This is the single most common integration mistake. - Don't over-fetch
@3xtiles;@2xis right for most zooms even on DPR-3 devices (the SDK'sTileScale.autoencodes this policy).
Everything above was executed verbatim against
https://navigation.wslt.app on 2026-07-31 with a live key; the JSON bodies
shown are real responses (truncated only for length). Not independently
verified here: none.