Derived history
Where someone has been, as place labels and time windows. This is the tier a journalling, photo, or timeline app actually needs — and it's why such an app never has to request coordinates.
Visits
GET /v1/visits?from=2026-08-24T00:00:00Z&to=2026-08-25T00:00:00Z&limit=100
{
"visits": [
{
"arrival": "2026-08-24T09:15:00.000Z",
"departure": "2026-08-24T10:40:00.000Z",
"place": { "id": "…", "label": "The gym", "kind": "gym" },
"labeled": true
},
{
"arrival": "2026-08-24T12:02:00.000Z",
"departure": null,
"place": null,
"labeled": false
}
],
"nextCursor": "MjAyNi0wOC0yNFQxMjowMjowMFo…"
}
| Parameter | Default | Notes |
|---|---|---|
from, to | none | ISO timestamps, both optional |
limit | 100 | max 500 |
cursor | none | opaque; pass nextCursor back verbatim |
include_unlabeled | true | set false to drop visits with no shared place |
nextCursor is null on the last page. Don't parse it — it's an encoded keyset, and its shape may change.
Unlabeled visits are not errors
A visit with "labeled": false is one that didn't fall inside any place the user shared with you. You get the time window — someone was somewhere, stationary, between these times — and no geography whatsoever.
They're included by default, and you should usually keep them. Dropping them gives you a timeline with silent holes: your UI would show a user going straight from the gym to home with a two-hour gap it can't explain, and you'd have no way to know the gap existed. A row that says "somewhere, 12:02 → 13:58" is more honest and often more useful.
Set include_unlabeled=false only when your feature is strictly about named places — say, a "time spent at the gym this month" total.
An open visit
departure is null while the visit is still in progress. Don't compute a duration from it without checking; a null departure means "hasn't left yet", not "left at time zero."
Lookup by moment
The photo-geotagging question: where was I when this was taken?
GET /v1/lookup/place?at=2026-08-24T09:30:00Z&tolerance_s=900
{
"place": { "id": "…", "label": "The gym", "kind": "gym" },
"matched": true,
"asOf": "2026-08-24T09:29:12.000Z",
"deltaSeconds": 48
}
We take the nearest fix on either side of at, and answer with the place it falls inside. deltaSeconds tells you how far off that fix was, so you can decide whether to trust it.
tolerance_s defaults to 900 and caps at 86 400. Outside it, you get matched: false with place and asOf both null — we won't guess.
matched: false with a non-null asOf means something different: we had a fix close enough in time, but it wasn't inside any place you can see. The user was somewhere, just not somewhere they've shared with you.
Notes for building on this
Visits come from the phone, not from us. They're CoreLocation visit events — the OS deciding someone stopped moving. That means they're sparse and biased toward real stops: no visit for driving through, and none for a five-minute errand. Don't expect a continuous timeline.
Labels are resolved at read time. If a user renames a place, past visits come back with the new label. If they archive it, past visits keep the old label but new ones stop matching. Re-read rather than caching indefinitely.
Backfill is normal. A phone that was offline uploads its backlog on reconnect, so a range you already fetched can gain rows. If you cache, re-fetch recent windows periodically rather than assuming a fetched day is final.