mapto.it documentation
mapto.it tells your app when a person's phone arrives at and leaves one of your places — a home, a job
site, a store, an office. Phones report their location to mapto.it — from your own app, or from the free
OwnTracks app if you'd rather not build one — and you get stop.arrived and stop.departed events by webhook or from the API. You never handle raw locations.
- Send your places with their coordinates (Places).
- Add a device for each person and show them its setup link once (Devices).
- Receive events by webhook, or pull them (Events).
Every API call carries a key's secret: Authorization: Bearer ms_…. A key has scopes —
devices, places, events — and only works for what they allow. Keep the secret on your
server: never in a web page or an app people download. Your account, its keys and its webhook are set up by us — ask at
contact@scaledapps.com.
The API lives at https://mapto.it/api/v1. Bodies are JSON; times are ISO 8601 in UTC. Lists take
limit (see each list for its default and maximum).
Places
A place is a point and a radius. You name each place by your own id, its external_ref (letters, digits and
. _ : @ -, up to 191 characters), so you don't need to keep ours.
PUT /api/v1/places/job-1042
{"name": "Smith residence", "latitude": 40.6200123, "longitude": -111.8001234, "radius_m": 150, "kind": "property"}
radius_m is optional (default 150 m, 25 to 2000). Use the building's own coordinates, not a ZIP-code centroid:
a stop is matched by its distance from this point. A store's parking lot may need 250 m.
To keep a whole list in step, send it at once — up to 1000 places, with prune: true to delete any not in it:
POST /api/v1/places/sync
{"places": [{"external_ref": "job-1042", "name": "…", "latitude": …, "longitude": …}, …], "prune": true}
→ {"created": 3, "updated": 120, "deleted": 1}
Groups. If you serve several companies from one account, give each device and place a group (your id for
the company). A device only matches places in its own group, and a sync with group set prunes only that group.
Without a group, a device matches only places without one.
Also GET /api/v1/places (in external_ref order, 500 per page or up to limit=1000,
paged with after={external_ref}, filtered with group),
GET and DELETE /api/v1/places/{external_ref}.
Devices
A device is one phone. Give it a label and, optionally, your id for the person:
POST /api/v1/devices
{"label": "Sam's iPhone", "external_ref": "user-77", "group": "acme", "paused": false}
→ 201 {"device": {"id": "Hx3kP9qa", …},
"setup": {"link": "owntracks:///config?inline=…", "url": "https://mapto.it/owntracks", "username": "mt-…", "password": "…", "config": {…}}}
The setup is in this response only — we keep only a hash of the password. Show the link to the person on their
phone (or as a QR code): with OwnTracks installed, opening it imports the settings. They must allow location access
Always. If the link is lost — or was shown on a computer instead of the phone — the device's owner gets a new one
with POST /api/v1/devices/{id}/setup; the old password stops working at once.
Also GET /api/v1/devices (filter with external_ref or group; revoked ones only with
include_revoked=1), GET /api/v1/devices/{id}, and POST /api/v1/devices/{id}/revoke — from then on
its reports are refused, and a visit in progress ends.
A device is shown as your account sees it: id, label, kind (work or
home), your external_ref and group, paused, owner (whether your account
set it up), last_seen_at (owner only), revoked_at, detached_at and created_at, plus
work and also_home (see Home and work).
From your own app
Your app can report instead of OwnTracks, using the same device credentials (setup.username and
setup.password; ignore the link). POST each location as JSON with HTTP Basic authentication:
POST https://mapto.it/owntracks
Authorization: Basic base64(username:password)
{"_type": "location", "lat": 40.6200123, "lon": -111.8001234, "tst": 1762095600, "acc": 12, "vel": 0}
tst is when the location was taken (Unix seconds), acc its accuracy in meters, vel
the speed in km/h (both optional). Any 2xx means it was received. Report at least every couple of minutes while the phone is
still, and every few hundred meters while moving, so stops can be confirmed; queue reports while offline and send them later in
the order they were taken. A report sent twice is kept once. Reports dated more than 10 minutes in the future or more than 7 days
ago are ignored, as are messages other than "_type": "location". Wrong credentials get a 401. This is the format
OwnTracks uses, so an existing OwnTracks integration works unchanged.
Pausing
When someone is off the clock, pause their device. Reports taken while paused are dropped on arrival — never stored, including ones the phone queued while offline and sends late. Pausing ends a visit in progress at the pause time. On a phone shared between home and work, pausing affects only your own profile (see Home and work).
POST /api/v1/devices/{id}/pause {"at": "2026-11-01T23:30:00Z"}
POST /api/v1/devices/{id}/resume {"at": "2026-11-02T14:00:00Z"}
at is optional (default now) and may be up to 7 days back, so a clock-out recorded late still pauses
from the right moment. Both calls are safe to repeat. Create a device with "paused": true if the person isn't working yet.
Home and work on one phone
People carry one phone. A device can have at most one home profile and one work profile, each belonging to a different account, and the phone is set up once. The split is by location:
- A report inside one of the home profile's places belongs to home alone:
place.enteredas the phone crosses into the place's radius,place.exitedonce it is beyond the radius plus 50 m — no waiting, so a house can react as someone arrives. - A home place is exclusive unless you send
"exclusive": false. Only exclusive places hide the phone from work; usefalsefor a wide "near home" circle, so job sites down the street still count. - Everywhere else belongs to work: the usual stops. If a work visit is in progress when the phone reaches home, it ends there as if the phone had simply moved on. The employer never learns where home is or when the phone is there — not even at a place that is also one of its own; home wins.
The person's own app sets the phone up and owns it ("kind": "home" when creating it; the default is
work). To let an employer use it for work, the owner asks for a one-time code — valid 10 minutes, once; asking again voids
the last one — and the person enters it at the employer:
POST /api/v1/devices/{id}/work-codes (owner) → {"code": "7KQM-3XPA", "expires_at": "…"}
POST /api/v1/work-profiles (employer)
{"code": "7KQM-3XPA", "external_ref": "user-77", "group": "acme", "name": "Acme Plumbing", "paused": false} → 201 {"device": {…}}
name is optional: what the owner sees you as. Set it if one account of yours serves several companies; otherwise your account's name is shown.
A wrong or expired code gets invalid_code; a phone that already has a work profile gets work_profile_exists.
Each account may try 10 codes per 10 minutes. The work profile counts toward the employer's device limit.
The employer gets a work profile and the device id; it never gets the phone's credentials. It sees "also_home": true,
and nothing else about home. The owner sees "work": {"account": "Acme", …} and receives profile.attached.
Either side can leave at any time: the employer with POST /api/v1/devices/{id}/revoke (which detaches its profile),
the owner with DELETE /api/v1/devices/{id}/work-profile or by revoking the whole phone. The side that didn't act
receives profile.detached. Attaching replaces the employer's older work-only phone for the same
external_ref and group.
Pausing is per profile. While work is paused (someone off the clock) and home is on, reports are still kept — for home only, which the owner agreed to by setting the phone up — and work sees nothing from that time.
How stops are found
- A phone that stays within 100 m for 5 minutes has stopped. The arrival is stamped when it came to rest, not 5 minutes later.
- The stop is matched to the nearest place whose radius covers it, or none.
- A visit at a place lasts while the phone is anywhere within the place's radius plus 50 m — the whole lot, not just where it parked. It ends on the second report in a row outside, so one stray GPS fix doesn't split a visit.
- The departure is stamped back from the first report outside by the time it took to get there, because a parked phone reports only every few minutes.
- Four hours without a report ends a visit at its last report. Reports less accurate than 150 m are ignored.
Events
Work profiles get stop.arrived when a stop is confirmed and stop.departed when it ends. place is
null for a stop at none of your places — ignore those, or use their coordinates to add a place. Home profiles get
place.entered / place.exited (data: device, place, at), and either side
gets profile.attached / profile.detached when the other joins or leaves (data: device,
and profile with the other side's kind and account name, plus by —
owner or holder — on a detach).
{"id": "evt_9mQ…", "type": "stop.departed", "created_at": "2026-11-02T16:05:12+00:00",
"data": {"stop": {
"id": "Ab3dEf7h", "status": "closed",
"device": {"id": "Hx3kP9qa", "external_ref": "user-77", "group": "acme"},
"place": {"id": "Qw8eRt2y", "external_ref": "job-1042", "name": "Smith residence"},
"distance_m": 34, "latitude": 40.620101, "longitude": -111.800212, "accuracy_m": 12,
"arrived_at": "2026-11-02T15:00:00+00:00", "departed_at": "2026-11-02T16:03:20+00:00",
"ended_by": "moved"}}}
ended_by is moved, gap (no reports for 4 hours), pause or revoked (the device or your profile on it ended).
Pull them in order with GET /api/v1/events?after={last id you processed} (100 per page, up to limit=500; kept 30 days).
GET /api/v1/stops lists confirmed stops, newest first, from the last 14 days unless you give since; filter with
device or unmatched=1. A stop's status is open while the visit goes on, then closed.
An event id is unique and stable: process each once.
Webhooks
Optional: pulling always works. With a webhook set on your account, each event is also POSTed to it, in order:
POST https://your-app.example/hooks/mapto
webhook-id: evt_9mQ…
webhook-timestamp: 1762099512
webhook-signature: v1,K5o…=
{"id": "evt_9mQ…", "type": "stop.arrived", "created_at": "…", "data": {"stop": {…}}}
They follow the Standard Webhooks scheme, so its libraries verify them.
By hand: base64-decode your secret after whsec_, compute HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body},
base64 it, and compare with the value after v1,. Reject timestamps more than 5 minutes old.
Answer with any 2xx. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then stop; later
events for your account wait behind a failing one within each round. Webhook URLs must be HTTPS on a public address; redirects
aren't followed. webhook-id is the event id on every attempt: use it to ignore repeats.
AI tools (MCP)
AI assistants that speak the Model Context Protocol
(Claude, Cursor, VS Code and others) can use mapto.it directly. The address is https://mapto.it/mcp, and it takes
the same API key as the API, in the same header. Each tool appears only if the key has its scope; plans, limits and what
each key can see are exactly the API's.
# Claude Code
claude mcp add --transport http mapto https://mapto.it/mcp --header "Authorization: Bearer ms_…"
# Clients configured with JSON (the exact layout varies by client)
{"mapto": {"url": "https://mapto.it/mcp", "headers": {"Authorization": "Bearer ms_…"}}}
| Scope | Tools |
|---|---|
places | list_places, get_place, upsert_place, delete_place*, sync_places* (as Places; prune deletes the rest) |
devices | list_devices, get_device, create_device, pause_device, resume_device, revoke_device*, create_work_code, remove_work_profile* (as Devices and Home and work) |
events | list_stops, list_events (as Events) |
* Marked as destructive, so assistants ask you before using them.
Phone credentials never go through MCP: create_device returns the device without its
setup link or password. Get those with POST /api/v1/devices/{id}/setup and show them to the person in your own app.
Stops and events are people's location history, so give AI tools a key with the events scope only where that is
needed. Errors come back as tool errors starting with the error code, for example not_owner: ….
Signing in without a key (needed for claude.ai connectors) is planned and not available yet.
Errors
Errors are {"error": {"code": "…", "message": "…"}}, with details for invalid fields. Program against the code;
messages may be reworded. Codes are only ever added.
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | The key is unknown, revoked, expired, or lacks the scope for this. |
account_suspended | 403 | This account is suspended. |
rate_limited | 429 | Too many requests in the last minute for this account's plan. |
not_found | 404 | No such item on this account. |
invalid_request | 422 | The request is not valid. |
device_limit | 422 | This account's plan allows no more devices. |
place_limit | 422 | This account's plan allows no more places. |
device_revoked | 409 | This device has been revoked. |
not_owner | 403 | Only the account that set this device up can do this. |
invalid_code | 422 | The code is wrong, already used, or older than 10 minutes. |
work_profile_exists | 409 | This device is already used for work by an account; it must be detached first. |
invalid_url | 422 | The URL is not a valid https address. |
blocked_address | 422 | The URL points at a private or reserved network address. |
Plans and limits
| Plan | Devices | Places | API calls a minute |
|---|---|---|---|
| Trial | 3 | 100 | 60 |
| Starter | 25 | 2,000 | 120 |
| Business | 250 | 20,000 | 600 |
| Enterprise | 1,000 | 100,000 | 1200 |
A revoked device no longer counts. An account may go up to 10% past its devices (rounded down) before new ones are
refused with device_limit; the account page warns from 80%. Phones' own reports don't count toward API calls. Over the limit, calls get
rate_limited with a Retry-After header.
Past your plan's limits. Phones already set up keep reporting and nothing is deleted; only new devices are refused once an account is past its margin. Plans are changed in ScaledApps.
Privacy and retention
- Raw location reports are never given to you or anyone else, and are deleted after 14 days.
- Reports from a paused device are not stored at all.
- Stops are kept 180 days; stops at none of your places 21 days. Events are kept 30 days. On your account page you can keep them for less (at least 7 days).
- Owners, admins and editors of your organization can see stops on a map on the account page (never the raw reports between them); viewers can't. The map's background comes from OpenStreetMap, whose servers see which area is being looked at, not the stops.
- You must tell the people you track, and get their consent where the law requires it — several US states require notice before an employer tracks employees by GPS. Pause devices outside working hours.
- Questions or requests: contact@scaledapps.com. See also the terms and privacy policy.