API guide
A working guide to exporting your organization's incident data over the Antare HTTP API with a service token: what you need before you start, what to call, in what order, and the behaviors that will silently give you the wrong data if you don't know about them.
You need five things. The first three come from Antare or your organization's administrator, the fourth you create in the console, and the fifth you install.
| You need | What it looks like | Where it comes from |
|---|---|---|
| Administrator access to your organization in the Antare console | console.antare.ai, with Manage β Services in the sidebar | Your organization's administrator grants it |
| Your organization's id | A UUID, e.g. 1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30 | Supplied by Antare when your organization was set up. The console does not display it, and the name in console URLs (/o/your-org/β¦) is a slug, not the id. If you don't have it, ask your Antare account contact. |
| Your regional API host | https://api.eu-west-2.console.antare.ai (Europe) or https://api.us-east-1.console.antare.ai (US) | Supplied with the id. If unsure, section 3 shows how to tell which is yours in one request. |
| A service token with View devices and View events | st_ followed by 64 hex characters, shown once | You create it in the console β section 2 |
| Python 3.8 or newer, for the runnable client | python3 --version | No additional Python packages are required. Any language with an HTTP client works for the rest of the guide. |
Integrations authenticate with a service token. A service is an identity in your organization that belongs to software rather than a person: it has a name, the roles that decide what it may read, and one token that expires on a date you choose. You create it in the console and present its token directly to the API.
A service can do exactly what its roles allow, nothing more. The export in this guide needs two permissions: View devices (cameras, transcripts) and View events (incidents). Under Manage β Roles, add a role β "API export", say β and enable just those two. The built-in Incident manager role also carries both, but with extras the export does not need; the default Member role carries neither, and every call returns 403.
st_1a2b3c4d) so you can recognize it later. Send the token as a bearer credential on every request:
Authorization: Bearer st_1a2b3c4dβ¦That is the whole handshake. There is no login step, no token exchange and no refresh token: the service token is the only credential, and it works until its expiry or until the service is deleted. To rotate, create a new service, switch your integration to its token, then delete the old service.
Every data endpoint lives under {host}/orgs/{orgId}/β¦. The two regional hosts both exist for every customer, but only one holds your organization: the wrong host answers 404 {"message": "not found"} for your id, the right one answers your data. Put your three values in environment variables once, then a single request proves token, id and host together.
# macOS / Linux (bash or zsh) β replace the values with yours
export ANTARE_TOKEN=st_β¦
export ANTARE_ORG_ID=1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30
export ANTARE_API_HOST=https://api.eu-west-2.console.antare.ai
curl -sS -w '\nHTTP %{http_code}\n' \
-H "Authorization: Bearer $ANTARE_TOKEN" \
"$ANTARE_API_HOST/orgs/$ANTARE_ORG_ID/cameras?limit=1"# Windows (PowerShell) β curl.exe is the real curl, not the PowerShell alias
$env:ANTARE_TOKEN = "st_β¦"
$env:ANTARE_ORG_ID = "1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30"
$env:ANTARE_API_HOST = "https://api.eu-west-2.console.antare.ai"
curl.exe -sS -w "`nHTTP %{http_code}`n" `
-H "Authorization: Bearer $env:ANTARE_TOKEN" `
"$env:ANTARE_API_HOST/orgs/$env:ANTARE_ORG_ID/cameras?limit=1"The last line printed tells you where you stand:
HTTP 200 with {"data":[β¦],"meta":β¦} above it β token, id and host are all right
HTTP 404 {"message":"not found"} β wrong host for this org (try the other), or wrong id
HTTP 401 {"error":{"status":"unauthenticated"}} β token wrong or expired
HTTP 403 "caller does not have permissionβ¦" β the service's role lacks View devices An incident is a detected event β a panic activation, an aggressive exchange, an unusual scene. It has a startedAt and an endedAt. This guide defines "the day's events" as the incidents that started on that UTC day. An incident that began at 23:58 and ran past midnight belongs to the day it started; one that began at 00:01 belongs to the next day. If you need everything that overlapped a day, widen the filter to endedAt > day start and startedAt < day end instead.
POST{host}/orgs/{orgId}/incidents/query
{
"limit": 100,
"sort": [{ "startedAt": "desc" }, { "id": "asc" }],
"filter": {
"startedAt": { "gt": "2026-09-27T23:59:59Z", "lt": "2026-09-29T00:00:00Z" }
}
}Each incident carries its timing, a numeric severity, a type and source, the camera(s) it came from, and any button presses that belong to it:
{
"id": "a1b2c3d4-β¦",
"startedAt": "2026-09-28T12:53:42Z",
"endedAt": "2026-09-28T12:55:11Z",
"severity": 1,
"incidentType": "neutral",
"source": "uploadAnalysis",
"title": { "data": [{ "text": "Investigation of strange kitchen odor" }] },
"summaryShort": { "data": [{ "text": "Multiple staff members investigate a persistent damp smellβ¦" }] },
"media": [{ "id": "β¦", "camera": { "id": "c0ffee01-β¦" }, "liveCamera": null }],
"buttonPushes": [{ "cameraId": "β¦", "buttonId": "β¦", "pushedAt": "2026-09-28T12:53:40Z" }],
"labelIds": [], "categoryIds": []
}// Extracting camera ids from a day's incidents
const cameraIds = [...new Set(
incidents.flatMap(i => (i.media || []).map(m =>
(m.camera && m.camera.id) || (m.liveCamera && m.liveCamera.id)
)).filter(Boolean)
)];
// Titles are structured, not plain text
const title = incident.title?.data?.[0]?.text ?? ""; One incident by id is GET {host}/orgs/{orgId}/incidents/{id}, and GET β¦/incidents/{id}/nearby returns other incidents close to it in time and place.
Transcripts are retrieved per camera, over a time range β not per incident. One call covers a whole day. There are two views of the same speech.
GET{host}/orgs/{orgId}/cameras/{cameraId}/transcript?start=&end=
GET β¦/cameras/c0ffee01-β¦/transcript
?start=2026-09-27T23:55:00Z
&end=2026-09-29T00:05:00Z
{
"data": {
"utterances": [
{
"startedAt": "2026-09-28T10:50:52.1Z",
"endedAt": "2026-09-28T10:51:01.139Z",
"speaker": null,
"fragment": "And I guess that's",
"startMs": 39352100, // offset from the start you asked for β see below
"endMs": 39361139
}
]
}
} Utterances are returned in time order. speaker is a diarization index where the model could separate voices and null where it could not. Both bounds are required; without them the response is 200 with an empty list.
GET{host}/orgs/{orgId}/cameras/{cameraId}/metadata?start=&end=
The metadata endpoint returns regions: contiguous stretches of speech each typed by tone (aggressive, negative, positive, uninteresting), with the same utterances nested under richTranscript.utterances. It is the right call for a review queue that wants the heated exchanges first. It can return "regions": [] for a day on which transcript returns speech β regions come from a separate analysis step, utterances from transcription β so use transcript for completeness and metadata for ranking.
Data for a day keeps arriving after the day ends. Body-worn cameras upload their footage when they are docked or otherwise have connectivity, and transcription and analysis run after the upload lands β so a shift that ends at 22:00 may not be fully transcribed until the cameras have docked overnight. Antare has not published a latency guarantee.
Results for a past day can also change after you first read them:
mergedIncidentIds, and the absorbed ones stop appearing. A reasonable starting policy is to export day D on the morning of D+1, after the fleet has docked, and to re-export the previous two or three days each run, replacing each day wholesale rather than upserting rows. That lookback is a starting point, not a guarantee: uploads can arrive later than that, and operators can merge or correct incidents at any time, so expect to re-export older days by hand when you learn of a late upload or a correction.
Incidents have a stable id, but an utterance does not: its boundaries can change when a segment is superseded, so a key built from camera id, startedAt and endedAt would leave the superseded row behind. Replacing the whole day avoids that. Rerunning a day is safe on the API side β every call here is a read.
Putting it together: the incidents that started on the day, the presses that belong to them, and every utterance from every camera that day. Transcripts are pulled for all cameras, not just those that appear in an incident β most of a day's speech happens outside any incident. The runnable client in section 9 makes the same choices.
const HOST = process.env.ANTARE_API_HOST; // https://api.eu-west-2.console.antare.ai
const ORG = process.env.ANTARE_ORG_ID; // your organization's UUID
const TOKEN = process.env.ANTARE_TOKEN; // st_β¦, from Manage β Services
const day = "2026-09-28"; // a UTC day
const API = `${HOST}/orgs/${ORG}`;
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
// fetch() does not throw on 401/403/404/500, and an error body has no `data`,
// so an unchecked `r.data?.utterances ?? []` would turn a failure into "no
// speech". Check the status and the shape on every response.
async function getJson(url, init) {
const res = await fetch(url, init);
const text = await res.text();
if (!res.ok) throw new Error(`${res.status} from ${url}: ${text.slice(0, 300)}`);
const body = JSON.parse(text);
if (!("data" in body)) throw new Error(`unexpected response shape from ${url}`);
return body;
}
// Incident filters use gt/lt (exclusive), so bracket the day from one second
// outside it and trim client-side. Transcript windows are inclusive and must
// contain the whole utterance, so pad those by five minutes and trim likewise.
const shift = (d, secs) => new Date(Date.parse(d + "T00:00:00Z") + secs * 1000)
.toISOString().replace(/\.\d+/, "");
const lo = shift(day, -1), hi = shift(day, 86400);
const tLo = shift(day, -300), tHi = shift(day, 86400 + 300);
// List endpoints return at most 100 rows per call; meta.pagination.total is
// always present and drives the loop. An empty page before the total is
// reached is an error, not the end: never return a partial day silently.
async function fetchAll(url, body) {
const rows = [];
for (let offset = 0; ; ) {
const r = await getJson(url, {
method: "POST", headers: H,
body: JSON.stringify({ ...body, limit: 100, offset })
});
const total = r.meta?.pagination?.total;
if (total === undefined) throw new Error(`no meta.pagination.total from ${url}`);
rows.push(...r.data);
offset += r.data.length;
if (offset >= total) return rows;
if (!r.data.length) throw new Error(`${url}: got ${offset} of ${total} rows, then an empty page`);
}
}
// 1 β incidents that started on the day (note gt/lt, not gte/lte)
const incidents = (await fetchAll(`${API}/incidents/query`, {
sort: [{ startedAt: "desc" }, { id: "asc" }],
filter: { startedAt: { gt: lo, lt: hi } }
})).filter(i => i.startedAt.startsWith(day));
// 2 β the presses that belong to those incidents
const presses = incidents.flatMap(i =>
(i.buttonPushes || []).map(p => ({ incidentId: i.id, ...p })));
// 3 β every utterance from every camera, assigned to the day by startedAt
// An empty deletedAt filter includes cameras since removed; without it they are left out
const cameras = await fetchAll(`${API}/cameras/query`, { filter: { deletedAt: {} }, sort: [{ id: "asc" }] });
const transcripts = {};
for (const cam of cameras) {
const url = `${API}/cameras/${cam.id}/transcript?start=${tLo}&end=${tHi}`;
const r = await getJson(url, { headers: H });
if (!Array.isArray(r.data.utterances)) throw new Error(`no utterances array from ${url}`);
transcripts[cam.id] = r.data.utterances.filter(u => u.startedAt.startsWith(day));
}A complete export in the Python standard library β no dependencies to install. It reads the token, organization id and host from the environment, runs unattended, and writes one CSV per data set into the directory you run it from.
antare_export.py.# macOS / Linux (bash or zsh)
export ANTARE_TOKEN=st_β¦
export ANTARE_ORG_ID=1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30
export ANTARE_API_HOST=https://api.eu-west-2.console.antare.ai
python3 antare_export.py 2026-09-28
# Windows (PowerShell)
$env:ANTARE_TOKEN = "st_β¦"
$env:ANTARE_ORG_ID = "1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30"
$env:ANTARE_API_HOST = "https://api.eu-west-2.console.antare.ai"
py antare_export.py 2026-09-28 The date is a UTC day; with no date it exports today so far. Progress goes to the terminal and three files appear in the directory you ran the command from. They are written as temporary files first: your existing CSVs are left untouched until every request and every temporary write has succeeded. Each file is then replaced individually; an interruption during that final step β a crash or a disk error between two renames β can still leave a mixed set, and a leftover .csv.tmp is the sign that a run did not finish. Running the same date again replaces all three, which is what you want after a re-export (section 7):
1f0c2a8e-7b3d-4e5f-9a6c-2d8e4b7f1a30 β 2026-09-28 (UTC)
incidents 3 rows incidents-2026-09-28.csv
presses 0 rows presses-2026-09-28.csv
transcripts 862 rows transcripts-2026-09-28.csvThe transcript file is one row per utterance, with the camera's id as well as its name so rows stay joinable if a camera is renamed:
| camera_id | camera | started_at | ended_at | speaker | text |
|---|---|---|---|---|---|
| c0ffee01-β¦ | AB2C-DEF-GHJ | 2026-09-28T10:50:52.1Z | 2026-09-28T10:51:01.139Z | And I guess that's |
#!/usr/bin/env python3
"""Export one UTC day of incidents, their button presses and all transcripts to CSV.
Standard library only (Python 3.8+). Reads a service token from the environment and
writes three CSVs into the current working directory, replacing them only once every
request has succeeded.
ANTARE_TOKEN=st_... ANTARE_ORG_ID=... ANTARE_API_HOST=https://api.eu-west-2.console.antare.ai \
python3 antare_export.py 2026-09-28
"""
import csv
import datetime as dt
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
TOKEN = os.environ.get("ANTARE_TOKEN", "")
ORG_ID = os.environ.get("ANTARE_ORG_ID", "")
HOST = os.environ.get("ANTARE_API_HOST", "https://api.eu-west-2.console.antare.ai").rstrip("/")
PAGE = 100 # server-side cap on list endpoints
TIMEOUT = 60 # seconds per request; transcripts for a long day can be large
PAD = dt.timedelta(minutes=5) # transcript windows must contain whole utterances
# --- HTTP -----------------------------------------------------------------
def die(msg):
print("error: " + msg, file=sys.stderr)
sys.exit(1)
def request(method, url, params=None, body=None, attempt=0):
"""One call. Retries 429 (honoring Retry-After) and 5xx with backoff; everything
else stops the run with a message that says what to fix."""
full = url + ("?" + urllib.parse.urlencode(params) if params else "")
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(full, data=data, method=method, headers={
"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
return json.load(r)
except urllib.error.HTTPError as e:
detail = e.read()[:300].decode(errors="replace")
if e.code == 401:
die("401 unauthenticated β the service token was rejected. It may have\n"
" expired or the service may have been deleted; create a new\n"
" service under Manage β Services in the console.")
if e.code == 403:
if "cannot access endpoint" in detail:
die("403 β this endpoint is not open to service tokens:\n " + full)
die("403 forbidden β the service's roles lack permission for\n"
" %s\n Give the service a role with View devices and "
"View events." % full)
if e.code == 404:
die("404 not found β wrong org id, or the wrong regional host for it: " + full)
if e.code in (429, 500, 502, 503, 504) and attempt < 5:
wait = 2 ** attempt # 1, 2, 4, 8, 16 s
if e.code == 429 and e.headers.get("Retry-After", "").isdigit():
wait = max(wait, int(e.headers["Retry-After"]))
print(" %d from server; retrying in %ds" % (e.code, wait), file=sys.stderr)
time.sleep(wait)
return request(method, url, params, body, attempt + 1)
die("%d from %s\n %s" % (e.code, full, detail))
except (urllib.error.URLError, TimeoutError, OSError) as e:
if attempt < 5:
wait = 2 ** attempt
print(" could not reach %s (%s); retrying in %ds" % (HOST, e, wait), file=sys.stderr)
time.sleep(wait)
return request(method, url, params, body, attempt + 1)
die("could not reach %s after several attempts: %s\n"
" Check the host, your network and any proxy." % (HOST, e))
def paginate(url, body=None):
"""Walk an offset-paginated collection, 100 rows at a time.
A full page is not the end of the data and the response never says it
truncated; meta.pagination.total is always present and drives the loop.
If it were missing, or a page came back empty early, stop loudly rather than
export a partial day."""
rows, offset = [], 0
while True:
if body is None:
r = request("GET", url, params={"offset": offset, "limit": PAGE})
else:
r = request("POST", url, body=dict(body, limit=PAGE, offset=offset))
page = r.get("data") or []
rows += page
total = (r.get("meta") or {}).get("pagination", {}).get("total")
if total is None:
die("response from %s had no meta.pagination.total; refusing to guess "
"whether the export is complete" % url)
offset += len(page)
if offset >= total:
return rows
if not page:
die("%s returned an empty page after %d of %d rows; refusing to export a "
"partial day" % (url, offset, total))
# --- day boundaries -------------------------------------------------------
def iso(t):
return t.strftime("%Y-%m-%dT%H:%M:%SZ")
def day_bounds(day):
"""(midnight, next midnight) of a UTC day as aware datetimes."""
d = dt.date.fromisoformat(day)
start = dt.datetime.combine(d, dt.time(), dt.timezone.utc)
return start, start + dt.timedelta(days=1)
def on_day(rows, field, day):
return [r for r in rows if (r.get(field) or "").startswith(day)]
# --- export ---------------------------------------------------------------
def main():
if not TOKEN.startswith("st_"):
die("set ANTARE_TOKEN to a service token from Manage β Services in the console")
if not ORG_ID:
die("set ANTARE_ORG_ID to your organization's id (a UUID supplied by Antare)")
day = sys.argv[1] if len(sys.argv) > 1 else str(dt.datetime.now(dt.timezone.utc).date())
start, end = day_bounds(day)
base = "%s/orgs/%s" % (HOST, ORG_ID)
print("%s β %s (UTC)" % (ORG_ID, day), file=sys.stderr)
staged = []
def write(name, header, rows):
"""Write to a temporary file. publish() renames them all at the end, so a
request failure leaves the previous export intact. The renames themselves
happen one file at a time and are not atomic as a set."""
path = "%s-%s.csv" % (name, day)
with open(path + ".tmp", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(header)
w.writerows(rows)
staged.append((path + ".tmp", path, len(rows)))
def publish():
for tmp, path, n in staged:
os.replace(tmp, path)
print(" %-12s %5d rows %s" % (path.split("-")[0], n, path), file=sys.stderr)
# 1 β incidents that started on the day. gt/lt are exclusive, so the bounds
# sit one second outside the day and rows are trimmed to it.
incidents = on_day(paginate(base + "/incidents/query", {
"sort": [{"startedAt": "desc"}, {"id": "asc"}],
"filter": {"startedAt": {"gt": iso(start - dt.timedelta(seconds=1)), "lt": iso(end)}},
}), "startedAt", day)
write("incidents",
["id", "started_at", "ended_at", "severity", "type", "title", "camera_ids"],
[[i["id"], i.get("startedAt"), i.get("endedAt"), i.get("severity"),
i.get("incidentType"), text_of(i.get("title")), " ".join(camera_ids(i))]
for i in incidents])
# 2 β the button presses that belong to those incidents (not every physical press)
write("presses", ["incident_id", "camera_id", "button_id", "pushed_at"],
[[i["id"], p.get("cameraId"), p.get("buttonId"), p.get("pushedAt")]
for i in incidents for p in (i.get("buttonPushes") or [])])
# 3 β every utterance from every camera, including cameras since deleted.
# The window is inclusive and must contain the whole utterance, so it is
# padded, and utterances are assigned to the day by startedAt.
cameras = paginate(base + "/cameras/query", {"filter": {"deletedAt": {}}, "sort": [{"id": "asc"}]})
rows = []
for cam in cameras:
data = request("GET", "%s/cameras/%s/transcript" % (base, cam["id"]),
params={"start": iso(start - PAD), "end": iso(end + PAD)})["data"]
for u in on_day(data.get("utterances") or [], "startedAt", day):
rows.append([cam["id"], cam.get("name"), u["startedAt"], u["endedAt"],
u.get("speaker"), (u.get("fragment") or "").replace("\n", " ").strip()])
write("transcripts",
["camera_id", "camera", "started_at", "ended_at", "speaker", "text"], rows)
publish()
def text_of(rich):
"""title and summaryShort are {"data": [{"text": ...}]}, not strings."""
return "".join(part.get("text") or "" for part in (rich or {}).get("data") or [])
def camera_ids(incident):
"""A camera is nested under media[]; either key may be the populated one."""
out = []
for m in incident.get("media") or []:
cam = m.get("camera") or m.get("liveCamera")
if cam and cam.get("id"):
out.append(cam["id"])
return sorted(set(out))
if __name__ == "__main__":
main() List endpoints return at most 100 rows per call, whatever limit you ask for β this is the API's configured page cap, and a response of exactly 100 rows looks no different from a complete one. Page with offset. The response's meta.pagination block is part of every list response and total is the count across all pages; drive the loop from it.
A page parameter is not supported; passing one is silently ignored and you will re-read page zero forever.
Give every paged query a sort that ends in a unique field such as id. Without one the server applies no order, so successive pages can repeat some rows and skip others while the running count still reaches total.
{
"data": [ β¦ ],
"meta": {
"pagination": { "total": 27, "currentPage": 2, "totalPages": 3 }
}
}Comparison operators on a filter field:
| Operator | Status | Notes |
|---|---|---|
| eq | Works | Exact match |
| ne | Works | Negated exact match |
| gt / lt | Works | Use these for date ranges; both exclusive |
| gte / lte | Ignored | Silently dropped β see Traps to avoid |
| Method | Path (under {host}/orgs/{orgId}) | Returns | Permission |
|---|---|---|---|
| GET | /cameras | Current camera fleet; removed cameras are left out (paged) | View devices |
| POST | /cameras/query | Camera fleet with a filter and sort in the body (paged like /incidents/query); filter deletedAt: {} to include removed cameras | View devices |
| GET | /cameras/{id} | One camera | View devices |
| POST | /incidents/query | Detected events, filtered and sorted | View events |
| GET | /incidents/{id} | One incident | View events |
| GET | /incidents/{id}/nearby | Incidents close in time and place | View events |
| GET | /cameras/{id}/transcript?start=&end= | Every utterance inside the window | View devices |
| GET | /cameras/{id}/metadata?start=&end= | Speech regions typed by tone | View devices |
| GET | /cameras/{id}/timeline?start=&end= | Motion and people-count samples | View devices |
| GET | /cameras/{id}/activity?start=&end= | Activity samples | View devices |
| GET | /cameras/{id}/playlist.m3u8 | Recorded footage playlist | View devices |
Not open to service tokens, and answering 403 "identity service cannot access endpoint β¦": /me, /cameraEventLogs, footage upload, live streaming, thumbnails and preview frames, reprocessing, and device control.
| Type | Use |
|---|---|
| aggressive | Raised or hostile exchange β usually the highest-value segments |
| negative | Adverse tone short of aggression |
| positive | Cooperative exchange |
| uninteresting | No raised voices. Still frequently substantive β rank it down, don't drop it |
| Status | Means | What to do |
|---|---|---|
| 401 | {"error":{"status":"unauthenticated"}} β token missing, wrong, expired, or the service was deleted | Not retryable. Create a new service in the console and switch to its token |
| 403 | "identity service cannot access endpoint β¦" | The endpoint is user-only. Use one from the table above |
| 403 | "caller does not have permission to perform action β¦" | Not retryable. Give the service a role with View devices and View events |
| 404 | {"message": "not found"} β wrong org id, or the wrong regional host for it | Check the host first, then the id |
| 429 | Too many requests. The configured limit is 150 per minute per endpoint for each service | Wait Retry-After seconds and retry; double the wait on repeats |
| 5xx | Transient server fault | Retry a few times with the same backoff, then fail loudly |
| 200 | β¦with a filter you thought you applied | Unrecognized filters are dropped silently β check the timestamps you got back |
Behavior described here is current as of 2 October 2026. The set of endpoints open to service tokens, the gte/lte behavior and the page cap may change β check back before relying on any of them staying as described.
We use cookies to make our site work, understand how it is used, and improve your experience. Necessary cookies are always on. You can accept or reject the rest, or choose what to allow.