Read your organisation's franchise territories programmatically — names, status, type, centre points, the census geographies they are built from, and boundary polygons as GeoJSON. Authenticate with an API key you create in Zors; the key determines which organisation's data you see.
Three steps from nothing to a working request.
Create an API key
In the Zors app, open the account menu → Workspace Settings → API Keys, under the Management heading. Give the key a name and create it. You must be an organisation admin.
The key is shown once. Copy it immediately — it cannot be retrieved later, only revoked and replaced.
Store it as an environment variable
Keys are long-lived secrets with full read access to your territories. Keep them server-side, never in client-side code or a public repository.
Make your first request
export ZORS_API_KEY="zors_live_your_key_here"
curl -H "Authorization: Bearer $ZORS_API_KEY" \
"https://us-central1-zors-ai.cloudfunctions.net/public_api/v1/territories"Every request needs an API key. Two header forms are accepted — use whichever suits your client. If both are present, the Authorization header wins.
Authorization: Bearer zors_live_your_key_here
# or
X-API-Key: zors_live_your_key_herezors_live_ and are 42 characters long.All endpoints are relative to:
https://us-central1-zors-ai.cloudfunctions.net/public_apiReturns every territory belonging to the organisation the API key was issued for.
/v1/territoriescurl -H "Authorization: Bearer $ZORS_API_KEY" \
"https://us-central1-zors-ai.cloudfunctions.net/public_api/v1/territories"| Field | Type | Nullable | Description |
|---|---|---|---|
| territories | object[] | never null | Array of territory summaries. See the fields below. |
| count | number | never null | Number of territories in this response. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | never null | Unique territory identifier. Use this as the {id} path segment on the detail endpoint. |
| name | string | nullable | Territory name as entered in Zors. |
| status | string | nullable | One of the status values listed below. Unrecognised values are returned unchanged rather than mapped, so treat this as an open set. |
| type | string | nullable | One of the type values listed below. Like status, unmapped values pass through unchanged. |
| centerPoint | number[] | nullable | Centroid as [longitude, latitude] — GeoJSON axis order, longitude first. Null if the territory has no stored centroid. |
| lastBoundaryUpdate | string | nullable | ISO-8601 timestamp of the last boundary edit. Null if the boundary has never been edited since the territory was created. |
{
"territories": [
{
"id": "aBc123XyZ",
"name": "North Dallas",
"status": "Available",
"type": "Brick and Mortar",
"centerPoint": [
-96.8089,
32.9483
],
"lastBoundaryUpdate": "2026-07-14T16:22:41.000Z"
}
],
"count": 1
}Returns a single territory including its boundary polygon and the census geographies it is built from.
/v1/territories/{id}| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | The territory id, as returned by the list endpoint. |
curl -H "Authorization: Bearer $ZORS_API_KEY" \
"https://us-central1-zors-ai.cloudfunctions.net/public_api/v1/territories/aBc123XyZ"| Field | Type | Nullable | Description |
|---|---|---|---|
| territory | object | never null | The territory object. See the fields below. |
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | never null | Unique territory identifier. Use this as the {id} path segment on the detail endpoint. |
| name | string | nullable | Territory name as entered in Zors. |
| status | string | nullable | One of the status values listed below. Unrecognised values are returned unchanged rather than mapped, so treat this as an open set. |
| type | string | nullable | One of the type values listed below. Like status, unmapped values pass through unchanged. |
| centerPoint | number[] | nullable | Centroid as [longitude, latitude] — GeoJSON axis order, longitude first. Null if the territory has no stored centroid. |
| lastBoundaryUpdate | string | nullable | ISO-8601 timestamp of the last boundary edit. Null if the boundary has never been edited since the territory was created. |
| description | string | nullable | Free-text description entered in Zors. |
| centerPointGeoJSON | object | nullable | The centroid as a GeoJSON Point: {"type":"Point","coordinates":[lng,lat]}. Null whenever centerPoint is null. |
| boundary | object | nullable | The territory outline as a GeoJSON Polygon or MultiPolygon. Null if the territory has no stored boundary yet. |
| tracts | array | never null | Census tracts making up the territory. Always an array — empty when the territory was not built from tracts. Element shape varies; treat elements as opaque. |
| zipCodes | array | never null | ZIP codes making up the territory. Always an array. Elements are objects for ZIP-built territories and plain strings for tract-built ones, so check the type before reading. |
| state | string | nullable | Two-letter state code, e.g. "TX". |
| stateFips | string | nullable | Census state FIPS code. |
| countyFips | string | nullable | Census county FIPS code. |
{
"territory": {
"id": "aBc123XyZ",
"name": "North Dallas",
"status": "Available",
"type": "Brick and Mortar",
"centerPoint": [
-96.8089,
32.9483
],
"lastBoundaryUpdate": "2026-07-14T16:22:41.000Z",
"description": "Territory covering north Dallas suburbs.",
"centerPointGeoJSON": {
"type": "Point",
"coordinates": [
-96.8089,
32.9483
]
},
"boundary": {
"type": "Polygon",
"coordinates": [
[
[
-96.8712,
32.9104
],
[
-96.7433,
32.9104
],
[
-96.7433,
32.9861
],
[
-96.8712,
32.9861
],
[
-96.8712,
32.9104
]
]
]
},
"tracts": [],
"zipCodes": [
"75080",
"75081",
"75082"
],
"state": "TX",
"stateFips": "48",
"countyFips": "113"
}
}Territories carry a status and a type. Both are returned as human-readable strings. Treat them as an open set: a value the API does not recognise is passed through unchanged rather than replaced, so your code should handle unexpected strings without failing.
| status | Meaning |
|---|---|
| Available | Open for sale, no commitment in place. |
| Pending | A sale is in progress but not yet closed. |
| Sold (Not Open) | Awarded to a franchisee; the unit is not trading yet. |
| Sold (Open) | Awarded and operating. |
| For Sale | An existing unit being resold. |
| Prospect | Under consideration; not yet available. |
| Closed | No longer operating. |
| type | Meaning |
|---|---|
| Brick and Mortar | Fixed premises. |
| Mobile | Service travels to the customer. |
| Remote | Delivered without a physical location. |
| Other | Anything not covered above. |
Errors return a matching HTTP status and a JSON body with a single error key. There are no machine-readable error codes, so branch on the HTTP status rather than on the message text — wording may change.
{
"error": "Invalid API key."
}| Status | error | When it happens |
|---|---|---|
401 | Missing API key. Provide it as 'Authorization: Bearer <key>' or 'X-API-Key: <key>'. | No API key was sent, or the Authorization header was not in the exact form "Bearer <key>". |
401 | Invalid API key. | The key does not match any issued key. |
401 | This API key has been revoked. | The key was valid but has since been revoked in Zors. Create a new key. |
404 | Not found | The path is not one of the documented endpoints. |
404 | Territory not found | No territory with that id exists in your organisation, it has been deleted, or it belongs to a different organisation. |
405 | Method not allowed | Only GET and OPTIONS are supported. Note this is checked before authentication, so a POST with an invalid key still returns 405. |
429 | Rate limit exceeded. Maximum 1000 requests per hour. Retry after N seconds. | The key exceeded its hourly quota. The retry delay is in the message text; there is currently no Retry-After header. |
500 | Internal server error | An unexpected error. Safe to retry with backoff; contact support if it persists. |
Requests are limited to 1,000 per hour, per API key. The window is fixed rather than rolling: it starts on your first request and resets an hour later. Because the limit is per key, issuing a second key gives that integration its own separate quota.
| Field | Type | Description |
|---|---|---|
| X-RateLimit-Limit | number | Your quota — currently 1000. |
| X-RateLimit-Remaining | number | Requests left in the current window. Approximate under concurrency. |
| X-RateLimit-Reset | number | Unix timestamp (seconds) when the window resets. Not sent on a 429 response. |
The API sends permissive CORS headers, so browser requests will technically succeed from any origin. That is a convenience for local experimentation, not an invitation to call it from your front end — doing so exposes a long-lived key with read access to every territory you own. Call it from your server and pass the results on.
| Field | Type | Description |
|---|---|---|
| Access-Control-Allow-Origin | * | Any origin may call the API. |
| Access-Control-Allow-Methods | GET, OPTIONS | Only reads are supported. |
| Access-Control-Allow-Headers | Content-Type, Authorization, X-API-Key | Headers you may send. |
| Access-Control-Expose-Headers | X-RateLimit-* | Rate-limit headers are readable from browser JavaScript. |
The API is versioned in the path. Everything documented here is v1. Additive changes — new fields on a response, new endpoints — can happen at any time, so parse responses tolerantly and ignore fields you do not recognise.
Two things are worth designing around today. The list endpoint returns every territory in one response with no pagination; if you have a large network, expect that response to be large, and expect pagination parameters to be added in future. And boundary polygons are simplified slightly for transport — they are suitable for mapping and analysis, but the authoritative description of a territory is the one in your franchise agreement, not this API.
Need something that isn't here?
Write access, filtering, pagination, webhooks and other resources are not part of v1. If your integration needs them, get in touch — and if you are automating workflows rather than building against an API, the Zapier integration may already do what you need.