Skip to main content
Skip to content

The API, version 1

Read it once, wire it once.

Start with no key: the MCP server lists its tools, reads places and detects objects for anyone. A key is for the REST API and the metered tools: land cover from satellite and scene description. 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
authnone for reads over MCP; Bearer on_… (public-scope key) for REST and the metered tools
openapi/api/public-docs/openapi and /llms.txt
statusvision and satellite tools live; MCP live
Earth-observation facts cite emem.dev, with Ed25519 and BLAKE3: you can check them offline

§ 1

Quickstart

Four steps. The first needs no key: ask the MCP server what is in a frame. Then mint a key for the REST API and the metered tools, and read the result.

1. Ask with no key

The MCP server answers reads with no key: tools/list, read_world, detect_objects and verify_receipt. This call asks it what is in one frame from a TfL street camera at Trafalgar Square:

POST /api/mcp, tools/call detect_objects
curl -X POST https://geo.qa/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"detect_objects","arguments":{"image_url":"https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk/00001.06502.jpg"}}}'

The answer lists each object with its box, label and confidence. Any MCP client can make the same call: claude mcp add --transport http geoqa https://geo.qa/api/mcp.

2. Mint a key for the REST API and the metered tools

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. Your plan on the pricing page sets the limits. We show the secret one time only.

3. Detect objects over REST

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 has a closed set of classes: a COCO-trained box model on CPU. It names itself in model on every response. When a prompt has no class, the detector refuses it by name in refused_prompts. It does not answer with an empty list: an empty list means that it looked and found none. GET /api/v1/object-detection (the same path with no body) lists all that it can return and all that it refuses by policy. Thus an agent can read the closed set before it sends a prompt. The detector also refuses prompts that are a measurement, not a sighting (building, road, water), and it names the corpus that holds the real answer.

4. 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 uses two kinds of source. Your own sensors (camera streams, drones, uploaded frames) go through the vision tools. For Earth observation (satellite land cover, indices, weather), geo.qa reads the open emem.dev grid. Both answer from one memory of the ground, with an address for each cell.

nounwhat it is
placeWhere. A lat/lon, a bounding box or a polygon. Satellite tools find the ground cell of a place on the open emem.dev grid (about 9.55 m). Thus the same spot has the same citation in all 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 or an index reading. Earth-observation facts name the source COG or scene that made them.
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. Admin endpoints refuse them (403).

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. Your memory-storage quota limits each call: 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. They keep one rule, and we state it first because it is unusual.

every numbercarries the band of its measurement
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. Thus you can check it against a ruler before you take a season of photographs with a card at the wrong scale.
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 and put it beside the fruit: then the call MEASURES millimetres
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 shows how hard a photograph is; it is not an error bar. It measures the spread of six variants of the same measurement. On an easy photograph it is five to ten times smaller than the true error. Use measurement_accuracy for the error.

People, privacy first

POST
/api/redact/people. Mosaic every person before geo.qa stores a photo, and report what was near the floor: the weak detections that the model possibly did not see. 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: the endpoint refuses a photograph that cannot answer, so nobody can file it 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

geo.qa signs each measurement 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 first is for the module alone, and it is the same everywhere. The second also pins OpenCV and NumPy, because a library upgrade can move a distance transform when the source does not change.

The full integrator index, with what each endpoint refuses and why, is at GET /api/partner. You can also read it as a page at geo.qa/partner. The running service makes that page, so it cannot drift from what the API 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, the change from three hours ago, and the receipt for each number (no key)
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 come from 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.

geo.qa signs two kinds of receipt. Both use Ed25519, and you can check both offline, with no call to us. No route below needs a key.

receipthashdata that the hash covers
emem.dev fact (satellite and climate facts)BLAKE3canonical CBOR of the fact
geo.qa receipt (for example a London camera clip, geoqa.clip.v1)SHA-256canonical JSON of the payload: sorted keys, no spaces, UTF-8, no floating-point values

To check a geo.qa receipt yourself:

  1. Get it: GET /api/world-model/verify/by-cid/{cid}?include_receipt=true.
  2. Make the canonical JSON of receipt.payload.
  3. Hash it with SHA-256. The hex value must equal payload_sha256 and the cid.
  4. Check signature_b64 with Ed25519 over the canonical bytes, not over the hash.
  5. The key pubkey_b64 must equal the key in /.well-known/geoqa.json.

The full receipt comes back only for public kinds of receipt (today geoqa.clip.v1). For other kinds, the answer gives the server’s verdict and a receipt_withheld field that says why. The Verify page does all of this in your browser.

# 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

geo.qa signs each uploaded file when it arrives. The ed25519 signature covers the sha256 of the stored bytes, their length, their media type and the time. To check a file that you got, hash it and ask if we hold that record. Only the hash leaves your machine, not the file.

# 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 will turn it on for your tenancy and document the exact shapes when they ship. We do not list an endpoint here before 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
429too many requests from this address; wait for Retry-After, then try again once
5xxour side; the request is safe to retry

A check can fail, pass or be undetermined. Undetermined means that the check did not run, for example because an archive did not answer. Stop a release on a fail, not on an undetermined result.

§ 11

Rate limits

There are two layers of limits.

1. The anonymous detector (/api/v1/object-detection) and the geo.qa MCP endpoint (/api/mcp) use a token bucket for each address. Each answer carries X-RateLimit-Remaining and X-RateLimit-Reset.

2. The public live-data routes have a limit for each address at the edge:

routesteady rateburst
/mcp (the DeHaat MCP)5 requests a second20
/api/worlds/*5 requests a second20
/api/world-model/*10 requests a second40

Above a limit, the answer is 429 with Retry-After: 1 and a JSON body with "error":"rate_limited". Wait one second, then try again once. Tools that need a key are not metered per call: they use the memory-storage quota of your plan, and they return 402 when it is full. See pricing.

§ 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 will document each here on the day that you can call it, 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 managed deployment of the open emem.dev protocol, by Vortx AI