Answers
Three endpoints, three questions. None of them return a coordinate, a distance, or a bearing — not in the response, and not in an error.
Every answer carries the same freshness stamp:
| Field | Meaning |
|---|---|
asOf | ISO timestamp of the fix the answer was computed from, or null |
ageSeconds | How old that fix was, or null when there is no fix at all |
reason | Why the answer is unknown — stale, no_data, or boundary |
Knowing the age of a fix tells you nothing about where someone is, so you get it even when the answer itself is unknown. Use it to decide whether to ask again later or give up.
Am I inside this zone?
POST /v1/answers/verify-zone
Authorization: Bearer cka_…
Content-Type: application/json
{
"lat": 37.7749,
"lon": -122.4194,
"radius_m": 250,
"label": "Shell on 5th",
"max_age_s": 900
}
{
"state": "inside",
"reason": null,
"asOf": "2026-08-24T09:15:00.000Z",
"ageSeconds": 12
}
state is inside, outside, or unknown. It's a POST because the zone belongs in the body — a query string would put your business geography into access logs, CDN logs, and browser history. It's a read, so it answers 200, never 201.
label is required, and the user reads it. It appears verbatim in their access log: asked: near "Shell on 5th" (37.77,-122.42 r=250m)? → outside. Write it as the reason you're asking, not as an internal identifier. zone_4718 looks like something to be suspicious of; Shell on 5th looks like a fuel purchase being verified.
radius_m is clamped to 100–10 000 metres. Below 100 m a zone stops being a question and starts being a way to pin someone down; that request is rejected rather than silently widened.
Which place am I at?
GET /v1/answers/current-place?max_age_s=900
{
"place": { "id": "…", "label": "Home", "kind": "home" },
"state": "at_place",
"reason": null,
"asOf": "2026-08-24T09:15:00.000Z",
"ageSeconds": 12
}
state is at_place, no_place, or unknown. Only places the user shared with you are considered, so no_place genuinely means "not at any place you can see" — it does not mean "not at a place." That ambiguity is deliberate and load-bearing: it's what stops the absence of an answer from being an answer.
When places overlap, the smallest circle wins, because it's the more specific truth: "the gym", not "downtown".
Am I at this specific place?
GET /v1/answers/places/{place_id}/presence?max_age_s=900
{
"present": true,
"state": "yes",
"reason": null,
"asOf": "2026-08-24T09:15:00.000Z",
"ageSeconds": 12
}
state is yes, no, or unknown; present is the same thing as a boolean, and is null when the state is unknown — never false. Don't collapse unknown into false, or you'll tell users they left somewhere they never did.
A place that wasn't shared with you returns 404, exactly like a place that doesn't exist. You cannot discover place ids by probing.
Three behaviours that will surprise you
These are the parts most integrations get wrong on first contact.
A margin, not a hard edge
A fix is only called inside or outside when it clears the circle's edge by max(50 m, the fix's own accuracy). Inside that band you get unknown with reason boundary.
This is deliberate. Without it, an app could ask about circle after circle, each slightly smaller, and read the inside/outside flip as a coordinate. With it, there's a band we will never resolve, no matter how many times you ask.
Practically: a user standing near the edge of your zone will produce boundary, repeatedly. Don't treat that as a transient glitch to retry through. Either widen the zone, or treat unknown as "no change" and keep your previous state.
Stale never means confident
If the newest fix is older than max_age_s, you get unknown / stale — even when that fix was squarely inside your zone. We will not answer confidently from old data.
max_age_s accepts 60–86 400 seconds and defaults to 900. Tightening it buys certainty and costs availability: a stricter window means more unknown. Pick based on what a wrong answer costs you. Unlocking a door deserves 60 seconds; tagging a photo can live with a day.
Unknown is a real answer
All three endpoints can return unknown, and a correct integration handles it explicitly. The reason tells you what to do:
| Reason | What happened | What to do |
|---|---|---|
stale | We have a fix, it's too old | Retry later, or widen max_age_s |
no_data | This user has never sent a location | Nothing to retry — check they've set up a device |
boundary | They're too near the edge to call | Keep your previous state; don't retry in a tight loop |
Budgets
These endpoints share an anti-triangulation budget: 60 calls/hour and 300/24 hours per connection, across all three combined. verify-zone additionally limits how many distinct areas you may ask about in a day.
Polling one zone often is fine. Sweeping many is not — see Errors and limits.