Custom geofences: check a coordinate against your own polygons
Draw or upload your own polygons, group them into a set, and every lookup can tell you which of them contain a point — alongside the country, region and municipality, in one request. Typical uses: delivery zones, service areas, sales territories, franchise regions.
Flow run end to end on 17 September 2026; base-layer values checked against the live API.
How it works
- A set is a named group of polygons, e.g.
delivery-zones. - Each boundary in it is one GeoJSON Polygon with a name and optional properties.
- A set only answers lookups once it is switched on and granted to an API key.
- Ask for it with
set=delivery-zoneson the normal lookup. Every boundary containing the point comes back, with its properties.
Steps 1, 2 and 4 work with an API key. Step 3 is done once in the dashboard.
1. Create a set
curl -X POST https://api.atlasfetch.xyz/boundaries/sets \
-H "Authorization: Bearer $ATLASFETCH_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "delivery-zones"}'{
"id": "cmu5ol88k0006txkzxu1zqlwu",
"name": "delivery-zones",
"available": false,
"createdAt": "2026-09-17T15:25:12.309Z"
}Names are 1-32 letters, digits, hyphens or underscores, starting with a letter or digit. A new set starts switched off.
2. Add a polygon
curl -X POST https://api.atlasfetch.xyz/boundaries \
-H "Authorization: Bearer $ATLASFETCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"set": "delivery-zones",
"name": "Zone A",
"geometry": {
"type": "Polygon",
"coordinates": [[
[18.410, -33.935], [18.435, -33.935], [18.435, -33.915],
[18.410, -33.915], [18.410, -33.935]
]]
},
"properties": { "tier": "express", "fee": 25 }
}'{
"id": "5ab696a0-e8f6-4c59-adac-99ad8283f384",
"set": "delivery-zones",
"name": "Zone A",
"properties": { "tier": "express", "fee": 25 },
"pointCount": 5,
"createdAt": "2026-09-17T15:25:12.492Z"
}Geometry rules — the API refuses anything else with a 400:
- A single GeoJSON
Polygon. Not aMultiPolygon,FeatureorFeatureCollection. - Positions are [longitude, latitude] — longitude first.
- Each ring has at least 4 positions and is closed: the last position repeats the first.
- The first ring is the outline; further rings are holes.
- No self-intersections.
{ "error": "geometry must be a GeoJSON Polygon" }name and properties. A point inside any part then matches.Properties are a flat object: keys up to 32 characters, values a string (up to 256 characters), number or boolean. They come back with every match, so put what you would otherwise look up next — a fee, a team, an SLA — right on the zone.
3. Switch the set on and grant it to your key
Until you do, lookups skip the set. The call still succeeds, and errors[] says why:
{
"base": {
"region": { "code": "ZA-WC", "name": "Western Cape" },
"municipal": { "code": "B-ZA-1", "name": "City of Cape Town" }
},
"sets": {},
"errors": [
{ "type": "access", "message": "Set 'delivery-zones' is not available for querying" }
]
}In the dashboard, switch the set on and choose which of your keys may query it. This is a dashboard step on purpose: a raw API key cannot grant a set to itself, so a leaked key cannot widen its own access. Granting per key also lets a test key and a production key see different sets.
4. Check a point
curl -H "Authorization: Bearer $ATLASFETCH_KEY" \
"https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241&base=region,municipal&set=delivery-zones"{
"base": {
"region": { "code": "ZA-WC", "name": "Western Cape" },
"municipal": { "code": "B-ZA-1", "name": "City of Cape Town" }
},
"sets": {
"delivery-zones": [
{ "name": "Zone A", "properties": { "fee": 25, "tier": "express" } }
]
},
"errors": []
}A point outside every zone gets an empty list for that set — still a successful answer:
{
"base": {
"region": { "code": "ZA-WC", "name": "Western Cape" },
"municipal": { "code": "B-ZA-1", "name": "City of Cape Town" }
},
"sets": { "delivery-zones": [] },
"errors": []
}const SET = "delivery-zones";
const url = new URL("https://api.atlasfetch.xyz/location/lookup");
url.search = new URLSearchParams({ lat: "-33.9249", lng: "18.4241", base: "municipal", set: SET });
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ATLASFETCH_KEY}` } });
if (!res.ok) throw new Error(`AtlasFetch ${res.status}: ${(await res.json()).error}`);
const data = await res.json();
if (data.errors.length > 0) console.warn("Skipped:", data.errors); // e.g. set not granted to this key
const zones = data.sets[SET] ?? [];
console.log(zones.length > 0 ? `In ${zones.map(z => z.name).join(", ")}` : "Outside every zone");errors[]before trusting an empty list. A set that is switched off, not granted to the key, or misspelled is skipped — and then its absence looks exactly like “outside every zone”.Overlapping zones all match, oldest first. A point lying exactly on a polygon’s edge counts as inside. Several sets can be checked at once with a comma list, set=delivery-zones,service-areas, and it is still one lookup.
Plan limits
| Plan | Sets | Boundaries per set | Positions per polygon | Properties per boundary |
|---|---|---|---|---|
| Public | 1 | 100 | 5 | 3, from fixed lists |
| Personal | 1 | 10 | 50 | 5 |
| Basic | 5 | 1,000 | 500 | 10 |
| Pro | 25 | 10,000 | 5,000 | 25 |
category, color and priorityproperties from fixed lists, and when the set is full, adding a boundary silently removes the oldest — possibly someone else’s. Never upload anything private with the demo key. On every other plan a full set refuses new boundaries instead.Read your boundaries back, with geometry, from GET /boundaries/view?set=delivery-zones, and delete one with DELETE /boundaries/:id. Your polygons are private to your account: they are never returned to anyone else, and the reference layers never include them.
Next
From an AI assistant, the AtlasFetch MCP server’s create_boundary_set and add_boundary tools do steps 1 and 2.