Docs / AI Box And Video Analytics Software / API Reference
AI Box Platform API
The part of the ARSA AI Box Series dashboard that is the same on every product: sites and their zones, cameras and what is drawn on them, live video and recordings, alerts, alert contacts and webhooks, sirens and relays, reports, and webhooks for integrators. Each product adds its own API beside this one, under /api/<product>/v1.
Calling the API
Send an API account's token in the Authorization header: Authorization: Bearer <API_KEY>. An admin makes the account on the dashboard (API accounts); the token is shown once. The token sees only the sites its account has a role on, and only what its scopes allow (read, write, export, personal_data). Each operation says who can call it and which scopes it needs. The dashboard serves HTTPS with the box's own certificate: an admin downloads a copy (box.pem) on the dashboard (System, then API accounts, then Download the box's certificate) with its SHA-256 fingerprint, and a token with read can fetch it with getBoxCertificate. Check the fingerprint before your client trusts the file.
Errors are RFC 9457 problem details (application/problem+json) with a stable code. Times are RFC 3339; hourly and daily figures follow the site's time zone. Long lists are paged: pass limit, then the answer's next_before as before. Calls are limited per API account (by default 600 a minute; then 429 with Retry-After).
Webhooks for Integrators
An API account can subscribe a receiver of its own (/webhook-subscriptions): detection events in batches, periodic summaries and alerts, signed the Standard Webhooks way. The webhooks guide explains the payloads, the signature and the retries.
1.0.0
Sites And Zones
Sites (a plant, a store, a corridor) and the zones inside them.
The Sites The API Account Has A Role On
GET/sites
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"name": "North Plant",
"timezone": "Asia/Jakarta",
"created": "2026-10-01T02:00:00Z",
"version": 4,
"my_role": "viewer",
"location": {
"lat": -6.2,
"lon": 106.8
},
"area": null,
"zones": 3
},
{
"id": 2,
"name": "Warehouse B",
"timezone": "Asia/Jakarta",
"created": "2026-10-03T02:00:00Z",
"version": 1,
"my_role": "admin",
"location": null,
"area": null,
"zones": 0
}
]
}A Site
GET/sites/{id}
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"name": "North Plant",
"timezone": "Asia/Jakarta",
"created": "2026-10-01T02:00:00Z",
"version": 4,
"my_role": "viewer",
"location": {
"lat": -6.2,
"lon": 106.8
},
"area": null,
"zones": 3
}Rename A Site, Change Its Timezone, Or Place It On The Map (Pinpoint, Boundary)
PATCH/sites/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
name | string | |
timezone | string | |
location | one of | The pinpoint; null removes it. |
area | one of | The boundary; null removes it. |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/sites/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","timezone":"Asia/Jakarta","location":{"lat":-6.2,"lon":106.8},"area":[[12,12],[12,12],[12,12]]}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/sites/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"timezone": "Asia/Jakarta",
"location": {
"lat": -6.2,
"lon": 106.8
},
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"timezone": "Asia/Jakarta",
"location": {
"lat": -6.2,
"lon": 106.8
},
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"name": "Main gate",
"timezone": "Asia/Jakarta",
"created": "2026-11-02T08:00:00Z",
"version": 3,
"my_role": "admin",
"location": {
"lat": -6.2,
"lon": 106.8
},
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
],
"zones": 12
}The Site's Zones
GET/sites/{id}/zones
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/zones" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/zones",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/zones", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
],
"created": "2026-11-02T08:00:00Z",
"version": 3
}
]
}Add A Zone To The Site
POST/sites/{id}/zones
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
namerequired | string | |
area | one of |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/zones" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","area":[[12,12],[12,12],[12,12]]}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/zones",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/zones", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
}),
});
console.log(r.status, await r.text());Response 201
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
],
"created": "2026-11-02T08:00:00Z",
"version": 3
}Rename A Zone Or Change Its Outline
PATCH/sites/{id}/zones/{zone}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
zonerequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
name | string | |
area | one of | The outline; null removes it. |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/sites/1/zones/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","area":[[12,12],[12,12],[12,12]]}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/sites/1/zones/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/zones/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
]
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"area": [
[
12,
12
],
[
12,
12
],
[
12,
12
]
],
"created": "2026-11-02T08:00:00Z",
"version": 3
}Delete A Zone (Its Cameras Stay On The Site, In No Zone)
DELETE/sites/{id}/zones/{zone}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
zonerequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/sites/1/zones/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/sites/1/zones/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/zones/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Cameras
Cameras, their streams, what is drawn on them (lines, zones, lanes) and snapshots.
The Cameras Of The API Account's Sites
GET/cameras
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/cameras" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/cameras",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 11,
"name": "Gate 1",
"site_id": 1,
"zone_id": 2,
"password_set": true,
"over_licence": false,
"status": {
"running": true,
"capture": "streaming"
}
},
{
"id": 12,
"name": "Loading Bay",
"site_id": 1,
"zone_id": null,
"password_set": true,
"over_licence": false,
"status": {
"running": true,
"capture": "streaming"
}
}
]
}Add A Camera To A Site
POST/cameras
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
Idempotency-Key header | string |
Body
| Field | Type | Description |
|---|---|---|
site_idrequired | integer | |
namerequired | string | |
rtsp_urlrequired | string | |
host | string | |
username | string | |
password | string | |
width | integer | |
height | integer | |
fps | integer |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/cameras" \
-H "Authorization: Bearer <API_KEY>" \
-H "Idempotency-Key: <IDEMPOTENCY_KEY>" \
-H "Content-Type: application/json" \
-d '{"site_id":1,"name":"Main gate","rtsp_url":"https://<RECEIVER_HOST>/hooks/arsa","host":"<host>","username":"<username>","password":"<password>","width":12,"height":12,"fps":12}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/cameras",
headers={"Authorization": "Bearer <API_KEY>", "Idempotency-Key": "<IDEMPOTENCY_KEY>"},
json={
"site_id": 1,
"name": "Main gate",
"rtsp_url": "https://<RECEIVER_HOST>/hooks/arsa",
"host": "<host>",
"username": "<username>",
"password": "<password>",
"width": 12,
"height": 12,
"fps": 12
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Idempotency-Key": "<IDEMPOTENCY_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"site_id": 1,
"name": "Main gate",
"rtsp_url": "https://<RECEIVER_HOST>/hooks/arsa",
"host": "<host>",
"username": "<username>",
"password": "<password>",
"width": 12,
"height": 12,
"fps": 12
}),
});
console.log(r.status, await r.text());Response 201
{
"id": 1,
"name": "Main gate",
"rtsp_url": "https://<RECEIVER_HOST>/hooks/arsa",
"password_set": true,
"site_id": 1,
"status": {},
"over_licence": true,
"zone_id": 1
}A Camera
GET/cameras/{id}
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/cameras/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/cameras/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"name": "Main gate",
"rtsp_url": "https://<RECEIVER_HOST>/hooks/arsa",
"password_set": true,
"site_id": 1,
"status": {},
"over_licence": true,
"zone_id": 1
}Change A Camera (JSON Merge Patch, If-Match)
PATCH/cameras/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
If-Match header | string | The ETag read before; required for changes. |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
412 | The ETag in If-Match is stale |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
428 | If-Match is required |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/cameras/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "If-Match: <IF_MATCH>"import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/cameras/1",
headers={"Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"name": "Main gate",
"rtsp_url": "https://<RECEIVER_HOST>/hooks/arsa",
"password_set": true,
"site_id": 1,
"status": {},
"over_licence": true,
"zone_id": 1
}Delete A Camera
DELETE/cameras/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
If-Match header | string | The ETag read before; required for changes. |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
412 | The ETag in If-Match is stale |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/cameras/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "If-Match: <IF_MATCH>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/cameras/1",
headers={"Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>" },
});
console.log(r.status, await r.text());Put A Camera In A Zone Of Its Site, Or In None
PUT/cameras/{id}/zone
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
zone_idrequired | integer | A zone of the camera's own site, or null for none. |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PUT "https://<BOX_HOST>/api/v1/cameras/1/zone" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"zone_id":1}'import requests
r = requests.put(
"https://<BOX_HOST>/api/v1/cameras/1/zone",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"zone_id": 1
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1/zone", {
method: "PUT",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"zone_id": 1
}),
});
console.log(r.status, await r.text());Response 200
{
"camera_id": 1,
"site_id": 1,
"zone_id": 1
}The Camera's Geometry (Lines, Zones, Lanes)
GET/cameras/{id}/geometry
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/cameras/1/geometry" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/cameras/1/geometry",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1/geometry", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{}Replace The Geometry (If-Match)
PUT/cameras/{id}/geometry
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
If-Match header | string | The ETag read before; required for changes. |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
412 | The ETag in If-Match is stale |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
428 | If-Match is required |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X PUT "https://<BOX_HOST>/api/v1/cameras/1/geometry" \
-H "Authorization: Bearer <API_KEY>" \
-H "If-Match: <IF_MATCH>" \
-H "Content-Type: application/json" \
-d '{}'import requests
r = requests.put(
"https://<BOX_HOST>/api/v1/cameras/1/geometry",
headers={"Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>"},
json={},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1/geometry", {
method: "PUT",
headers: { "Authorization": "Bearer <API_KEY>", "If-Match": "<IF_MATCH>", "Content-Type": "application/json" },
body: JSON.stringify({}),
});
console.log(r.status, await r.text());Response 200
{}The Camera's Current Frame
GET/cameras/{id}/snapshot
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | JPEG |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/cameras/1/snapshot" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/cameras/1/snapshot",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/cameras/1/snapshot", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Live Video And Recordings
Live view (WebRTC, HLS), recorded segments, MP4 exports and event clips.
WHEP: The Browser's SDP Offer For The Camera's Live Stream; Answers The SDP Answer (RFC 9725)
POST/live/{camera}/whep
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: write, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
camerarequired path | integer |
Responses
| Status | Meaning |
|---|---|
201 | the SDP answer |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/live/1/whep" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/live/1/whep",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/live/1/whep", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());The Camera's HLS Playlist (The Fallback When WebRTC Cannot Connect)
GET/live/{camera}/hls.m3u8
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
camerarequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/live/1/hls.m3u8" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/live/1/hls.m3u8",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/live/1/hls.m3u8", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());An HLS Segment
GET/live/{camera}/segments/{name}
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
camerarequired path | integer | |
namerequired path | string |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/live/1/segments/{name}" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/live/1/segments/{name}",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/live/1/segments/{name}", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());The Camera's Recorded Segments (One Camera Per Query)
GET/recordings
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
camerarequired query | integer | |
from query | string | |
to query | string | |
cursor query | string | |
limit query | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/recordings?camera=1&from=2026-11-01T00%3A00%3A00%2B07%3A00&to=2026-11-02T00%3A00%3A00%2B07%3A00&limit=50" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/recordings?camera=1&from=2026-11-01T00%3A00%3A00%2B07%3A00&to=2026-11-02T00%3A00%3A00%2B07%3A00&limit=50",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/recordings?camera=1&from=2026-11-01T00%3A00%3A00%2B07%3A00&to=2026-11-02T00%3A00%3A00%2B07%3A00&limit=50", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{}Export Up To 60 Minutes Of A Camera To MP4 (A Job; Recorded In The Audit Log)
POST/recordings/export
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: write, export, personal_data.
Body
| Field | Type | Description |
|---|---|---|
camerarequired | integer | |
fromrequired | date-time | |
torequired | date-time |
Responses
| Status | Meaning |
|---|---|
202 | a job; poll its Location (/api/v1/recordings/exports/{job}) |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/recordings/export" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"camera":12,"from":"2026-11-02T08:00:00Z","to":"2026-11-02T08:00:00Z"}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/recordings/export",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"camera": 12,
"from": "2026-11-02T08:00:00Z",
"to": "2026-11-02T08:00:00Z"
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/recordings/export", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"camera": 12,
"from": "2026-11-02T08:00:00Z",
"to": "2026-11-02T08:00:00Z"
}),
});
console.log(r.status, await r.text());Response 202
{}An Export Or Clip Job (File Link To The Dashboard When Done)
GET/recordings/exports/{job}
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: read, export, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
jobrequired path | string |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/recordings/exports/{job}" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/recordings/exports/{job}",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/recordings/exports/{job}", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{}The Exported MP4 (Streamed; Byte Ranges)
GET/recordings/exports/{job}/file
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: read, export, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
jobrequired path | string |
Responses
| Status | Meaning |
|---|---|
200 | OK |
206 | a byte range |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/recordings/exports/{job}/file" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/recordings/exports/{job}/file",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/recordings/exports/{job}/file", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());A Clip Around An Event (By Default 10 s Before To 20 s After; A Job; Recorded In The Audit Log)
POST/events/{id}/clip
Who can call it: An account with a role (viewer or admin) on the camera's site. Token scopes: write, export, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | string |
Responses
| Status | Meaning |
|---|---|
202 | a job; poll its Location (/api/v1/recordings/exports/{job}) |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
502 | The analysis engine refused the request (engine_refused) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/events/{id}/clip" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/events/{id}/clip",
headers={"Authorization": "Bearer <API_KEY>"},
json={},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/events/{id}/clip", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({}),
});
console.log(r.status, await r.text());Response 202
{}Alerts
Alerts raised from detections; list them, read one, acknowledge it.
Alerts, Newest First
GET/alerts
Who can call it: Any API account; the answer covers the sites it has a role on. Token scopes: read. Without personal_data the answer is the anonymous form: no names, plates, pictures or footage.
Parameters
| Field | Type | Description |
|---|---|---|
site query | integer | only this site |
state query | string | open or acknowledged |
kind query | string | only this kind |
before query | integer | the next_before of the previous page |
limit query | integer | 1..500, default 50 |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/alerts?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/alerts?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/alerts?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"camera_id": 1,
"kind": "default",
"severity": "info",
"first": "2026-11-02T08:00:00Z",
"last": "2026-11-02T08:00:00Z",
"count": 12,
"event_id": "<event_id>",
"details": "<details>",
"state": "open",
"acked_by": "<acked_by>",
"acked_by_name": "<acked_by_name>",
"acked": "2026-11-02T08:00:00Z",
"note": "<note>",
"footage": "<footage>",
"footage_state": "none"
}
],
"next_before": 12
}Open Alerts Per Visible Site
GET/alerts/summary
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/alerts/summary" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/alerts/summary",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/alerts/summary", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"open": 12,
"sites": [
{
"site_id": 1,
"open": 12
}
]
}The Product's Alert Kinds
GET/alerts/kinds
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/alerts/kinds" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/alerts/kinds",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/alerts/kinds", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": "<id>",
"severity": "info",
"window_s": 60
}
]
}An Alert
GET/alerts/{id}
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read. Without personal_data the answer is the anonymous form: no names, plates, pictures or footage.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/alerts/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/alerts/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/alerts/1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"camera_id": 1,
"kind": "default",
"severity": "info",
"first": "2026-11-02T08:00:00Z",
"last": "2026-11-02T08:00:00Z",
"count": 12,
"event_id": "<event_id>",
"details": "<details>",
"state": "open",
"acked_by": "<acked_by>",
"acked_by_name": "<acked_by_name>",
"acked": "2026-11-02T08:00:00Z",
"note": "<note>",
"footage": "<footage>",
"footage_state": "none"
}Acknowledge An Alert, With An Optional Note (Recorded In The Audit Log)
POST/alerts/{id}/ack
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
note | string |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/alerts/1/ack" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"note":"<note>"}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/alerts/1/ack",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"note": "<note>"
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/alerts/1/ack", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"note": "<note>"
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"camera_id": 1,
"kind": "default",
"severity": "info",
"first": "2026-11-02T08:00:00Z",
"last": "2026-11-02T08:00:00Z",
"count": 12,
"event_id": "<event_id>",
"details": "<details>",
"state": "open",
"acked_by": "<acked_by>",
"acked_by_name": "<acked_by_name>",
"acked": "2026-11-02T08:00:00Z",
"note": "<note>",
"footage": "<footage>",
"footage_state": "none"
}Contacts And Notifications
Who is told about alerts, and the log of the messages sent to them.
The Site's Alert Contacts
GET/sites/{id}/contacts
Who can call it: An account that is an admin of the site. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/contacts" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/contacts",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/contacts", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": true
}
]
}Add An Alert Contact
POST/sites/{id}/contacts
Who can call it: An account that is an admin of the site. Token scopes: write, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
namerequired | string | |
email | string | |
whatsapp | string | E.164, e.g. +6281234567890 |
telegram | string | a Telegram chat id |
language | enum | en, id |
channels | enum[] | |
kinds | string[] | alert kinds; [] = every kind |
min_severity | enum | info, warning, critical |
quiet_start | string | |
quiet_end | string | |
active | boolean |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/contacts" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","email":"ops@example.com","whatsapp":"<whatsapp>","telegram":"<telegram>","language":"en","channels":["email"],"kinds":["default"],"min_severity":"info","quiet_start":"08:00","quiet_end":"08:00","active":true}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/contacts",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/contacts", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": true
}),
});
console.log(r.status, await r.text());Response 201
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": true
}Change An Alert Contact
PATCH/contacts/{id}
Who can call it: An account that is an admin of the site. Token scopes: write, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
name | string | |
email | string | |
whatsapp | string | E.164, e.g. +6281234567890 |
telegram | string | a Telegram chat id |
language | enum | en, id |
channels | enum[] | |
kinds | string[] | alert kinds; [] = every kind |
min_severity | enum | info, warning, critical |
quiet_start | string | |
quiet_end | string | |
active | boolean |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/contacts/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","email":"ops@example.com","whatsapp":"<whatsapp>","telegram":"<telegram>","language":"en","channels":["email"],"kinds":["default"],"min_severity":"info","quiet_start":"08:00","quiet_end":"08:00","active":true}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/contacts/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/contacts/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": true
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"email": "ops@example.com",
"whatsapp": "<whatsapp>",
"telegram": "<telegram>",
"language": "en",
"channels": [
"email"
],
"kinds": [
"default"
],
"min_severity": "info",
"quiet_start": "08:00",
"quiet_end": "08:00",
"active": true
}Remove An Alert Contact
DELETE/contacts/{id}
Who can call it: An account that is an admin of the site. Token scopes: write, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/contacts/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/contacts/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/contacts/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Deliveries, Newest First
GET/notify/deliveries
Who can call it: An account that is an admin of a site; the answer covers those sites. Token scopes: read, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
site query | integer | only this site |
ref query | string | e.g. alert:12 |
state query | string | queued, sending, sent or failed |
before query | integer | the next_before of the previous page |
limit query | integer | 1..500, default 50 |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/notify/deliveries?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/notify/deliveries?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/notify/deliveries?site=1&before=2026-11-02T08%3A00%3A00Z&limit=50", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"ref": "<ref>",
"channel": "email",
"recipient": "<recipient>",
"state": "queued",
"attempts": 12,
"created": "2026-11-02T08:00:00Z",
"next": "2026-11-02T08:00:00Z",
"sent": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
],
"next_before": 12
}Queue A Failed Delivery Again (A New Retry Period)
POST/notify/deliveries/{id}/retry
Who can call it: An account that is an admin of the site. Token scopes: write, personal_data.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/notify/deliveries/1/retry" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/notify/deliveries/1/retry",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/notify/deliveries/1/retry", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"ref": "<ref>",
"channel": "email",
"recipient": "<recipient>",
"state": "queued",
"attempts": 12,
"created": "2026-11-02T08:00:00Z",
"next": "2026-11-02T08:00:00Z",
"sent": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}Webhooks
Signed HTTP posts of alerts to other systems (Standard Webhooks).
The Site's Webhooks
GET/sites/{id}/webhooks
Who can call it: An account that is an admin of the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/webhooks" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/webhooks",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/webhooks", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": true,
"created": "2026-11-02T08:00:00Z"
}
]
}Add A Webhook (Its Secret Is In This Answer Only)
POST/sites/{id}/webhooks
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
namerequired | string | |
urlrequired | string | http(s); internal addresses refused |
kinds | string[] | alert kinds; [] = every kind |
active | boolean |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/webhooks" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","url":"https://<RECEIVER_HOST>/hooks/arsa","kinds":["default"],"active":true}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/webhooks",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/webhooks", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": true
}),
});
console.log(r.status, await r.text());Response 201
{
"webhook": {
"id": 1,
"site_id": 1,
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": true,
"created": "2026-11-02T08:00:00Z"
},
"secret": "<WEBHOOK_SECRET>"
}Change A Webhook
PATCH/webhooks/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
name | string | |
url | string | http(s); internal addresses refused |
kinds | string[] | alert kinds; [] = every kind |
active | boolean |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/webhooks/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","url":"https://<RECEIVER_HOST>/hooks/arsa","kinds":["default"],"active":true}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/webhooks/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhooks/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": true
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"kinds": [
"default"
],
"active": true,
"created": "2026-11-02T08:00:00Z"
}Remove A Webhook
DELETE/webhooks/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Done |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/webhooks/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/webhooks/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhooks/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());A New Secret (The Old One Stops At Once)
POST/webhooks/{id}/secret
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhooks/1/secret" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhooks/1/secret",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhooks/1/secret", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"secret": "<WEBHOOK_SECRET>"
}Post A Test Event Now And Report The Outcome
POST/webhooks/{id}/test
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhooks/1/test" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhooks/1/test",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhooks/1/test", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"ok": true,
"error": "Text",
"retryable": true
}Webhooks For Integrators
An API account's subscriptions: detection events in batches, periodic summaries and alerts, signed, with a delivery log.
The Product's Event Types For Webhook Subscriptions, And The Limits
GET/webhook-event-types
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/webhook-event-types" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/webhook-event-types",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-event-types", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"product": "traffic",
"items": [
{
"id": "traffic.passage",
"label": {
"en": "Vehicle passages",
"id": "Lintasan kendaraan"
},
"personal": [
"plate",
"photo"
],
"personal_admin": false,
"personal_only": false
},
{
"id": "traffic.speeding",
"label": {
"en": "Speeding",
"id": "Melaju terlalu cepat"
},
"personal": [
"plate"
],
"personal_admin": false,
"personal_only": false
}
],
"summaries": true,
"periods": [
"15m",
"1h",
"1d"
],
"batch_seconds": {
"min": 1,
"max": 3600,
"default": 10
},
"batch_max": {
"min": 1,
"max": 1000,
"default": 100
},
"max_per_account": 10
}The Webhook Subscriptions Of The Token's Own API Account
GET/webhook-subscriptions
Who can call it: The API account that owns the subscription (its own token). Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
account query | integer | only this API account's |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/webhook-subscriptions?account=1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/webhook-subscriptions?account=1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions?account=1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"account_id": 1,
"account": "erp-sync",
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"<event_type>"
],
"sites": [
12
],
"batch_seconds": 60,
"batch_max": 12,
"summary": "15m",
"alerts": true,
"active": true,
"created": "2026-11-02T08:00:00Z",
"created_by": "<created_by>",
"health": {
"state": "ok",
"last_success": "2026-11-02T08:00:00Z",
"last_failure": "2026-11-02T08:00:00Z",
"last_error": "<last_error>",
"dropped": 12,
"queued": 12
},
"version": 3
}
]
}Subscribe The API Account (The Secret Is In This Answer Only)
POST/webhook-subscriptions
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Body
| Field | Type | Description |
|---|---|---|
account_id | integer | the API account; a token: its own (may be left out) |
namerequired | string | |
urlrequired | string | http(s); private networks only when the box allows them |
event_types | string[] | the product's event types that are on (GET /webhook-event-types) |
sites | integer[] | site ids within the account's; [] = every site of the account |
batch_seconds | integer | default 10 on creation |
batch_max | integer | default 100 on creation |
summary | enum | periodic summaries of the overview's figures per site, in the site's time zone; null: none 15m, 1h, 1d, null |
alerts | boolean | default false on creation |
active | boolean | default true on creation |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
409 | Conflict |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhook-subscriptions" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"ERP receiver","url":"https://<RECEIVER_HOST>/hooks/arsa","event_types":["traffic.passage","traffic.speeding"],"sites":[1],"summary":"1h","alerts":true}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhook-subscriptions",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "ERP receiver",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"traffic.passage",
"traffic.speeding"
],
"sites": [
1
],
"summary": "1h",
"alerts": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "ERP receiver",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"traffic.passage",
"traffic.speeding"
],
"sites": [
1
],
"summary": "1h",
"alerts": true
}),
});
console.log(r.status, await r.text());Response 201
{
"subscription": {
"id": 5,
"account_id": 2,
"account": "erp-sync",
"name": "ERP receiver",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"traffic.passage",
"traffic.speeding"
],
"sites": [
1
],
"batch_seconds": 10,
"batch_max": 100,
"summary": "1h",
"alerts": true,
"active": true,
"created": "2026-11-06T03:00:00Z",
"created_by": "api:erp-sync",
"health": {
"state": "ok",
"last_success": null,
"last_failure": null,
"last_error": null,
"dropped": 0,
"queued": 0
},
"version": 1
},
"secret": "<WEBHOOK_SECRET>"
}A Webhook Subscription And Its Health
GET/webhook-subscriptions/{id}
Who can call it: The API account that owns the subscription (its own token). Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/webhook-subscriptions/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"account_id": 1,
"account": "erp-sync",
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"<event_type>"
],
"sites": [
12
],
"batch_seconds": 60,
"batch_max": 12,
"summary": "15m",
"alerts": true,
"active": true,
"created": "2026-11-02T08:00:00Z",
"created_by": "<created_by>",
"health": {
"state": "ok",
"last_success": "2026-11-02T08:00:00Z",
"last_failure": "2026-11-02T08:00:00Z",
"last_error": "<last_error>",
"dropped": 12,
"queued": 12
},
"version": 3
}Change A Webhook Subscription (Pause It, Or Resume It From Now On)
PATCH/webhook-subscriptions/{id}
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
account_id | integer | cannot change |
name | string | |
url | string | http(s); private networks only when the box allows them |
event_types | string[] | the product's event types that are on (GET /webhook-event-types) |
sites | integer[] | site ids within the account's; [] = every site of the account |
batch_seconds | integer | default 10 on creation |
batch_max | integer | default 100 on creation |
summary | enum | periodic summaries of the overview's figures per site, in the site's time zone; null: none 15m, 1h, 1d, null |
alerts | boolean | default false on creation |
active | boolean | default true on creation |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/webhook-subscriptions/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"account_id":1,"name":"Main gate","url":"https://<RECEIVER_HOST>/hooks/arsa","event_types":["<event_type>"],"sites":[12],"batch_seconds":60,"batch_max":12,"summary":"15m","alerts":true,"active":true}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"account_id": 1,
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"<event_type>"
],
"sites": [
12
],
"batch_seconds": 60,
"batch_max": 12,
"summary": "15m",
"alerts": True,
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"account_id": 1,
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"<event_type>"
],
"sites": [
12
],
"batch_seconds": 60,
"batch_max": 12,
"summary": "15m",
"alerts": true,
"active": true
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"account_id": 1,
"account": "erp-sync",
"name": "Main gate",
"url": "https://<RECEIVER_HOST>/hooks/arsa",
"event_types": [
"<event_type>"
],
"sites": [
12
],
"batch_seconds": 60,
"batch_max": 12,
"summary": "15m",
"alerts": true,
"active": true,
"created": "2026-11-02T08:00:00Z",
"created_by": "<created_by>",
"health": {
"state": "ok",
"last_success": "2026-11-02T08:00:00Z",
"last_failure": "2026-11-02T08:00:00Z",
"last_error": "<last_error>",
"dropped": 12,
"queued": 12
},
"version": 3
}Delete A Webhook Subscription And Its Delivery Log
DELETE/webhook-subscriptions/{id}
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/webhook-subscriptions/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());A New Signing Secret (The Old One Stops At Once); In This Answer Only
POST/webhook-subscriptions/{id}/secret
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhook-subscriptions/1/secret" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1/secret",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1/secret", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"secret": "<WEBHOOK_SECRET>"
}Post A Signed Test Message Now And Report The Outcome (Kept In The Delivery Log As A Test)
POST/webhook-subscriptions/{id}/test
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhook-subscriptions/1/test" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1/test",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1/test", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"ok": true,
"error": "Text",
"retryable": true
}The Subscription's Delivery Log, Newest First
GET/webhook-subscriptions/{id}/deliveries
Who can call it: The API account that owns the subscription (its own token). Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
state query | enum | queued, sending, sent, failed, dropped |
before query | integer | the next_before of the previous page |
limit query | integer | 1..500, default 50 |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/webhook-subscriptions/1/deliveries?state=queued&before=2026-11-02T08%3A00%3A00Z&limit=50" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/webhook-subscriptions/1/deliveries?state=queued&before=2026-11-02T08%3A00%3A00Z&limit=50",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-subscriptions/1/deliveries?state=queued&before=2026-11-02T08%3A00%3A00Z&limit=50", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"subscription_id": 1,
"kind": "events",
"site_id": 1,
"count": 12,
"bytes": 12,
"webhook_id": "<webhook_id>",
"state": "queued",
"attempts": 12,
"status": 12,
"created": "2026-11-02T08:00:00Z",
"next": "2026-11-02T08:00:00Z",
"expires": "2026-11-02T08:00:00Z",
"sent": "2026-11-02T08:00:00Z",
"last_error": "<last_error>",
"retryable": true
}
],
"next_before": 12
}Send A Failed Or Waiting Delivery Again Now, With A New 24 h
POST/webhook-deliveries/{id}/retry
Who can call it: The API account that owns the subscription (its own token). Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/webhook-deliveries/1/retry" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/webhook-deliveries/1/retry",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/webhook-deliveries/1/retry", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"subscription_id": 1,
"kind": "events",
"site_id": 1,
"count": 12,
"bytes": 12,
"webhook_id": "<webhook_id>",
"state": "queued",
"attempts": 12,
"status": 12,
"created": "2026-11-02T08:00:00Z",
"next": "2026-11-02T08:00:00Z",
"expires": "2026-11-02T08:00:00Z",
"sent": "2026-11-02T08:00:00Z",
"last_error": "<last_error>",
"retryable": true
}Sirens And Relays
Outputs switched by alerts: Modbus TCP boards, HTTP relays and sirens, ONVIF alarm outputs.
The Site's Siren And Relay Outputs, With Their State
GET/sites/{id}/relays
Who can call it: An account that is an admin of the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/relays" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/relays",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/relays", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password_set": true,
"state": {
"on": true,
"since": "2026-11-02T08:00:00Z",
"until": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
}
]
}Add An Output (Modbus TCP Board, HTTP Relay Or Siren, ONVIF Camera Alarm Output)
POST/sites/{id}/relays
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
namerequired | string | |
kindrequired | enum | modbus, http, onvif |
settingsrequired | object | per kind. modbus: host, port (502), unit (1), coil, invert. http: on_url, off_url, method (GET|POST|PUT), username. onvif: device_url, relay_token, username. |
kinds | string[] | |
cameras | integer[] | |
min_severity | enum | info, warning, critical |
mode | enum | pulse, until_ack |
pulse_s | integer | |
active | boolean | |
password | string |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/relays" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","kind":"modbus","settings":{},"kinds":["default"],"cameras":[12],"min_severity":"info","mode":"pulse","pulse_s":60,"active":true,"password":"<password>"}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/relays",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": True,
"password": "<password>"
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/relays", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password": "<password>"
}),
});
console.log(r.status, await r.text());Response 201
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password_set": true,
"state": {
"on": true,
"since": "2026-11-02T08:00:00Z",
"until": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
}Change An Output (Paused While On: Switched Off)
PATCH/relays/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
name | string | |
kind | enum | modbus, http, onvif |
settings | object | per kind. modbus: host, port (502), unit (1), coil, invert. http: on_url, off_url, method (GET|POST|PUT), username. onvif: device_url, relay_token, username. |
kinds | string[] | |
cameras | integer[] | |
min_severity | enum | info, warning, critical |
mode | enum | pulse, until_ack |
pulse_s | integer | |
active | boolean | |
password | string | absent: keep; empty: remove |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/relays/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"name":"Main gate","kind":"modbus","settings":{},"kinds":["default"],"cameras":[12],"min_severity":"info","mode":"pulse","pulse_s":60,"active":true,"password":"<password>"}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/relays/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": True,
"password": "<password>"
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/relays/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password": "<password>"
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password_set": true,
"state": {
"on": true,
"since": "2026-11-02T08:00:00Z",
"until": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
}Remove An Output (Switched Off First)
DELETE/relays/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/relays/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/relays/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/relays/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());A 2 s Test Pulse Now; Reports Whether The Device Switched On
POST/relays/{id}/test
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/relays/1/test" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/relays/1/test",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/relays/1/test", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"ok": true,
"error": "Text"
}Switch An Output Off Now (Silence A Siren)
POST/relays/{id}/off
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
409 | Conflict |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/relays/1/off" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/relays/1/off",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/relays/1/off", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"name": "Main gate",
"kind": "modbus",
"settings": {},
"kinds": [
"default"
],
"cameras": [
12
],
"min_severity": "info",
"mode": "pulse",
"pulse_s": 60,
"active": true,
"password_set": true,
"state": {
"on": true,
"since": "2026-11-02T08:00:00Z",
"until": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
}The Last 50 Switches, Newest First
GET/relays/{id}/events
Who can call it: An account that is an admin of the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/relays/1/events" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/relays/1/events",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/relays/1/events", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"at": "2026-11-02T08:00:00Z",
"action": "on",
"cause": "alert",
"alert_id": 1,
"ok": true,
"error": "Text"
}
]
}Reports
PDF and XLSX reports made now or on a schedule.
The Reports This Dashboard Offers
GET/reports/kinds
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/reports/kinds" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/reports/kinds",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/reports/kinds", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": "<id>",
"title": {
"en": "<en>",
"id": "<id>"
}
}
]
}The Site's Report Files, Newest First (Kept 90 Days)
GET/sites/{id}/reports
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer | |
limit query | integer | 1..500, default 100 |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/reports?limit=50" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/reports?limit=50",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/reports?limit=50", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"report": "<report>",
"title": "Weekly summary",
"period": "1h",
"language": "en",
"format": "pdf",
"bytes": 12,
"created": "2026-11-02T08:00:00Z",
"created_by": "<created_by>",
"schedule_id": 1,
"download": "<download>"
}
]
}Generate A Report Now For Local Dates In The Site's Time Zone (At Most 366 Days)
POST/sites/{id}/reports
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
reportrequired | string | |
fromrequired | date | |
torequired | date | |
language | enum | en, id |
formats | enum[] |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/reports" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"report":"<report>","from":"2026-11-02","to":"2026-11-02","language":"en","formats":["pdf"]}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/reports",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"report": "<report>",
"from": "2026-11-02",
"to": "2026-11-02",
"language": "en",
"formats": [
"pdf"
]
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/reports", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"report": "<report>",
"from": "2026-11-02",
"to": "2026-11-02",
"language": "en",
"formats": [
"pdf"
]
}),
});
console.log(r.status, await r.text());Response 201
{
"items": [
{
"id": 1,
"site_id": 1,
"report": "<report>",
"title": "Weekly summary",
"period": "1h",
"language": "en",
"format": "pdf",
"bytes": 12,
"created": "2026-11-02T08:00:00Z",
"created_by": "<created_by>",
"schedule_id": 1,
"download": "<download>"
}
]
}Download A Report File
GET/reports/files/{id}
Who can call it: An account with a role (viewer or admin) on the site. Token scopes: read, export.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | The file |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/reports/files/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/reports/files/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/reports/files/1", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Delete A Report File
DELETE/reports/files/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/reports/files/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/reports/files/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/reports/files/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());The Site's Scheduled Reports
GET/sites/{id}/report-schedules
Who can call it: An account that is an admin of the site. Token scopes: read.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/sites/1/report-schedules" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/sites/1/report-schedules",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/report-schedules", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"items": [
{
"id": 1,
"site_id": 1,
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": true,
"next_run": "2026-11-02T08:00:00Z",
"last_run": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}
]
}Schedule A Report (E-Mailed To The Chosen Contacts In Their Language)
POST/sites/{id}/report-schedules
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
reportrequired | string | |
periodrequired | enum | daily: the previous day; weekly (Mondays): the previous Monday to Sunday; monthly (the 1st): the previous month daily, weekly, monthly |
hour | integer | local hour of sending |
recipients | integer[] | the site's contact ids (those with an e-mail address receive it, in their language) |
formats | enum[] | |
active | boolean |
Responses
| Status | Meaning |
|---|---|
201 | Created |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/sites/1/report-schedules" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"report":"<report>","period":"daily","hour":7,"recipients":[12],"formats":["pdf"],"active":true}'import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/sites/1/report-schedules",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/sites/1/report-schedules", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": true
}),
});
console.log(r.status, await r.text());Response 201
{
"id": 1,
"site_id": 1,
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": true,
"next_run": "2026-11-02T08:00:00Z",
"last_run": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}Change A Schedule (The Next Due Time Follows)
PATCH/report-schedules/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Body
| Field | Type | Description |
|---|---|---|
report | string | |
period | enum | daily: the previous day; weekly (Mondays): the previous Monday to Sunday; monthly (the 1st): the previous month daily, weekly, monthly |
hour | integer | local hour of sending |
recipients | integer[] | the site's contact ids (those with an e-mail address receive it, in their language) |
formats | enum[] | |
active | boolean |
Responses
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad request |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
413 | Body too large |
415 | Not JSON |
422 | Invalid body |
Request
curl -s -X PATCH "https://<BOX_HOST>/api/v1/report-schedules/1" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"report":"<report>","period":"daily","hour":7,"recipients":[12],"formats":["pdf"],"active":true}'import requests
r = requests.patch(
"https://<BOX_HOST>/api/v1/report-schedules/1",
headers={"Authorization": "Bearer <API_KEY>"},
json={
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": True
},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/report-schedules/1", {
method: "PATCH",
headers: { "Authorization": "Bearer <API_KEY>", "Content-Type": "application/json" },
body: JSON.stringify({
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": true
}),
});
console.log(r.status, await r.text());Response 200
{
"id": 1,
"site_id": 1,
"report": "<report>",
"period": "daily",
"hour": 7,
"recipients": [
12
],
"formats": [
"pdf"
],
"active": true,
"next_run": "2026-11-02T08:00:00Z",
"last_run": "2026-11-02T08:00:00Z",
"last_error": "<last_error>"
}Remove A Schedule
DELETE/report-schedules/{id}
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
204 | Deleted |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X DELETE "https://<BOX_HOST>/api/v1/report-schedules/1" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.delete(
"https://<BOX_HOST>/api/v1/report-schedules/1",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/report-schedules/1", {
method: "DELETE",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Send It Now For The Latest Completed Period
POST/report-schedules/{id}/run
Who can call it: An account that is an admin of the site. Token scopes: write.
Parameters
| Field | Type | Description |
|---|---|---|
idrequired path | integer |
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
503 | The analysis engine is not answering (engine_unavailable); retry after Retry-After |
Request
curl -s -X POST "https://<BOX_HOST>/api/v1/report-schedules/1/run" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.post(
"https://<BOX_HOST>/api/v1/report-schedules/1/run",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/report-schedules/1/run", {
method: "POST",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"files": [
12
],
"queued": 12
}System
Whether the dashboard is up.
Whether The Dashboard Is Up
GET/health
Who can call it: No authentication.
Responses
| Status | Meaning |
|---|---|
200 | OK |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/health"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/health",
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/health", {
method: "GET",
});
console.log(r.status, await r.text());Response 200
{
"status": "ok"
}The Dashboard's TLS Certificate As PEM (box.pem), For A Client To Verify The Connection
GET/system/certificate
The box's certificate as a PEM file (box.pem), the certificates only, never the key; the X-Certificate-SHA256 header carries its fingerprint. The certificate is public, so any API account's token with read can fetch it. Compare the fingerprint with the one an admin reads on the dashboard before your client trusts the file. Answers 404 with the code certificate_not_available when the dashboard is reached through a proxy that holds the certificate.
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | The certificate chain, certificates only (never the key); the fingerprint is also in X-Certificate-SHA256 |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
404 | Not found (also when the account has no role on the site) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/system/certificate" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/system/certificate",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/system/certificate", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Whether The Dashboard Has A Certificate To Download, Its SHA-256 Fingerprint And The Names It Covers
GET/system/certificate/info
Whether the box has a certificate to hand out, where it comes from, its SHA-256 fingerprint (the figure to compare with what your client sees) and the names it covers: the host names and addresses your client may connect by.
Who can call it: Any API account. Token scopes: read.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Not authenticated: the token is missing, wrong, revoked or expired (token_invalid) |
403 | Forbidden: the account has no role for this, lacks a scope (scope_required), or the call is refused to API tokens (api_account_refused) |
Request
curl -s -X GET "https://<BOX_HOST>/api/v1/system/certificate/info" \
-H "Authorization: Bearer <API_KEY>"import requests
r = requests.get(
"https://<BOX_HOST>/api/v1/system/certificate/info",
headers={"Authorization": "Bearer <API_KEY>"},
)
print(r.status_code, r.text)const r = await fetch("https://<BOX_HOST>/api/v1/system/certificate/info", {
method: "GET",
headers: { "Authorization": "Bearer <API_KEY>" },
});
console.log(r.status, await r.text());Response 200
{
"available": true,
"filename": "box.pem",
"source": "self-signed",
"sha256": "3A:1F:00:7C:AA:42:9B:10:5E:61:D3:0C:88:24:F7:93:2B:6E:C1:58:04:9A:7D:E2:13:B5:66:0F:4C:D8:A1:39",
"certificates": 1,
"names": [
"<BOX_HOST>",
"<BOX_HOST>.local",
"localhost",
"::1",
"<BOX_LAN_ADDRESS>"
]
}