Asking questions
Answers
Three endpoints, three questions. None return a coordinate, a distance, or a bearing — not in a response, and not in an error.
Every answer carries the same freshness fields:
| Field | Meaning |
|---|---|
asOf | Timestamp of the fix the answer was computed from, or null |
ageSeconds | How old that fix was, or null if there is no fix |
reason | Why the answer is unknown: stale, no_data, or boundary |
asOf is not when you asked. We answer from the newest fix we hold, so inside with an asOf of 09:15 means as of 09:15 they were inside — read every answer that way before acting on it.
The age of a fix tells you nothing about where someone is, so you get it even when the answer is unknown.
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 so the zone travels in the body. A query string would put your business geography into access logs and browser history. It's a read, so it returns 200.
label is required, and the user reads it. It appears in their log as asked: near "Shell on 5th" (37.77,-122.42 r=250m)? → outside. Write it as the reason you're asking. zone_4718 looks suspicious; Shell on 5th looks like a fuel purchase being checked.
radius_m is 100–10,000 meters. The floor is the privacy control: under 100 m a zone stops being a question and becomes a way to pin someone down, so small requests are rejected rather than quietly widened. The ceiling is only a sanity bound — a large zone gives away very little, so err on the generous side.
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 shared with you count. no_place means "not at any place you can see" — not "not at a place".
When places overlap, the smallest one wins: "the gym", not "downtown".
Am I at this place?
GET /v1/answers/places/{place_id}/presence?max_age_s=900
{
"state": "yes",
"reason": null,
"asOf": "2026-08-24T09:15:00.000Z",
"ageSeconds": 12
}
state is yes, no, or unknown. There is deliberately no boolean beside it: a boolean has two values and this answer has three, so branch on state and handle unknown on its own. Don't fold it into no, or you'll tell users they left somewhere they never did.
A place not shared with you returns 404, exactly like one that doesn't exist.
Three things that will surprise you
There's a margin at the edge
We only answer inside or outside when the fix clears the circle's edge by max(50 m, the fix's own accuracy). Within that band the answer is unknown, with reason boundary.
Without it, yes/no questions would give up coordinates. Ask about a circle, then a slightly smaller one, then smaller again: the radius at which the answer flips is the user's distance from your center, and three of those from three centers is a position. The margin blurs the edge by more than you can usefully shrink the circle, so the search never converges. It also keeps a phone sitting on a boundary from flapping between answers.
The dashed line is the 150 m circle in both. Only the phone changed. The green core is where we will commit to yes; the amber ring is where we refuse to guess; past it the answer is no.
Both circles are 150 m. The only thing that changed is how well the phone knows where it is — and that decides whether the place has a middle at all.
A phone on Wi-Fi usually knows its position to within about 10 m, so the band stays at its 50 m floor and a 150 m place answers yes across its inner 100 m. The same phone on cell towers alone can be 150 m out, the band grows to match, and the place has no middle left at all. This is why arrivals are far more reliable once someone's phone joins their home Wi-Fi: nothing about the place changed, the fix just got sharper.
Note the floor cuts both ways. Below 50 m of error, extra accuracy buys nothing — a 5 m fix and a 50 m fix produce exactly the same band, on purpose.
In practice: someone standing near the edge of your zone produces boundary repeatedly. Don't retry through it. Either widen the zone, or treat unknown as "no change" and keep your previous answer.
It also means a zone must be bigger than the margin to have an inside at all. A 100 m zone read by a 50 m-accurate fix has almost none. Size your zones accordingly — the places a user draws are held to a 150 m minimum for exactly this reason, and your own zones get no such floor.
Old data never gets a confident answer
If the newest fix is older than max_age_s, you get unknown / stale, even if that fix was well inside your zone.
max_age_s accepts 60–86,400 seconds and defaults to 900. A tighter window means more certainty and more unknown. Choose based on what a wrong answer costs: a door lock deserves 60 seconds, a photo tag can use a day.
unknown is a real answer
| Reason | What happened | What to do |
|---|---|---|
stale | There's 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 their setup |
boundary | Too close to the edge to call | Keep your previous answer |
Limits
These three endpoints share a budget: 60 calls an hour and 300 a day per connection, combined. Your app also has a ceiling across every connection at once, which grows as more people connect it. verify-zone additionally limits how many different areas you may ask about per day.
Asking about one zone often is fine. Asking about many is not. See Errors and limits.