Skip to content

Areas (geofences)

An “area” is a geofence — circle, place, polygon or track — scoped to the key owner (their own and their organization’s, same perimeter as v2). Another customer’s area always returns 404, never 403: the key can’t even discover that it exists.

Field Type Notes
id integer
name string max 100 characters
color string #rrggbb; #3b82f6 if omitted at creation
type enum circle | polygon | polyline | point
center object | null { lat, lng } — circles and places; on polygons it’s the computed centroid
radius_m number | null only circle (minimum 25 m) and point (default 50 m if omitted) — maximum 100000 m
buffer_m number | null only polyline: maximum distance from the track, 5–5000 m, default 50 m
points array { lat, lng }[] — polygons (≥3) and tracks (≥2), maximum 2000
external_id string | null the area’s code in your own system (ERP, CRM…); unique per account; 1–100 characters made of letters, digits and . _ : / @ # -
dwell_seconds integer minimum dwell time before notifying entry, in seconds; 0 = immediately (default, unchanged behavior) — see Minimum dwell time
metrics.area_m2 number | null polygons and circles
metrics.perimeter_m number | null polygons and circles
metrics.length_m number | null tracks only
bounds object | null { south, west, north, east }
created_at string (ISO 8601) | null

An integer from 0 to 86400 (seconds), meant to avoid notifying on a vehicle that merely passes through an area without stopping (a road running through a construction site, an intersection inside a customer’s radius). 0 is the default and behaves exactly as before this field existed: entry is notified as soon as the device is inside.

With a value above zero, the entry event (notifications, automations, and the entry shown in GET /areas/{id}/events and in /api/v3/events) only fires after the device has stayed inside the area for at least that long, measured on the GPS timestamps of the points received — not the server clock. A vehicle that merely passes through without staying that long generates neither an entry nor an exit. If the device leaves before the time has elapsed, the count resets: a later entry starts from zero again. The exit is, as a result, only notified after a confirmed entry.

Set it with dwell_seconds on POST/PATCH/PUT .../by-external-id and with properties.dwell_seconds on POST /areas/import; in the app’s area editor it corresponds to the “Notify entry after (minutes)” field. Out of range → 422 VALIDATION_ERROR with code: "OUT_OF_RANGE" — see Validation codes.

List of areas. Permission: read_areas.

Parameter Default Notes
limit 50 1–200, clamped
offset 0
type — circle | polygon | polyline | point; other values → 400 BAD_REQUEST
format json geojson returns a FeatureCollection in data
external_id — look up the area with this exact external code (0 or 1 result); not a partial filter
Finestra del terminale
curl -H "X-API-Key: $AITRACK_API_KEY" \
"https://api.aitrack.it/api/v3/areas?type=circle&limit=50"
{
"data": [
{
"id": 42,
"name": "North Warehouse",
"color": "#3b82f6",
"type": "circle",
"center": { "lat": 45.4642, "lng": 9.19 },
"radius_m": 150,
"buffer_m": null,
"points": [],
"metrics": { "area_m2": 70686, "perimeter_m": 942, "length_m": null },
"bounds": { "south": 45.4629, "west": 9.1881, "north": 45.4655, "east": 9.1919 },
"created_at": "2026-09-20T10:00:00.000Z"
}
],
"meta": { "pagination": { "limit": 50, "offset": 0, "count": 1 }, "total": 1 }
}

With ?format=geojson the same area comes back as a Feature inside a FeatureCollection — see GeoJSON: input and output.

Creates an area. Permission: write_areas. Optional Idempotency-Key header (24 h) — see Overview. The body accepts the v3 JSON shape or { "geojson": <Feature> }.

Finestra del terminale
curl -X POST https://api.aitrack.it/api/v3/areas \
-H "X-API-Key: $AITRACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "North Warehouse",
"color": "#3b82f6",
"type": "circle",
"center": { "lat": 45.4642, "lng": 9.19 },
"radius_m": 150
}'
{
"data": {
"id": 42,
"name": "North Warehouse",
"color": "#3b82f6",
"type": "circle",
"center": { "lat": 45.4642, "lng": 9.19 },
"radius_m": 150,
"buffer_m": null,
"points": [],
"metrics": { "area_m2": 70686, "perimeter_m": 942, "length_m": null },
"bounds": { "south": 45.4629, "west": 9.1881, "north": 45.4655, "east": 9.1919 },
"created_at": "2026-09-27T09:00:00.000Z"
}
}

Response 201, header Location: /api/v3/areas/42. If there are non-blocking warnings (nearly coincident vertices, a very small or very large area) meta.warnings also appears. Invalid data → 422 VALIDATION_ERROR with details[] — see Validation codes. external_id already used by another of your areas → 409 CONFLICT: use PUT /api/v3/areas/by-external-id/{external_id} to update it instead of trying to recreate it.

PUT /api/v3/areas/by-external-id/{externalId}

Section titled “PUT /api/v3/areas/by-external-id/{externalId}”

Creates the area if none exists yet with this external_id, otherwise updates it — an upsert. Permission: write_areas. Built for a synchronization job that periodically resends its whole list: calling it repeatedly with the same list never creates a duplicate.

Path parameter Notes
externalId 1–100 characters made of letters, digits and . _ : / @ # -; otherwise 400 BAD_REQUEST before hitting the database

The body is the same v3 JSON shape as POST/PATCH. If the body also includes external_id, it must match the one in the path, otherwise 422 VALIDATION_ERROR (code: "MISMATCH").

Finestra del terminale
# First sync for customer 4521: the area doesn't exist yet → created (201)
curl -X PUT https://api.aitrack.it/api/v3/areas/by-external-id/erp-customer-4521 \
-H "X-API-Key: $AITRACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer 4521 — Rossi Trasporti",
"type": "circle",
"center": { "lat": 45.4642, "lng": 9.19 },
"radius_m": 150
}'
{
"data": {
"id": 88,
"name": "Customer 4521 — Rossi Trasporti",
"color": "#3b82f6",
"type": "circle",
"center": { "lat": 45.4642, "lng": 9.19 },
"radius_m": 150,
"buffer_m": null,
"points": [],
"external_id": "erp-customer-4521",
"dwell_seconds": 0,
"metrics": { "area_m2": 70686, "perimeter_m": 942, "length_m": null },
"bounds": { "south": 45.4629, "west": 9.1881, "north": 45.4655, "east": 9.1919 },
"created_at": "2026-09-27T02:00:00.000Z"
},
"meta": { "upsert": "created" }
}

The next night, the same call with the same externalId — say, with an updated center because the customer moved sites — updates area 88 instead of creating a new one (200, meta.upsert: "updated"); just like PATCH, only the fields you send change:

{ "data": { "id": 88, "name": "Customer 4521 — Rossi Trasporti", "center": { "lat": 45.4650, "lng": 9.1905 } }, "meta": { "upsert": "updated" } }

Response 201 (created, Location header) or 200 (updated) — also distinguishable via meta.upsert ("created" | "updated"). 400 BAD_REQUEST if the path externalId is invalid; 422 VALIDATION_ERROR if the data is invalid or the body carries an external_id different from the one in the path (MISMATCH).

Detail of one area. Permission: read_areas. ?format=geojson returns a Feature. A malformed id (not a positive integer) → 400 BAD_REQUEST, before hitting the database. A valid id that’s nonexistent or belongs to another customer → 404 NOT_FOUND (the two cases aren’t distinguished).

Finestra del terminale
curl -H "X-API-Key: $AITRACK_API_KEY" \
https://api.aitrack.it/api/v3/areas/42

Partial update. Permission: write_areas. Only the fields you send change; omitted ones keep their saved value.

Finestra del terminale
# Name only
curl -X PATCH https://api.aitrack.it/api/v3/areas/42 \
-H "X-API-Key: $AITRACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "North Warehouse — truck entrance"}'
Finestra del terminale
# From circle to polygon: the new geometry must be sent
curl -X PATCH https://api.aitrack.it/api/v3/areas/42 \
-H "X-API-Key: $AITRACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "polygon",
"points": [
{ "lat": 45.4640, "lng": 9.1895 },
{ "lat": 45.4648, "lng": 9.1900 },
{ "lat": 45.4638, "lng": 9.1912 }
]
}'

external_id and dwell_seconds update like any other field — see Model and Minimum dwell time. Sending "external_id": null removes the external code from the area (for example if you stop syncing it from an external system); omitting it keeps whatever is already saved, like any other field on a partial PATCH.

Response 200 with the full updated area (same shape as GET). 400 BAD_REQUEST if the path id is malformed (not a positive integer). 404 NOT_FOUND if it doesn’t exist, 422 VALIDATION_ERROR if the data is invalid.

Deletes an area. Permission: write_areas. Response 204 with no body. 400 BAD_REQUEST if the id is malformed. 404 NOT_FOUND if it doesn’t exist or isn’t yours.

Finestra del terminale
curl -X DELETE https://api.aitrack.it/api/v3/areas/42 \
-H "X-API-Key: $AITRACK_API_KEY"

Imports several areas from GeoJSON in one call. Permission: write_areas. Maximum 200 areas per call (features past the 200th are dropped and reported in errors; the first 200 are still processed). Body: a FeatureCollection, a Feature, or a bare Geometry, either as the request body directly or wrapped in { "geojson": ... }. One invalid feature doesn’t block the others.

?dry_run=true (or "dry_run": true in the body) validates without saving anything — useful for showing the user what would be imported before they confirm.

A feature with properties.external_id updates the existing area with that code instead of duplicating it (listed in data.updated, not data.created) — the same logic as PUT .../by-external-id, handy when importing a GeoJSON export already enriched with your own system’s codes. properties.dwell_seconds sets the minimum dwell time of each imported area, following the same rule as a direct POST/PATCH. A Multi* geometry (MultiPoint, MultiLineString, MultiPolygon) with properties.external_id on the same feature would produce several areas sharing the same code: it’s rejected with the MULTI_GEOMETRY_EXTERNAL_ID error — split it into one feature per part, each with its own external_id, or omit it.

Finestra del terminale
curl -X POST "https://api.aitrack.it/api/v3/areas/import?dry_run=true" \
-H "X-API-Key: $AITRACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": { "name": "South Site", "radius": 100 },
"geometry": { "type": "Point", "coordinates": [9.195, 45.460] } },
{ "type": "Feature", "properties": { "name": "Depot" },
"geometry": { "type": "Polygon", "coordinates": [[[9.10,45.40],[9.10,45.40]]] } }
]
}'
{
"data": {
"dry_run": true,
"valid": 1,
"created": [],
"updated": [],
"errors": [{ "index": 1, "code": "errPolygonMin" }],
"warnings": []
}
}

Response 200 when dry_run, or when the real call only updated existing areas (none created); 201 if at least one area was created; 422 if none was valid. errors[] mixes two families of codes, distinguishable by whether message is present:

  • from reading the GeoJSON (only index and code): errGeoJsonInvalid, errGeometryUnsupported, errCoordinates, errTooManyPoints, errPolygonMin, errPolylineMin, errTooManyFeatures (the last one with index: -1, when there are more than 200 features), MULTI_GEOMETRY_EXTERNAL_ID (index, code and message: an external_id on a Multi* geometry).
  • from validating the resulting area (index, code, message): the same codes as the Validation codes table below.

warnings[] (non-blocking): warnHolesIgnored (a polygon’s interior holes are ignored), warnSimplifiedDuplicates (nearly coincident vertices simplified).

Is the point lat/lng inside the area? Permission: read_areas. Same geometric rule as the engine that generates entry/exit events, so it’s consistent with the notifications received by anyone who has an alert configured on that area.

Parameter Required
lat yes
lng yes
Finestra del terminale
curl -H "X-API-Key: $AITRACK_API_KEY" \
"https://api.aitrack.it/api/v3/areas/42/contains?lat=45.4642&lng=9.19"
{ "data": { "inside": true, "distance_m": -37.5 } }

distance_m is negative inside the area (meters from the edge, toward the interior), positive outside; it can be null if it can’t be computed. Missing or invalid lat/lng, or a malformed id → 400 BAD_REQUEST. A valid id for a nonexistent area or another customer’s → 404 NOT_FOUND.

Entries and exits recorded by the key owner’s geofence notification rules on this area — with a dwell_seconds minimum set, the entries listed here follow that rule: a vehicle that merely passed through doesn’t show up. Permission: read_areas.

Parameter Default Notes
event — entry | exit — filter only entries or only exits; any other value → 400 BAD_REQUEST
device — filter by IMEI
from / to — time window, ISO 8601; unparseable → 400 BAD_REQUEST
limit 50 1–200
offset 0
Finestra del terminale
curl -H "X-API-Key: $AITRACK_API_KEY" \
"https://api.aitrack.it/api/v3/areas/42/events?event=entry&limit=50"
{
"data": [
{
"id": 5521,
"area_id": 42,
"device": "352093081452312",
"event": "entry",
"position": { "lat": 45.4641, "lng": 9.1899 },
"timestamp": "2026-09-27T08:59:00.000Z"
}
],
"meta": { "pagination": { "limit": 50, "offset": 0, "count": 1 } }
}

A malformed id → 400 BAD_REQUEST, before hitting the database. A valid id for a nonexistent area or another customer’s → 404 NOT_FOUND.

Each entry is an element of details[] in the 422 VALIDATION_ERROR response of POST/PATCH/ PUT .../by-external-id (direct create, update and upsert) — on POST /areas/import the same codes appear in errors[].code alongside the GeoJSON-reading codes, see above. external_id already used by another area isn’t a validation error: it responds 409 CONFLICT (no details[]) only on POST /areas — see creation and upsert above.

Field Code When
name REQUIRED missing or empty name
name TOO_LONG over 100 characters
type INVALID missing or not one of circle/polygon/polyline/point
color INVALID not in #rrggbb format
center REQUIRED missing center on a circle or a place
center INVALID_COORDINATES center lat/lng out of range or not numeric
points INVALID_COORDINATES a point has lat/lng out of range or not numeric
external_id INVALID doesn’t match the format: 1–100 characters made of letters, digits and . _ : / @ # -
external_id MISMATCH only on PUT .../by-external-id/{externalId}: the body carries an external_id different from the one in the path
dwell_seconds OUT_OF_RANGE not an integer between 0 and 86400
points TOO_FEW_POINTS fewer than 3 points on a polygon, fewer than 2 on a track
points TOO_MANY_POINTS over 2000 points
points SELF_INTERSECTION the polygon’s sides cross each other
radius_m OUT_OF_RANGE below 25 m (circle only) or above 100000 m (circle and place)
buffer_m OUT_OF_RANGE outside 5–5000 m (track only)
geojson INVALID_GEOJSON { "geojson": ... } doesn’t contain a valid geometry
(none) INVALID_BODY the request body isn’t a JSON object

To create or update an area from GeoJSON, the body of POST/PATCH /areas/{id} accepts { "geojson": <Feature | Geometry> } — one geometry per call. POST /areas/import instead accepts a FeatureCollection, a Feature, or a bare Geometry directly (also wrapped in { "geojson": ... }), up to 200 elements, with Multi* types becoming one area per part:

GeoJSON geometry Becomes Notes
Point point (place, 50 m radius) or circle if the feature has properties.radius
LineString polyline properties.bufferDistance (or buffer_distance) sets the margin, otherwise 50 m
Polygon polygon interior holes (rings past the first) are dropped with the warnHolesIgnored warning
MultiPoint / MultiLineString / MultiPolygon several areas only on /areas/import, one area per part

properties.name (or properties.title) and properties.color (or properties.stroke, if #rrggbb) become name/color unless the request body overrides them. Same rule for properties.external_id and properties.dwell_seconds → external_id/dwell_seconds.

On read, ?format=geojson on GET /areas and GET /areas/{id} returns a Feature (or a FeatureCollection for the list) per RFC 7946:

{
"data": {
"type": "Feature",
"id": 42,
"geometry": { "type": "Point", "coordinates": [9.19, 45.4642] },
"properties": {
"id": 42,
"name": "North Warehouse",
"color": "#3b82f6",
"type": "circle",
"radius": 150,
"external_id": null,
"dwell_seconds": 0,
"metrics": { "area_m2": 70686, "perimeter_m": 942, "length_m": null },
"created_at": "2026-09-27T09:00:00.000Z"
}
}
}