The API · v1
Read it once, wire it once.
What you need to mint a key, detect objects in a frame, classify land cover from satellite, describe a scene, and add the MCP server to your agent. Every endpoint below is live today at https://geo.qa/api. For Earth-observation facts, geo.qa builds on the open emem.dev ledger, whose answers carry verifiable receipts.
§ 1
Quickstart
Three steps. Mint a key, ask geo.qa to detect what’s in a frame, and read the result. The same key unlocks the satellite tools and the MCP server.
1 · Mint a key
Sign in at geo.qa, then generate a key from Account → API keys. Keys are public-scope: they call the public API and cannot reach admin endpoints. Usage is bounded by your plan on the pricing page. The secret is shown once.
2 · Detect objects in a frame
curl -X POST https://geo.qa/api/tool/camera-stream/detections \ -H "Authorization: Bearer $GEOQA_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_url": "https://example.com/frame.jpg", "prompts": ["person", "vehicle", "truck"], "alert_triggers": ["truck"], "confidence_threshold": 0.25 }'
Detection returns boxes, labels and confidences. Any prompt in alert_triggers comes back with is_alert: true and in a top-level alerts[] list. Add ask_question for a vision-language read of the scene under scene_analysis.
What is answering. The detector on duty is closed-set — a COCO-trained box model on CPU — and it names itself in model on every response. A prompt it has no class for is refused, by name, in refused_prompts, rather than answered with an empty list: an empty list here means looked and found none. GET /api/v1/object-detection — the same path without a body — lists exactly what it can return and what it refuses by policy, so an agent can read the closed set before it sends a prompt. Prompts that are a measurement rather than a sighting — building, road, water — are refused too, naming the corpus that holds the real answer.
3 · Read the response
{
"detections": [
{ "label": "car", "confidence": 0.9063,
"bbox": [x1,y1,x2,y2], /* corners in pixels, not x/y/w/h */
"bbox_normalized": [0.259,0.322,0.400,0.393],
"is_alert": false },
/* … */
],
"total_objects": 21,
"model": "fasterrcnn_mobilenet_v3_large_fpn (cpu_detect@3)",
"refused_prompts": [
{ "prompt": "fire", "reason": "closed-set; no class for this" }
],
"alerts": [],
"scene_analysis": null
}
No account? POST /api/v1/object-detection runs the same detector without a key, rate-limited per IP — the fastest way to try it. See § 4.
§ 2
Concepts
geo.qa fuses two kinds of source. Your own sensors — camera streams, drones, uploaded frames — run through the vision tools. Earth observation — satellite land cover, indices, weather — is resolved against the open emem.dev grid. Both answer against one addressable memory of ground.
| noun | what it is |
|---|---|
| place | Where. A lat/lon, a bounding box, or a polygon. Satellite tools resolve a place to its ground cell on the open emem.dev grid (~9.55 m), so the same spot cites the same way across sources. |
| band | What. A named measurement: a land-cover class, a vegetation index, a weather field, a detection label. The satellite bands come from the emem.dev registry; the vision labels are open-vocabulary prompts you supply. |
| frame | An observation. A single image or video frame from a sensor you run. Pass it as an image_url or image_base64. The raw frame is analyzed and discarded; only the result is returned. |
| fact | The answer. A value at a place with its provenance — a detection with a score, a land-cover proportion, an index reading. Earth-observation facts carry the source COG/scene they were materialized from. |
| receipt | The proof. For facts cited from the open emem.dev ledger: canonical CBOR → BLAKE3 hash → ed25519 signature, verifiable offline against the signer’s public key. See § 9. |
§ 3
Authentication
The authenticated tools take a public-scope key as a bearer token. Generate one from Account → API keys in the app; it starts with on_. Public keys call the public API and are rejected (403) on admin endpoints.
Authorization: Bearer on_…
One tool is open: POST /api/v1/object-detection needs no key and is rate-limited per IP, so you can try detection before you sign up. Everything under /api/tool/… and /api/scene needs a key, and each call is bounded by your memory-storage quota — a full quota returns 402 (see § 10).
MCP clients (§ 7) pass the same key in an X-API-Key header, which the server forwards to the tool it proxies.
§ 4
Vision tools
Analyze a frame from any camera, drone, or upload. These run the detection and vision-language heads geo.qa serves for its own sensor pipeline.
/api/v1/object-detection. Open-vocabulary detection. No auth, rate-limited per IP./api/tool/camera-stream/detections. Detection with alert_triggers and an optional ask_question. Bearer key./api/scene. Vision-language scene description — a human-readable read of what’s in the frame. Bearer key.Request body
{
"image_url": "https://…", // or image_base64 (raw or data: URL)
"prompts": ["person", "vehicle"], // labels to detect, up to 20
"confidence_threshold": 0.15, // drop weaker boxes
"alert_triggers": ["fire"], // flag these as alerts
"ask_question": "anything unusual?" // optional VLM read
}
§ 5
Satellite tools
Land-use / land-cover from satellite, at a point, across a box, or inside a polygon. These resolve a place against Sentinel-2/Landsat and return class labels with fractional coverage. All take a Bearer key.
/api/tool/satellite/lulc/analyze?lat=..&lon=... Land cover at a single point./api/tool/satellite/lulc/bbox. Samples a grid across a box./api/tool/satellite/lulc/polygon. Samples points inside a polygon ring.# land cover across a bounding box curl -X POST https://geo.qa/api/tool/satellite/lulc/bbox \ -H "Authorization: Bearer $GEOQA_KEY" \ -H "Content-Type: application/json" \ -d '{ "min_lat": 12.9, "min_lon": 77.5, "max_lat": 13.0, "max_lon": 77.6, "grid_size": 5 }'
For richer Earth-observation — vegetation indices, surface water, forest loss, weather, similarity search — call the open emem.dev protocol directly, or let geo.qa cite it for you (§ 9).
§ 6
Measurement
Detection tells you what is in a frame. These endpoints tell you how big it is, in millimetres — and they are built on one rule that is unusual enough to state first.
A length from a photograph with no scale in it is not a length, so we do not return one. An unreadable pin is not a bad pin, so it is not scored zero. If a photo cannot answer the question, you get the reason instead of a number — which is the part your auditor will care about.
Produce
/api/produce/measure. Finger length, grade, blemish and colour stage from a photograph, against EU 2023/2429. Returns reference_finger with length_mm and grade_mm only when a scale is present — otherwise pixels and a why_no_mm./api/produce/card.png. The printable ArUco scale card. Print at 100%, lay it beside the fruit. The geometry is in the file and on the card, so it can be checked against a ruler before a season of photographs is taken with a mis-scaled one./api/produce/selftest. The accuracy band, re-measured live on this host. Not a constant we typed in.# measure a banana. no key, no account, nothing to sign up for. printf '{"image_base64":"%s"}' "$(base64 -w0 banana.jpg)" > body.json curl -s https://geo.qa/api/produce/measure \ -H 'content-type: application/json' -d @body.json # no scale in the frame, so no millimetres — and it says why → { "metric": false, "reference_finger": { "length_px_along_convex_face": 1346.3, "why_no_mm": "no scale anchor was supplied…" }, "colour": { "stage": 7, "label": "full yellow" }, "self_agreement": { "disagreement_pct": 0.83, "stable": true } } # print the card, lay it beside the fruit, and the millimetres are MEASURED curl -s https://geo.qa/api/produce/card.png -o card.png && lp card.png
That is the whole contract in one call. metric: false is not an error — it is the honest answer to a photograph with no scale in it, and why_no_mm tells you exactly what to add. Put the card in the frame and the same call returns millimetres, with scale_basis naming where they came from. disagreement_pct is a hardness detector, not an error bar: it measures how far six variants of the same measurement sit apart, which understates the true error five to tenfold on an easy photograph. Use measurement_accuracy for the error.
People, privacy first
/api/redact/people. Mosaic every person before a photo is stored, and report what was near the floor — the weak detections that might have been missed. A mosaic, never a blur: published attacks recover faces from blurs./api/ppe/spraying. Respirator, gloves and coverall, keeping no face. Every verdict is not_visible today — there is no calibrated band yet and we will not guess one. What already works is refusing a photograph that cannot answer, so it can never be filed as a pass.Fields and lots
/api/prescreen. Rank candidate farms from pins. Pins that could not be read come back under could_not_screen with every band named — not scored zero at the bottom of the list, which is indistinguishable from land that was read and found poor./api/crop/agree. Does this photograph agree with the field it claims? Keeps disagrees and cannot_say apart, and routes only the first to a person./api/water/distance. Nearest recurring surface water, for a spray buffer./api/stack/count. Crate faces in a stack, or an abstention. It counts a face, not a load — what is behind the front layer is not in the photograph./api/decide. Rank candidates on typed features, renormalising over what is actually known. A candidate with no known feature is not ranked last; it is not ranked.What a measurement looks like
{
"reference_finger": {
"length_mm": 201.4,
"grade_mm": 33.8,
"grade_measured_by": "sub-pixel edge"
},
"measurement_accuracy": { // the band, always
"length_pct": [-2.3, 0.9],
"grade_pct": [-0.9, 1.5],
"basis": "24 arcs, scored against coverage area"
},
"receipt": { "signature": "…" } // verifies offline
}
Receipts, and checking them without us
Every measurement is signed over the image bytes and the numbers together. GET /api/verify/key returns the ed25519 key; verification needs nothing of ours and does not ask this host whether the answer is yes. A measurement in a compliance record that cannot be re-run is not evidence.
Running it in your own region
Calling these endpoints sends the photograph across a border, which for a photo of a person is the thing the requirement is trying to avoid. GET /api/partner/dist serves the same code as an installable wheel, with the hash to pin it to. model_identity() returns two hashes: the module alone, identical anywhere, and one that also pins OpenCV and NumPy, because a library upgrade can move a distance transform while the source is untouched.
The full integrator index, with what each endpoint refuses and why, is at GET /api/partner — and as a page you can read at geo.qa/partner, which is generated from the running service rather than written out here, so it cannot drift from what the API actually does.
§ 7
MCP server
geo.qa runs a native Model Context Protocol server over Streamable HTTP at https://geo.qa/api/mcp. It wraps the same tools above so an agent can use them natively. Five tools today — and tools/list is the authority, not this list:
Client config
Drop this into your MCP client (Claude Desktop, Cursor, Windsurf, any agent runtime). The key is a public-scope key from your account; the open detect_objects tool works even without it.
{
"mcpServers": {
"geoqa": {
"transport": "streamable-http",
"url": "https://geo.qa/api/mcp",
"headers": {
"X-API-Key": "on_…"
}
}
}
}
Discover the live schema by POSTing {"jsonrpc":"2.0","id":1,"method":"tools/list"} to /api/mcp. For the ~81-tool Earth-observation surface (NDVI, water, forest, weather, receipts), point a second MCP client at the open emem.dev protocol.
§ 8
Discovery
Everything an agent needs to find and call the API on its own:
/api/public-docs/openapi. The machine-readable OpenAPI 3.0 spec — generate a typed client from it./.well-known/agent-card.json. The A2A agent card, every tool published as a skill. Also served at /agent.json./.well-known/oauth-protected-resource. RFC 9728 metadata for this API. A refusal from /api/mcp points here in WWW-Authenticate./llms.txt. A curated index for LLM crawlers and agent frameworks./.well-known/ai-plugin.json. OpenAI-compatible tool manifest.There is no separate published SDK to install — generate one from the OpenAPI spec, or add the MCP server for native tool use. Field names are stable; breaking changes ship under a new /v2 path.
The spec covers the whole public surface, including every measurement route in § 6 — it did not until 2026-09-29, when a check of each documented path against the served document found the measurement half missing from it entirely. A generated client built before that date has the verification routes and none of the measuring ones; regenerate it. Two checks now run against production continuously: documented-paths-are-routed calls every /api path this page shows, and public-surface-answers calls everything the partner index names. Both are on /api/probes.
§ 9
Recall & receipts
geo.qa’s Earth-observation answers are grounded in the open emem.dev ledger: a signed, content-addressed memory of Earth built on public satellite and climate sources. When geo.qa cites an EO fact, it carries a receipt from that ledger.
A receipt is canonical CBOR → a BLAKE3 content hash → an ed25519 signature. It verifies offline against the signer’s public key, with no callback to us. Every route below takes no credential, now or ever: an answer you cannot check without an account is not worth much.
# one door, any token shape. no key, no account. curl -s https://geo.qa/api/world-model/verify \ -H 'content-type: application/json' \ -d '{"token":"emem:fact:defi.zb4e6.fIsI.hujO:oqdnpskm73q53cnuo46ieyb5dvtxr4fey6qlffxtkjqbzqe7y5gq"}' → { "matches": true, "value_verbatim": "0.23483583750695602", "signer": "777er3yih…" } # or skip us: pin the key and check it in your own process curl -s https://geo.qa/.well-known/geoqa.json
Check a file, not a receipt
Every file uploaded to geo.qa is signed the moment it arrives: an ed25519 signature over the sha256 of the bytes as stored, their length, their media type and the time. Hash a file you were handed and ask whether we hold that record — the file never has to leave your machine, only the hash is sent.
# what geo.qa holds for these exact bytes. no key, no account. curl -s https://geo.qa/api/world-model/media/receipt/$(sha256sum ./drone.mp4 | cut -d' ' -f1) → { "found": true, "receipt": {…}, "verify": "…/verify/by-cid/<cid>" } → 404 { "found": false } — no record of those bytes. That is a statement about our records, not a verdict about your file.
It proves the bytes have not changed since we received them. It does not say the content is true, and a coords block, when present, is what the FILE claimed, not a verified location. The same check is on the public verifier, which hashes in your browser.
Also unauthenticated: POST /api/world-model/verify/receipt for a whole receipt object, /verify/batch for up to 256 at once, and the transparency log at /verify/log/sth, /verify/log/proof/{cid} and /verify/log/consistency. There is a paste-a-receipt page at /verify that needs no sign-in.
A single verifier checks any emem-format receipt. Durable signing of private-tenancy facts (your own cameras and sensors) is rolling out — see the roadmap in § 12.
Preview
Private tenancy
Beyond the stateless tools above, geo.qa is building a private, per-tenant memory: register a sensor once, then recall its dated observations against a place and window, run monitoring loops, and receive signed webhooks. The pieces exist inside the app (sensor registry, saved streams, monitored areas, world-model recall); a stable public REST surface for them is in private preview, not yet generally available.
If you want early access to per-tenant recall, monitoring loops, or scoped agent keys, talk to us. We’ll turn it on for your tenancy and document the exact shapes as they ship — we won’t list an endpoint here until you can call it.
§ 10
Errors
Standard HTTP status codes with a machine-readable code and a human message in the body.
| status | meaning |
|---|---|
| 400 | request shape or parameter error; fix and resend |
| 401 | missing, malformed, or revoked key |
| 402 | memory storage limit reached; free space or upgrade your plan |
| 403 | admin-scope key used on the public API; mint a public-scope key |
| 429 | rate-limited; back off per Retry-After |
| 5xx | our side; the request is safe to retry |
§ 11
Rate limits
The anonymous detector (/api/v1/object-detection) and the MCP endpoint are rate-limited per IP with a token bucket; responses carry X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 carries Retry-After. Authenticated tools are not per-call metered — they draw against your plan’s memory-storage quota, and return 402 when it is full. Your daily allowances follow your tier on the pricing page.
§ 12
Status & roadmap
geo.qa is early and we keep this honest. Here is what is live today and what is still ahead — no version history we didn’t ship.
| Live now | Object detection, camera-stream detections, scene analysis, satellite LULC (point / bbox / polygon), the native 3-tool MCP server, OpenAPI + llms.txt + ai-plugin.json discovery, and receipt verification against the open emem.dev ledger. |
|---|---|
| In progress | Durable ed25519 signing of private-tenancy facts; a stable public REST surface for per-tenant recall and monitoring loops (today in the app, in private preview for the API); scoped agent keys. |
| Ahead | Signed webhooks, world-model training on your own memory, and airgapped tenancy delivery. We’ll document each here the day it can be called, not before. |
Call one endpoint today.
Hit the open detector with a frame URL, or mint a key and classify land cover from satellite. Everything on this page is live — nothing here is a promise you can’t call.
geo.qa · a vortx ground decoder · emem.dev open protocol