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 is stable, but parse columns by header name rather than fixed position. 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.
Demo organisations
Section titled “Demo organisations”A demo organisation gets the same API, with an additional 50 requests per day on top of the per-minute limit. That is enough to prove an integration end to end — list your queues, pull some tickets, take one CSV export — and deliberately not enough to run one in production. Exceeding it returns a 429 whose body identifies the cause, rather than the usual bare rate-limit response:
{ "error": "demo_quota_exceeded", "limit": 50, "resetAt": "2026-01-02T09:00:00.000Z"}Demo CSV exports are also marked, so an exported file cannot be mistaken for production data: the filename is prefixed demo-, the response carries X-Jonot-Demo: 1, and a trailing demo column is appended to the CSV. Paid exports are unchanged — no extra column, no extra header. Subscribing to a paid plan lifts the daily quota and drops the markings.
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.