Skip to main content

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.

basehttps://geo.qa/api
mcphttps://geo.qa/api/mcp
authBearer on_… (public-scope key)
openapi/api/public-docs/openapi · /llms.txt
statusvision + satellite tools live · MCP live
Earth-observation facts cite emem.dev · ed25519 · BLAKE3 · verifiable offline

§ 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

POST /api/tool/camera-stream/detections
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.

nounwhat it is
placeWhere. 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.
bandWhat. 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.
frameAn 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.
factThe 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.
receiptThe 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.

POST
/api/v1/object-detection. Open-vocabulary detection. No auth, rate-limited per IP.
POST
/api/tool/camera-stream/detections. Detection with alert_triggers and an optional ask_question. Bearer key.
POST
/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.

GET
/api/tool/satellite/lulc/analyze?lat=..&lon=... Land cover at a single point.
POST
/api/tool/satellite/lulc/bbox. Samples a grid across a box.
POST
/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.

every numbercarries the band it was measured over
every refusalsays what it could not read, and why
nothingreturns a guessed value

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

POST
/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.
GET
/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.
GET
/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

POST
/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.
POST
/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

POST
/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.
POST
/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.
POST
/api/water/distance. Nearest recurring surface water, for a spray buffer.
POST
/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.
POST
/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:

read_worlda whole place at once: what the cameras see now, how that compares with three hours ago, and the receipt under every number (no key needed)
verify_receiptre-check any geo.qa or emem receipt, including one you were handed by someone else. valid:null means nothing was checked, never a pass in disguise (no key needed, now or ever)
detect_objectsopen-vocabulary detection on an image (no key needed)
classify_land_coversatellite LULC for a bounding box or polygon
analyze_scenevision-language description of a frame

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.

mcp.json
{
  "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:

GET
/api/public-docs/openapi. The machine-readable OpenAPI 3.0 spec — generate a typed client from it.
GET
/.well-known/agent-card.json. The A2A agent card, every tool published as a skill. Also served at /agent.json.
GET
/.well-known/oauth-protected-resource. RFC 9728 metadata for this API. A refusal from /api/mcp points here in WWW-Authenticate.
GET
/llms.txt. A curated index for LLM crawlers and agent frameworks.
GET
/.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.

statusmeaning
400request shape or parameter error; fix and resend
401missing, malformed, or revoked key
402memory storage limit reached; free space or upgrade your plan
403admin-scope key used on the public API; mint a public-scope key
429rate-limited; back off per Retry-After
5xxour 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 nowObject 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 progressDurable 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.
AheadSigned 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