Read-only Analytics API
The api.jonot.io/v1/* HTTP API gives read-only access to your organisation's ticket and queue data — no admin login required, just a bearer token. Use it to pull data into a data warehouse, a BI tool, or a custom dashboard.
Getting a token
Section titled “Getting a token”- Open admin.jonot.io/settings/integrations.
- Click the API tokens tab.
- Click Create token, give it a name (e.g. "Power BI"), and confirm.
- Copy the token immediately — it is shown once and cannot be recovered. If you lose it, revoke it and create a new one.
Tokens are prefixed jot_ and never expire on their own; revoke them from the same tab when they're no longer needed.
Authentication
Section titled “Authentication”Authorization: Bearer jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Status | Meaning |
|---|---|
401 | Missing, malformed, unknown, or revoked token. |
402 | Valid token, but the organisation doesn't have the API feature enabled. |
429 | Rate limit exceeded — see Rate limits below. |
400 | Invalid query parameters, or a date range over 90 days. |
Endpoints
Section titled “Endpoints”GET /v1/queues
Section titled “GET /v1/queues”Returns your organisation's locations and queues, unfiltered and unpaginated:
curl https://api.jonot.io/v1/queues \ -H "Authorization: Bearer jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "locations": [ { "id": "loc_…", "name": "Downtown", "slug": "downtown", "queues": [ { "id": "q_…", "name": "Main Queue", "slug": "main-queue", "status": "ACTIVE" } ] } ]}Use the returned id values to filter /v1/tickets and the CSV export by queueId / locationId.
GET /v1/tickets
Section titled “GET /v1/tickets”Paginated ticket read, scoped to your organisation.
| Param | Required | Repeatable | Notes |
|---|---|---|---|
from | yes | no | ISO-8601, inclusive lower bound on createdAt. |
to | yes | no | ISO-8601, exclusive upper bound. Max 90-day span. |
queueId | no | yes | Repeat the param to filter multiple queues. |
locationId | no | yes | Repeat the param to filter multiple locations. |
status | no | yes | One of WAITING, CALLED, COMPLETED, CANCELED, SKIPPED, NO_SHOW. |
cursor | no | no | Opaque value from the previous page's nextCursor. |
limit | no | no | Default 100, max 500. |
curl "https://api.jonot.io/v1/tickets?from=2026-06-01T00:00:00Z&to=2026-06-08T00:00:00Z&status=COMPLETED" \ -H "Authorization: Bearer jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "items": [ { "id": "tkt_…", "number": 42, "queueId": "q_…", "locationId": "loc_…", "status": "COMPLETED", "createdAt": "2026-06-01T09:14:02.000Z", "calledAt": "2026-06-01T09:20:11.000Z", "completedAt": "2026-06-01T09:24:47.000Z", "cancelledAt": null, "skippedAt": null, "noShowAt": null, "calledByDeviceSessionId": "dev_…" } ], "nextCursor": "eyJjcmVhdGVkQXQi…"}Rows never include the ticket's bearer hash or any customer-entered PII (name, notes, party size) — only ids, status, and the lifecycle timestamps.
Pagination: when nextCursor is non-null, pass it as cursor on the next request to continue from where you left off (same from/to/filters). A null nextCursor means you've reached the end of the range.
GET /v1/exports/tickets.csv
Section titled “GET /v1/exports/tickets.csv”Same filters as /v1/tickets (from/to required, queueId/locationId/status repeatable) minus cursor/limit — the whole matching range streams as one CSV response, so there's no 500-row page limit to work around for a bulk pull.
Column order (stable, do not rely on header names for positional parsing changing — but the header row is always present and matches this list exactly):
id,number,queueId,locationId,status,createdAt,calledAt,completedAt,cancelledAt,skippedAt,noShowAt,calledByDeviceSessionIdcurl "https://api.jonot.io/v1/exports/tickets.csv?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z" \ -H "Authorization: Bearer jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -o tickets.csvThe response streams (Transfer-Encoding: chunked) so a 90-day/100k-row export doesn't need to buffer in memory on either end — pipe it straight to a file or a parser.
90-day range cap
Section titled “90-day range cap”Every endpoint that takes from/to rejects a span over 90 days with 400. Pull data incrementally (e.g. one call per week) if you need a longer history — the ticket lifecycle timestamps (calledAt, completedAt, …) let you reconstruct wait/service durations without re-fetching the same rows twice.
Rate limits
Section titled “Rate limits”60 requests/minute per token (not per IP — the budget travels with the token). Every response carries:
X-RateLimit-Remaining: 42X-RateLimit-Reset: 1751328000000A 429 additionally carries Retry-After (seconds). Back off and retry after that window; a scheduled sync every few minutes comfortably stays under the limit.
Python + pandas
Section titled “Python + pandas”import requestsimport pandas as pd
TOKEN = "jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"BASE = "https://api.jonot.io/v1"HEADERS = {"Authorization": f"Bearer {TOKEN}"}
def fetch_tickets(frm: str, to: str) -> pd.DataFrame: rows = [] cursor = None while True: params = {"from": frm, "to": to, "limit": 500} if cursor: params["cursor"] = cursor res = requests.get(f"{BASE}/tickets", headers=HEADERS, params=params, timeout=30) res.raise_for_status() body = res.json() rows.extend(body["items"]) cursor = body["nextCursor"] if not cursor: break return pd.DataFrame(rows)
df = fetch_tickets("2026-06-01T00:00:00Z", "2026-07-01T00:00:00Z")df["waitSeconds"] = ( pd.to_datetime(df["calledAt"]) - pd.to_datetime(df["createdAt"])).dt.total_seconds()print(df.groupby("queueId")["waitSeconds"].mean())Or read the CSV export directly — pandas handles the streaming response transparently:
df = pd.read_csv( f"{BASE}/exports/tickets.csv?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z", storage_options={"Authorization": f"Bearer {TOKEN}"},)Power BI (Web connector)
Section titled “Power BI (Web connector)”- In Power BI Desktop: Get Data → Web.
- Choose Advanced, and build the URL with your date range, e.g.
https://api.jonot.io/v1/exports/tickets.csv?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z. - Under HTTP request header parameters, add a header named
Authorizationwith the valueBearer jot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. - Click OK — Power BI detects the CSV and opens the Table Preview.
- Click Load (or Transform Data first if you want to set column types —
createdAt/calledAt/etc. import as text; convert them toDate/Timein Power Query). - Set a scheduled refresh in the Power BI service if you're pulling on a cadence; keep the range comfortably under 90 days per refresh.