Territory design (beta)
Cut weighted points into compact, non-overlapping territories that each carry a balanced share of the work.
territory.design
Beta
point_1k
How to call it
REST
POST
/v1/territory/design
GraphQL
mutation
openTerritoryDesign
MCP
open_territory_design
Cut weighted points (stops, addresses, postcodes with their demand) into compact, non-overlapping territories that each carry a balanced share of the total weight. Ask for a number of territories or name them with optional anchors and shares. Distances are straight-line. Billed per point.
Request schema
Generated from the same validation rules the endpoint runs, so a field cannot be documented without being validated.
POST /v1/territory/design
{
"type": "object",
"properties": {
"count": {
"type": "integer",
"minimum": 1,
"maximum": 200
},
"outline": {
"type": [
"boolean",
"null"
]
},
"points": {
"type": "array",
"minItems": 1,
"maxItems": 10000,
"items": {
"type": "object",
"properties": {
"allowed_zones": {
"type": "array",
"maxItems": 200,
"items": {
"type": "string",
"maxLength": 128
}
},
"id": {
"type": "string",
"maxLength": 128
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"weight": {
"type": [
"integer",
"null"
],
"minimum": 1,
"maximum": 1000000
},
"zone": {
"type": [
"string",
"null"
],
"maxLength": 128
}
},
"required": [
"id",
"lat",
"lng"
]
}
},
"tolerance_percent": {
"type": [
"number",
"null"
],
"minimum": 0,
"maximum": 50
},
"zones": {
"type": "array",
"minItems": 1,
"maxItems": 200,
"items": {
"type": "object",
"properties": {
"anchor": {
"type": [
"object",
"null"
],
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90,
"description": "Required when zones.*.anchor is present."
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180,
"description": "Required when zones.*.anchor is present."
}
}
},
"id": {
"type": "string",
"maxLength": 128
},
"share": {
"type": [
"number",
"null"
],
"maximum": 1000,
"exclusiveMinimum": 0
}
},
"required": [
"id"
]
}
}
},
"additionalProperties": false,
"required": [
"points",
"zones"
]
}
Request example
{
"points": [
{
"id": "stop-1",
"lat": 45.48886,
"lng": -73.63285,
"weight": 1
},
{
"id": "stop-2",
"lat": 45.45869,
"lng": -73.56354,
"weight": 3
},
{
"id": "stop-3",
"lat": 45.51993,
"lng": -73.49625,
"weight": 2
},
{
"id": "stop-4",
"lat": 45.4545,
"lng": -73.58194,
"weight": 1
},
{
"id": "stop-5",
"lat": 45.47888,
"lng": -73.56081,
"weight": 1
},
{
"id": "stop-6",
"lat": 45.54922,
"lng": -73.63772,
"weight": 2
},
{
"id": "stop-7",
"lat": 45.52568,
"lng": -73.55506,
"weight": 1
},
{
"id": "stop-8",
"lat": 45.51925,
"lng": -73.5886,
"weight": 2
},
{
"id": "stop-9",
"lat": 45.45559,
"lng": -73.50548,
"weight": 3
},
{
"id": "stop-10",
"lat": 45.5003,
"lng": -73.56268,
"weight": 3
},
{
"id": "stop-11",
"lat": 45.51723,
"lng": -73.53724,
"weight": 1
},
{
"id": "stop-12",
"lat": 45.51979,
"lng": -73.545,
"weight": 3
}
],
"zones": [
{
"id": "east",
"anchor": {
"lat": 45.5,
"lng": -73.55
}
},
{
"id": "west",
"anchor": {
"lat": 45.5,
"lng": -73.64
}
},
{
"id": "north",
"share": 0.5
}
],
"tolerance_percent": 20
}
Response
What a successful call returns: the schema of `data` and `meta`, with a description of each field, followed by an example response from a real call.
POST /v1/territory/design
Response schema
{
"description": "The territories with their centres, weights and outlines, and the territory of every point. Billed per point.",
"data": {
"type": "object",
"properties": {
"zones": {
"type": "array",
"description": "The territories, in the order asked for.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The territory id: as given in `zones`, or 1..count."
},
"center": {
"type": [
"object",
"null"
],
"description": "The territory's centre, `{lat, lng}`. Null for a territory that received no point."
},
"anchored": {
"type": "boolean",
"description": "Whether the territory grew around its `anchor`."
},
"points": {
"type": "integer",
"description": "Points in the territory."
},
"weight": {
"type": "integer",
"description": "The weight of its points."
},
"target": {
"type": "integer",
"description": "The weight it should carry: its share of the total."
},
"outline": {
"type": [
"object",
"null"
],
"description": "The convex outline of its points as a GeoJSON Polygon ([lng, lat]). Null below three distinct points; absent when `outline` is false."
}
}
}
},
"assignment": {
"type": "array",
"description": "Every point with its territory, in the order of `points`.",
"items": {
"type": "object",
"properties": {
"point": {
"type": "string",
"description": "The point id."
},
"zone": {
"type": [
"string",
"null"
],
"description": "The territory id; null when the point fits none."
}
}
}
},
"unplaced": {
"type": "array",
"items": {
"type": "string"
},
"description": "Points that fit no territory (their `allowed_zones` are full)."
},
"summary": {
"type": "object",
"properties": {
"zones": {
"type": "integer",
"description": "Territories."
},
"points": {
"type": "integer",
"description": "Points submitted."
},
"weight": {
"type": "integer",
"description": "Total weight."
},
"largest_deviation_percent": {
"type": "number",
"description": "The largest difference between a territory's weight and its target, as a percentage of the target."
}
}
}
},
"required": [
"zones",
"assignment",
"unplaced",
"summary"
]
},
"meta": {
"type": "object",
"properties": {},
"additionalProperties": true
}
}
Response example
{
"request_id": "req_01k8zq3m5n7p9r1t3v5x7z9b2d",
"capability": "territory.design",
"billing": {
"unit": "point_1k",
"quantity": 12,
"counted": true
},
"data": {
"zones": [
{
"id": "east",
"center": {
"lat": 45.520306,
"lng": -73.5314
},
"anchored": true,
"points": 4,
"weight": 7,
"target": 9,
"outline": {
"type": "Polygon",
"coordinates": [
[
[
-73.55506,
45.52568
],
[
-73.545,
45.51979
],
[
-73.53724,
45.51723
],
[
-73.49625,
45.51993
],
[
-73.55506,
45.52568
]
]
]
}
},
{
"id": "west",
"center": {
"lat": 45.506008,
"lng": -73.591628
},
"anchored": true,
"points": 6,
"weight": 10,
"target": 9,
"outline": {
"type": "Polygon",
"coordinates": [
[
[
-73.63772,
45.54922
],
[
-73.63285,
45.48886
],
[
-73.58194,
45.4545
],
[
-73.56081,
45.47888
],
[
-73.56268,
45.5003
],
[
-73.5886,
45.51925
],
[
-73.63772,
45.54922
]
]
]
}
},
{
"id": "north",
"center": {
"lat": 45.45714,
"lng": -73.53451
},
"anchored": false,
"points": 2,
"weight": 6,
"target": 5,
"outline": null
}
],
"assignment": [
{
"point": "stop-1",
"zone": "west"
},
{
"point": "stop-2",
"zone": "north"
},
{
"point": "stop-3",
"zone": "east"
},
{
"point": "stop-4",
"zone": "west"
},
{
"point": "stop-5",
"zone": "west"
},
{
"point": "stop-6",
"zone": "west"
},
{
"point": "stop-7",
"zone": "east"
},
{
"point": "stop-8",
"zone": "west"
},
{
"point": "stop-9",
"zone": "north"
},
{
"point": "stop-10",
"zone": "west"
},
{
"point": "stop-11",
"zone": "east"
},
{
"point": "stop-12",
"zone": "east"
}
],
"unplaced": [],
"summary": {
"zones": 3,
"points": 12,
"weight": 23,
"largest_deviation_percent": 22.2
}
},
"meta": {
"distance": "straight_line",
"rounds": 3,
"mode": "sync"
}
}
Example response when queued (202)
{
"request_id": "req_01k8zq3m5n7p9r1t3v5x7z9b2d",
"capability": "packing.solve",
"billing": {
"unit": "job",
"quantity": 1,
"counted": true,
"breakdown": {
"packagings": 2,
"item_rows": 2,
"units": 5
}
},
"data": {
"job": {
"id": "job_01k8zq4a6c8e0g2j4l6n8q0s2u",
"status": "queued",
"capability": "packing.solve",
"poll_after_ms": 1000,
"result_url": "/v1/jobs/job_01k8zq4a6c8e0g2j4l6n8q0s2u/result",
"cancel_url": "/v1/jobs/job_01k8zq4a6c8e0g2j4l6n8q0s2u/cancel"
}
},
"meta": {
"mode": "async"
}
}
What it costs
Thousands of points, charged fractionally.
Every billable response carries a billing block with the unit, the quantity and how the quantity was derived.
Errors it can return
| Status | Code | Description |
|---|---|---|
| 401 | TOKEN_INVALID |
Unknown or revoked API token. |
| 401 | TOKEN_MISSING |
No API token. Send Authorization: Bearer <your token>. |
| 402 | QUOTA_EXHAUSTED |
The included allowance for this capability is used up for the current period. |
| 403 | CAPABILITY_NOT_ENABLED |
This capability is not enabled for your account. See :doc to enable it. |
| 403 | CAPABILITY_NOT_GRANTED_TO_KEY |
Your account has this capability, but this API key was not granted it. |
| 403 | OPEN_API_DISABLED |
Open Platform access is not enabled for this account. |
| 413 | REQUEST_TOO_LARGE |
The request body is too large. Upload large payloads as a reusable resource instead. |
| 422 | CALLBACK_NOT_VERIFIED |
Verify the callback endpoint first: POST /v1/webhooks/verify and echo the challenge back. |
| 422 | CALLBACK_URL_UNSAFE |
The callback URL must be a public HTTPS endpoint. |
| 422 | VALIDATION_FAILED |
The request failed validation. See details. |
| 429 | CONCURRENCY_LIMIT |
Too many solves are already running for this account. Retry when one finishes. |
| 429 | RATE_LIMITED |
Too many requests. Please retry after the indicated delay. |
| 500 | INTERNAL_ERROR |
An unexpected error occurred. Quote the request_id when contacting support. |