{"openapi":"3.0.0","info":{"title":"Geo.qa Public API","description":"Ask a place what was observed there, and check the answer without an account. Verification endpoints need no credential and never will. Metered endpoints take `Authorization: Bearer on_…`, minted at https://geo.qa/chat/account/api-keys, and are bounded by the owner's storage quota. A refusal returns the full set of ways to reach that resource in `detail`, with `WWW-Authenticate` pointing at https://geo.qa/.well-known/oauth-protected-resource.","version":"2.1.0"},"servers":[{"url":"https://geo.qa","description":"Production"}],"paths":{"/api/tool/satellite/lulc/analyze":{"get":{"tags":["Geospatial tools"],"summary":"Analyze land use at a point","description":"Returns the LULC classification at a single lat/lon point.","security":[{"BearerAuth":[]}],"parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number"},"description":"Latitude."},{"name":"lon","in":"query","required":true,"schema":{"type":"number"},"description":"Longitude."}],"responses":{"200":{"description":"LULC classification result"},"401":{"description":"Missing or invalid API key."},"402":{"description":"Memory storage limit reached. Upgrade plan or free space."},"403":{"description":"Admin-scope key not allowed here — use a key generated at /chat/account/api-keys."}}}},"/api/tool/satellite/lulc/bbox":{"post":{"tags":["Geospatial tools"],"summary":"Analyze land use for a bounding box","description":"Samples a grid across the bounding box and returns LULC proportions plus per-point classifications.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["min_lat","min_lon","max_lat","max_lon"],"properties":{"min_lat":{"type":"number","description":"Minimum latitude (south)."},"min_lon":{"type":"number","description":"Minimum longitude (west)."},"max_lat":{"type":"number","description":"Maximum latitude (north)."},"max_lon":{"type":"number","description":"Maximum longitude (east)."},"grid_size":{"type":"integer","minimum":2,"maximum":20,"default":5,"description":"Grid size (2-20 points per side)."}}},"example":{"min_lat":12.9,"min_lon":77.5,"max_lat":13,"max_lon":77.6,"grid_size":5}}}},"responses":{"200":{"description":"LULC classification result"},"401":{"description":"Missing or invalid API key."},"402":{"description":"Memory storage limit reached."}}}},"/api/tool/satellite/lulc/polygon":{"post":{"tags":["Geospatial tools"],"summary":"Analyze land use inside a polygon","description":"Samples points inside the given polygon and returns LULC classifications.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["coordinates"],"properties":{"coordinates":{"type":"array","items":{"type":"array","items":{"type":"number"}},"minItems":3,"description":"List of [lat, lon] pairs forming the polygon ring (at least 3)."},"sample_points":{"type":"integer","minimum":1,"maximum":100,"description":"Number of points to sample inside the polygon."}}},"example":{"coordinates":[[12.9,77.5],[12.9,77.6],[13,77.6],[13,77.5]],"sample_points":20}}}},"responses":{"200":{"description":"LULC classification result"},"401":{"description":"Missing or invalid API key."},"402":{"description":"Memory storage limit reached."}}}},"/api/tool/camera-stream/detections":{"post":{"tags":["Vision tools"],"summary":"Detections in camera feed","description":"Runs open-vocabulary object detection on a single frame. Exposes the same knobs as the Live Monitor config form: labels to detect (`prompts`), confidence cutoff, alert labels, and an optional natural-language question about the scene.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"image_url":{"type":"string","format":"uri","description":"HTTPS URL of the image/frame to analyze."},"image_base64":{"type":"string","description":"Raw base64 image data or a data: URL."},"prompts":{"type":"array","items":{"type":"string"},"description":"Labels to detect (e.g. ['person','vehicle','fire']). Same as the form's Analysis Prompt."},"confidence_threshold":{"type":"number","minimum":0,"maximum":1,"description":"Drop detections below this score. 0.15 is a reasonable default."},"alert_triggers":{"type":"array","items":{"type":"string"},"description":"Labels that should be flagged as alerts. Matching detections come back with `is_alert: true` and also appear in a top-level `alerts[]` convenience list."},"ask_question":{"type":"string","description":"Optional natural-language question about the scene. When set, a VLM response is returned under `scene_analysis`."},"max_detections":{"type":"integer","description":"Advanced: cap on returned boxes."},"max_dimension":{"type":"integer","description":"Advanced: resize the input so the longer edge matches this."}}},"examples":{"basic":{"summary":"Basic detection","value":{"image_url":"https://example.com/frame.jpg","prompts":["person","car"]}},"withAlerts":{"summary":"With alerts","value":{"image_url":"https://example.com/frame.jpg","prompts":["person","fire","smoke","car"],"alert_triggers":["fire","smoke"],"confidence_threshold":0.25}},"withQuestion":{"summary":"With scene question","value":{"image_url":"https://example.com/frame.jpg","prompts":["person","vehicle"],"ask_question":"Is anything unusual happening in the frame?"}}}}}},"responses":{"200":{"description":"Detection result, optionally with `alerts[]` and `scene_analysis`."},"401":{"description":"Missing or invalid API key."},"402":{"description":"Memory storage limit reached."}}}},"/api/produce/measure":{"post":{"tags":["Measure"],"summary":"Measure produce in a photograph against a marketing standard","description":"Length along the convex face, grade at the mid-point, colour stage, blemish fraction and slenderness, each with its own accuracy. Millimetres need a scale: send `mm_per_px` with a `scale_basis` naming where it came from, or print GET /api/produce/card.png, lay it flat beside the fruit and send neither — the scale is then measured from four markers at a printed spacing rather than typed, and taken at the fruit's own position. A `mm_per_px` with no stated origin is refused. With the card in frame the photograph is rectified onto the card's plane first, so one pixel is one distance in every direction. Every reply carries `model` (a hash over the measuring code and its opencv and numpy versions), a `self_agreement` spread across six measurement variants — a hardness detector, not an error bar — and an ed25519 `receipt` over the image bytes and the numbers, with `verify` giving the five-step offline recipe.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"image_base64":{"type":"string","description":"Raw or data:-prefixed base64."},"image_url":{"type":"string","description":"https URL, as an alternative to image_base64."},"mm_per_px":{"type":"number","description":"Only with scale_basis. Omit entirely when the scale card is in frame."},"scale_basis":{"type":"string","description":"Where mm_per_px came from. Required whenever mm_per_px is given."},"find_card":{"type":"boolean","description":"Look for the printed scale card. Default true."},"stability":{"type":"boolean","description":"Run the six measurement variants. Default true."}}}}}},"responses":{"200":{"description":"{metric, measurement_accuracy, colour, blemish, reference_finger, fingers_separated, background, background_fraction, mask_touches_frame_sides, scale_card, scale_basis, no_card_scale, self_agreement, measures_only, measured_on, regulation, model, receipt, verify, image}"},"400":{"description":"A scale with no stated origin, or an image that could not be decoded. `fix` names the correction."}}}},"/api/produce/card.png":{"get":{"tags":["Measure"],"summary":"Print the scale card","description":"A PNG to print at 100% and lay flat beside the fruit. The marker spacing is printed on the card, so it can be checked against a ruler before a season of measurements is taken with it. Recovers the scale within 0.85% across size, rotation, tilt to 26 degrees, blur, JPEG and noise.","security":[],"parameters":[{"name":"dpi","in":"query","required":false,"schema":{"type":"integer"},"description":"Match your printer. The printed spacing on the card changes with it."}],"responses":{"200":{"description":"image/png"}}}},"/api/produce/selftest":{"get":{"tags":["Measure"],"summary":"Score the measurement against geometry whose answer is exact","description":"Runs the live measuring code over shapes whose true length is known by construction and returns every case. This is the accuracy claim asserted against the running code rather than against a number in a comment — `measured_band` is what this run produced, `published_band` is what we claim, and `reproduce_without_us` is the recipe to rebuild the fixture yourself.","security":[],"responses":{"200":{"description":"{family, shapes_scored, cases[], measured_band, published_band, shape_gate, every_error_is_negative, not_a_photograph, why_that_matters, reproduce_without_us}"}}}},"/api/redact/people":{"post":{"tags":["Measure"],"summary":"Mosaic every person in a photograph before it is stored","description":"Returns the redacted image, what was redacted, what sat near the floor, and whether the floor was actually reached. It refuses to call the result safe: this is a COCO person detector, not an anonymiser — it does not see a face in a mirror, a name on a shirt, or a hand holding an identity document, and `this_is_not_a_guarantee` says so in the response. A mosaic, not a Gaussian: measured here, a 41-pixel blur still correlates 0.977 with what it replaced and the mosaic 0.05. Calling this sends the photograph across a border; the same code runs as a package where the photograph already is (GET /api/partner/dist).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image_base64"],"properties":{"image_base64":{"type":"string"},"floor":{"type":"number","description":"Detector confidence floor. Default 0.05."}}}}}},"responses":{"200":{"description":"{image_base64, redacted, boxes, scores, floor, detector, detector_floor, near_the_floor, below_the_floor, floor_not_achieved, floor_unverified, send_to_a_person, method, why_not_blur, this_is_not_a_guarantee, run_it_yourself, decide_for_yourself}"}}}},"/api/stack/count":{"post":{"tags":["Measure"],"summary":"Count the crate faces in a stack photograph","description":"The count per axis, the pitch it measured, and — when it abstains — which of the three reasons. It refuses a load with no lattice, a stack with a row part out of frame, and a photograph taken at an angle; the last comes back as `retake: square_on`. It counts a FACE, not a load: what is behind the front layer is not in the photograph, which is why `count_is_a_floor` and `errs_towards` are in every reply. Measured over 160 synthetic stacks at four angles: never a wrong count.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image_base64"],"properties":{"image_base64":{"type":"string"}}}}}},"responses":{"200":{"description":"{crate_faces, across, down, abstained, retake, method, count_is_a_floor, errs_towards, model, receipt, verify, image}"}}}},"/api/ppe/spraying":{"post":{"tags":["Measure"],"summary":"Read a spray photograph for respirator, gloves and coverall","description":"Each of the three comes back present / absent / not_visible, with the reason whenever it is not_visible, plus a signed receipt over the image bytes and the verdicts. TODAY EVERY VERDICT IS not_visible, on purpose: there is no calibrated bare-skin band yet and the signal contract forbids a guessed value — `usable` is false and `calibrated` says why. What already works is the refusal path: no person, a person under 100 px, no visible eyes, each with its own reason, so a photograph that cannot answer can never be filed as a pass. Every distance is measured against this subject's own face in this light, so the check does not depend on skin tone. No crop, colour, coordinate or box leaves the call and the image is stored nowhere.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image_base64"],"properties":{"image_base64":{"type":"string"}}}}}},"responses":{"200":{"description":"{respirator, gloves, coverall, usable, why, is_not, no_face_kept, identity, model, receipt, verify}"}}}},"/api/prescreen":{"post":{"tags":["Measure"],"summary":"Rank candidate farms from their pins, before anyone drives out","description":"The pins worth visiting in order, the ones disqualified by a post-cut-off loss year removed from the ranking rather than scored low, and what could not be read at each. It refuses a readiness window — that needs a season of NDVI the shared memory does not hold at an unseeded cell — and it refuses a value range, which is your schedule against your market. A pin is not a plot: this screens which pins are worth a visit, and is not a Due Diligence Statement.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["pins"],"properties":{"pins":{"type":"array","items":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}},"weights":{"type":"object","additionalProperties":{"type":"number"},"description":"Defaults to {plausibly_farmland: 0.6, something_growing: 0.4}. Renormalised over the signals actually read."}}}}}},"responses":{"200":{"description":"{ranked, do_not_visit, could_not_screen, why_could_not_screen, pins_read, pins_unread, weights, how, readiness_window, why_no_readiness_window, no_value_range, why_eligibility_is_not_weighted, a_pin_is_not_a_plot}"}}}},"/api/water/distance":{"post":{"tags":["Measure"],"summary":"Find the nearest recurring surface water, for a spray buffer","description":"A distance BRACKET and the bearing, plus every ring it read. It refuses a single metre: the rings are sampled, so water lies between two radii and saying 412 m would be precision invented out of a sampling grid — `it_is_a_bracket` and `a_gap_this_wide_could_hide_water` are in every reply. Not a watercourse test: JRC recurrence is how often water RETURNS between years, so a ditch that was dry when the satellite passed scores zero and is still a watercourse.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number"},"lng":{"type":"number"},"radii_m":{"type":"array","items":{"type":"number"},"description":"Default [25,50,100,200,350,500]."},"bearings":{"type":"integer","description":"How many bearings to sample per ring. Default 16."}}}}}},"responses":{"200":{"description":"{answer, water_within_m, distance_bracket_m, bearing_deg, band, rings, radii_m, bearings, samples, searched_to_m, at_the_point, unread, some_points_could_not_be_read, a_gap_this_wide_could_hide_water, it_is_a_bracket, what_recurrence_is_not, upstream_reads}"},"400":{"description":"lat and lng are required numbers. `fix` shows a working body."}}}},"/api/crop/agree":{"post":{"tags":["Measure"],"summary":"Check whether a photograph could be of the field it claims","description":"consistent / disagrees / cannot_say, with the reason, and `needs_a_person` on a disagreement. It compares CATEGORIES — could this photograph be of this cell at all — not stages: the stage-against-the-curve comparison needs a season of NDVI the shared memory does not hold at an unseeded cell, and a curve fitted to one point would be the failure this check exists to catch. `taken_at` is strongly recommended; without it the cell reading cannot be matched to the photograph's own moment.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image_base64","lat","lng"],"properties":{"image_base64":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"taken_at":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"{verdict, why, needs_a_person, photo, photo_measurement, cell, what_this_compares, the_colour_stage_caveat, the_stage_comparison_is_not_here_yet}"}}}},"/api/decide":{"post":{"tags":["Measure"],"summary":"Rank candidates on typed features, renormalising over what is known","description":"A ranking, the per-feature contributions, the margin, whether that margin is decisive, and what was left unmeasured. It refuses to price a feature nobody measured: an absent value is renormalised out of the weights rather than scored as zero, so a candidate is never punished for a reading that failed. When the margin is inside `decisive_margin` it says so and hands the choice back — `you_decide` is not a hedge, it is the contract.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["candidates","weights"],"properties":{"candidates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"features":{"type":"object","additionalProperties":{"type":"object","required":["value","known"],"properties":{"value":{"type":"number"},"known":{"type":"boolean"}}}}}}},"weights":{"type":"object","additionalProperties":{"type":"number"}},"decisive_margin":{"type":"number"}}}}}},"responses":{"200":{"description":"{ranked, chosen, margin, decisive, decisive_margin, how, could_not_rank, why_could_not_rank, not_decisive_means, you_decide}"}}}},"/api/partner":{"get":{"tags":["Measure"],"summary":"Read the measurement index","description":"One document describing every measurement route in the words of the service itself: what each does, what it needs, what it returns and what it refuses. Also how to gate your own deploy on ours (GET /api/probes) and how to stop calling us altogether (GET /api/partner/dist).","security":[],"responses":{"200":{"description":"{for, endpoints, gate_your_deploy_on_ours, run_it_yourself, neither}"}}}},"/api/partner/dist":{"get":{"tags":["Measure"],"summary":"Run the same code where the photograph already is","description":"The wheel and the exported detector, with their sha256s, so both can be pinned. Calling the hosted routes sends the photograph across a border; this is the same measuring code as a package, and `verify_it_yourself` gives the hashes to check before installing.","security":[],"responses":{"200":{"description":"{current, current_wheel, files, install, sha256, pin_the_hashes, verify_it_yourself, person_onnx_is, upgrading_from_1_0_0, url, note, why}"}}}},"/api/verify/key":{"get":{"tags":["Measure"],"summary":"Read the public key every receipt is signed with","description":"The ed25519 public key, the schemas it signs, and — for each schema — exactly what the signature binds and what it does NOT bind. A receipt over a clip binds the bytes, the camera and the moment; it does not bind anything a detector produced from them. Reading this before verifying is the difference between checking a signature and believing a claim it never made.","security":[],"responses":{"200":{"description":"{alg, pubkey_b64, signs[], what_is_signed_and_what_is_not}"}}}},"/api/partner/dist/{name}":{"get":{"tags":["Measure"],"summary":"Download one published artefact","description":"The wheel, the superseded wheel or the exported detector, by the exact name GET /api/partner/dist lists. Every response carries `X-SHA256` over the bytes actually sent, so the hash can be checked against the index without hashing the file twice. A name outside the published set is a 404 that lists the set rather than a bare miss. The superseded 1.0.0 wheel stays downloadable on purpose: an integration mid-migration needs a version to pin, not a 404 the morning it changes.","security":[],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"As listed in `files[].name` by GET /api/partner/dist."}],"responses":{"200":{"description":"The bytes, with X-SHA256 and Content-Disposition."},"404":{"description":"Not a published artefact. `published` lists what is."},"503":{"description":"Published but not built on this host."}}}},"/api/probes":{"get":{"tags":["Measure"],"summary":"Gate your deploy on ours","description":"Every claim this service makes, re-checked against the running code, as holds / fails / undetermined. Fail your deploy on `counts.fails > 0` and on `stale`. Do NOT fail on `counts.undetermined`: that means a check did not run, which is a different fact from a claim being broken — most often an upstream we depend on was unreachable, which is not our answer being wrong.","security":[],"parameters":[{"name":"only","in":"query","required":false,"schema":{"type":"string"},"description":"Substring filter, for just the checks you depend on."}],"responses":{"200":{"description":"{probes[], counts, refreshing_now}"}}}},"/api/worlds/london":{"get":{"tags":["Worlds"],"summary":"Read the public London world","description":"Every number geo.qa can currently state about London, with the evidence under it: counts per curated place from that camera's latest retained clip, the clip's sha256, the detector's function id, the three-hour paired-camera trend, and one place taken all the way to a signed receipt this server checked. The bundle is rebuilt about every two minutes; while the next one builds you are served the one already in hand, so a reading can be a little older than that — every panel carries its own age, and that age is the truth. No key.","security":[],"parameters":[{"name":"place","in":"query","required":false,"schema":{"type":"string"},"description":"Narrow `places` to one slug, e.g. trafalgar-square."}],"responses":{"200":{"description":"The world bundle.","content":{"application/json":{"schema":{"type":"object","properties":{"world":{"type":"string","example":"london"},"tenant":{"type":"string","example":"public"},"generatedAt":{"type":"string","format":"date-time"},"featured":{"type":"object","nullable":true},"trend":{"type":"object","nullable":true},"places":{"type":"array","items":{"type":"object"}},"coverage":{"type":"object"},"errors":{"type":"array","items":{"type":"string"},"description":"What could not be read this time, named rather than hidden."}}}}}}}}},"/api/world-model/verify":{"post":{"tags":["Verification"],"summary":"Verify any token or receipt","description":"One door for every token shape: `emem:fact:<cell64>:<cid>`, `emem:bundle:…`, `moment:<id>`, `card:<cid>`, `lineage:<kind>:<id>`, or a bare content id. Send `token` OR `receipt`, not both: a receipt cannot vouch for a token it does not carry, so the pair is refused with 400. Returns `matches` (the verdict), `checked` (whether a check actually ran), `value_verbatim`, `drift`, `signer` and `reason`. A pass is never painted on evidence that was not examined.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"A citation handle."},"receipt":{"type":"object","description":"An inline signed receipt."},"schema_cid":{"type":"string","description":"Required for an emem receipt."},"claimed_value":{"description":"Optional. Enables the drift check."}}}}}},"responses":{"200":{"description":"A verdict. `matches:null` with `checked:false` means the verifier was unreachable and nothing was checked."},"400":{"description":"Neither token nor receipt supplied, or both supplied and unrelated to each other."}}}},"/api/world-model/verify/by-cid/{cid}":{"get":{"tags":["Verification"],"summary":"Verify a receipt by its content id","description":"The keyless verify path the whole product points at (the London page, the MCP tools, llms.txt). Give a receipt's cid (its payload_sha256) and get the full geoqa.verify_verdict.v1 verdict: `valid` (tri-state — true, false, or null when it could not be established), a per-step `checks[]` breakdown (payload hash, signer key, ed25519 signature, schema binding, transparency-log lookup — each with its own result, null meaning not run), `does_not_establish[]` naming what a passing signature does NOT prove, and `signed_at`. Was documented nowhere despite being the advertised path; this is that entry.","security":[],"parameters":[{"name":"cid","in":"path","required":true,"schema":{"type":"string"},"description":"The receipt's payload_sha256 (64 hex chars)."}],"responses":{"200":{"description":"The verdict. HTTP is always 200 for a well-formed cid; read the body — an unknown cid is {found:false, valid:null}, an outage is {checked:false, valid:null}. valid:false is the only state that means a checked-and-failed signature.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyVerdict"}}}}}}},"/api/world-model/verify/receipt":{"post":{"tags":["Verification"],"summary":"Re-verify one signed receipt","description":"Checks a geo.qa ed25519 receipt against the published root of trust. The same check runs offline against https://geo.qa/.well-known/geoqa.json with no call to us at all.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receipt":{"type":"object"}},"required":["receipt"]}}}},"responses":{"200":{"description":"{valid, reason, pubkey_b32}"}}}},"/api/world-model/verify/batch":{"post":{"tags":["Verification"],"summary":"Re-verify up to 256 receipts","description":"Over the limit is refused, never truncated: answering about the first 256 of 260 would read as a full pass to anyone comparing `valid` to `count`. The response carries `submitted` and `complete` for the same reason.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receipts":{"type":"array","items":{"type":"object"},"maxItems":256}},"required":["receipts"]}}}},"responses":{"200":{"description":"{results[], count, valid, submitted, complete}"},"413":{"description":"More than 256 receipts. Split the set and call again."}}}},"/api/world-model/verify/log/sth":{"get":{"tags":["Verification"],"summary":"Signed tree head of the transparency log","description":"The anchor inclusion and consistency proofs are checked against. The signed message is the JSON canonicalization of `payload` with sorted keys and no whitespace; `payload_sha256` is an integrity aid, not the signed bytes.","security":[],"responses":{"200":{"description":"{available, log_id, sth}"}}}},"/api/world-model/verify/log/proof/{cid}":{"get":{"tags":["Verification"],"summary":"RFC 6962 inclusion proof for a content id","security":[],"parameters":[{"name":"cid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{found, leaf_index, tree_size, leaf_hash, audit_path[], root_sha256, sth}. `found:false` is a real answer, not an error."}}}},"/api/world-model/verify/log/consistency":{"get":{"tags":["Verification"],"summary":"Proof the log only ever appended","security":[],"parameters":[{"name":"first","in":"query","required":true,"schema":{"type":"integer"}},{"name":"second","in":"query","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"{first_size, second_size, first_root, second_root, consistency_path[], sth}"}}}},"/api/v1/object-detection":{"post":{"tags":["Open tools"],"summary":"Open-vocabulary object detection","description":"Needs no credential. Rate-limited per IP at 30 requests a minute. Give `image_url` or `image_base64` plus the labels to look for.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"image_url":{"type":"string"},"image_base64":{"type":"string"},"prompts":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"{detections[], total_objects, image_width, image_height, model}"},"429":{"description":"Rate limit exceeded. Retry-After names the wait."}}}},"/api/world-model/verify/agent-card":{"get":{"tags":["Verification"],"summary":"How to check the agent card's signature, and this host checking its own","description":"The card at /.well-known/agent-card.json carries a detached JWS (RFC 7515 §F, alg EdDSA) over its own RFC 8785 canonical form with `signatures` removed. This returns the recipe step by step, the key set to check it against, and a self-check run on the bytes the card route serves — so a card that would not verify in your JOSE library fails here first.","security":[],"responses":{"200":{"description":"{schema, recipe[], construction, jwks, self_check}"}}},"post":{"tags":["Verification"],"summary":"Check an agent card's signature","description":"Send a card (bare, or wrapped as {card}). valid:true checked and passed; valid:false checked and FAILED; valid:null nothing was checked — which is the answer for a card signed by a key this verifier does not hold, together with the jku where it can be checked. A card signed by someone else is never called invalid here.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"card":{"type":"object"}}}}}},"responses":{"200":{"description":"{valid, signed, checks[], kid, alg, canonicalization, construction}"}}}},"/api/world-model/media/receipt/{sha256}":{"get":{"tags":["Verification"],"summary":"The custody receipt for a file, by the file's own hash","description":"Every file uploaded to geo.qa is signed at 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 (sha256sum, or crypto.subtle in a browser — the file itself never has to leave your machine), ask here, and check the signature offline against https://geo.qa/.well-known/geoqa.json. It proves the bytes have not changed since we received them; it does not say the content is true. 404 with found:false means no such bytes are registered — a statement about our records, never a verdict about your file. No credential, now or ever.","security":[],"parameters":[{"name":"sha256","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{64}$"},"description":"Lowercase hex sha256 of the file's bytes."}],"responses":{"200":{"description":"{found, cid, receipt, verify, verify_offline, context?, means}"},"400":{"description":"Not a sha256."},"404":{"description":"{found: false, valid: null, reason} — nothing was checked"}}}},"/api/world-model/a2a":{"get":{"tags":["Open tools"],"summary":"What the A2A endpoint speaks","description":"The methods implemented, the ones deliberately not implemented and why, which skills are served here without a credential, and the keyless budget. No key.","security":[],"responses":{"200":{"description":"{protocol, protocolVersion, methods[], skills, budget}"}}},"post":{"tags":["Open tools"],"summary":"A2A JSON-RPC endpoint","description":"`message/send`, `tasks/get`, `tasks/cancel`, per the agent card at /.well-known/agent-card.json (which is signed). Keyless. Select a skill with a data part {\"skill\":\"<id>\", …arguments}; a text part naming a watched place is read_world. The metered skills answer with a Task in state `auth-required` whose data part is the access ladder. message/stream is refused with -32004 because the card says streaming is false — a capability claimed and not implemented is worse than one declared absent.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","number"]},"method":{"type":"string","enum":["message/send","tasks/get","tasks/cancel"]},"params":{"type":"object"}},"required":["jsonrpc","method"]}}}},"responses":{"200":{"description":"A JSON-RPC response whose result is an A2A Task"},"429":{"description":"The keyless budget is spent. Retry-After names the wait."}}}},"/api/mcp":{"post":{"tags":["Open tools"],"summary":"Model Context Protocol endpoint","description":"JSON-RPC 2.0 over Streamable HTTP. `initialize`, `ping` and `tools/list` need no credential; `tools/call detect_objects` does not either. The metered tools answer with the access ladder rather than a bare refusal. Read `tools/list` for the live tool set rather than trusting any list written in prose.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"method":{"type":"string","description":"initialize | ping | tools/list | tools/call. The first three need no credential."},"params":{"type":"object","description":"Method-specific. For tools/call: { name, arguments }. Read tools/list for the live set rather than a list written here, which is why no enum is pinned."},"id":{"description":"Request id echoed back. Omit for a notification, which expects no response."}}},"examples":{"tools_list":{"summary":"list the live tool set, no credential","value":{"jsonrpc":"2.0","id":1,"method":"tools/list"}},"detect_objects":{"summary":"call the keyless detection tool","value":{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_objects","arguments":{"image_url":"https://example.com/frame.jpg"}}}}}}}},"responses":{"200":{"description":"A JSON-RPC response. A metered tool refused for credentials still returns 200 with the challenge as the tool result, and WWW-Authenticate on the response."},"429":{"description":"Rate limit exceeded. 120/min for reads, 30/min for tools/call, per IP."}}},"get":{"tags":["Open tools"],"summary":"MCP server descriptor","security":[],"responses":{"200":{"description":"Endpoint, tools, docs, payment status."}}}}},"security":[{"BearerAuth":[]}],"components":{"schemas":{"VerifyVerdict":{"additionalProperties":true,"description":"The answer to \"is this receipt genuine?\", from GET /verify/by-cid/{cid} and POST\n/verify/receipt. Three outcomes, kept apart: valid true (every step held), valid false\n(a step ran and failed), valid null (nothing was established; see reason,\nretry_will_help and not_a_transient). A 404 carries found:false with valid null.","properties":{"alg":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Alg"},"checks":{"anyOf":[{"items":{"$ref":"#/components/schemas/VerifyCheck"},"type":"array"},{"type":"null"}],"default":null,"description":"One entry per step. Render one row per check; never collapse them into one badge.","title":"Checks"},"cid":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Cid"},"does_not_establish":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"default":null,"description":"What a pass does not mean, in plain sentences. Show them beside the result.","title":"Does Not Establish"},"emem_raw":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"description":"emem's own verifier answer, verbatim.","title":"Emem Raw"},"established":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Established"},"found":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"description":"by-cid only. false: no retrievable receipt for this cid. null: the store did not answer, so we do not know.","title":"Found"},"hint":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Hint"},"kid_claimed":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Kid Claimed"},"not_a_transient":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Not A Transient"},"payload_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Payload Sha256"},"pubkey":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Pubkey"},"pubkey_encoding":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Pubkey Encoding"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Reason"},"retry_will_help":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Retry Will Help"},"root_of_trust":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Root Of Trust"},"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The schema inside the signed payload -- the authoritative kind.","title":"Schema"},"schema_asserted":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Schema Asserted"},"schema_verified":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Schema Verified"},"signed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"When the receipt was signed (ISO 8601).","title":"Signed At"},"signer":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"geoqa, or emem for an emem receipt.","title":"Signer"},"trusted_pubkey_b64":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Trusted Pubkey B64"},"valid":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true: verified. false: checked and failed -- the only accusing value. null: not established, which is not a statement that anything failed.","title":"Valid"},"valid_null_means":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Valid Null Means"},"verified_offline":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"description":"true: this verdict can be reproduced offline from the receipt and our published key, with no call to us.","title":"Verified Offline"}},"required":["valid"],"title":"VerifyVerdict","type":"object"},"VerifyCheck":{"additionalProperties":true,"description":"One step of a verification, named, with what it compared against what.","properties":{"against":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"description":"What was compared against: the signer, kid and pubkey for signer_key; the log id and its independent witness count for logged.","title":"Against"},"check":{"description":"Which step. Each is re-run on its own, so a failure names its step.","enum":["payload_hash","signer_key","signature","envelope_schema","schema","logged"],"title":"Check","type":"string"},"kind":{"description":"What sort of check this is: cryptographic hash, source identity, cryptographic, binding, or record lookup (not a proof).","title":"Kind","type":"string"},"proof":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Where to check this yourself (logged).","title":"Proof"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Why a result is false or null.","title":"Reason"},"result":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true: the comparison held. false: it ran and did not hold. null: it did not run (not reached because an earlier step failed, or not asked for); see reason.","title":"Result"},"what":{"description":"The comparison, in one sentence.","title":"What","type":"string"},"witness_note":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Present on logged while no independent party co-signs the log's head.","title":"Witness Note"}},"required":["check","kind","what","result"],"title":"VerifyCheck","type":"object"}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"on_…","description":"Generate a key above. Use it as `Authorization: Bearer on_…`. Keys are scoped to the public API only — they cannot access admin endpoints."}}},"x-spec-source":"static-fallback"}