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-zones on 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
curl -X POST https://api.atlasfetch.xyz/boundaries/sets \
  -H "Authorization: Bearer $ATLASFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "delivery-zones"}'
response · 201
{
  "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
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 }
  }'
response · 201
{
  "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 a MultiPolygon, Feature or FeatureCollection.
  • 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.
response · 400 (a MultiPolygon)
{ "error": "geometry must be a GeoJSON Polygon" }
Have a MultiPolygon? Add each of its polygons as its own boundary with the same 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:

response · 200 (set not yet enabled)
{
  "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
curl -H "Authorization: Bearer $ATLASFETCH_KEY" \
  "https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241&base=region,municipal&set=delivery-zones"
response · 200 (inside Zone A)
{
  "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:

response · 200 (Woodstock, east of Zone A)
{
  "base": {
    "region":    { "code": "ZA-WC",  "name": "Western Cape" },
    "municipal": { "code": "B-ZA-1", "name": "City of Cape Town" }
  },
  "sets": { "delivery-zones": [] },
  "errors": []
}
JavaScript
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");
Always read 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

PlanSetsBoundaries per setPositions per polygonProperties per boundary
Public110053, from fixed lists
Personal110505
Basic51,00050010
Pro2510,0005,00025
The demo key is on the Public plan, and its one set is shared by every demo user. It accepts only 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