Read-only Analytics API
The api.jonot.io/v1/* HTTP API gives read-only access to your organisation’s
ticket and queue data. Authenticate with a bearer token instead of an admin
login. Use the API to import data into a data warehouse, business intelligence
tool, or 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 customer-entered personally identifiable information (PII), such as a name, notes, or party size. They only include IDs, status, and lifecycle timestamps.
Pagination: When nextCursor is not null, pass it as cursor on the next
request. Keep the same from, to, and filter values. A null nextCursor
means you have reached the end of the range.
GET /v1/exports/tickets.csv
Section titled “GET /v1/exports/tickets.csv”This endpoint accepts the same filters as /v1/tickets: from and to are
required, and queueId, locationId, and status are repeatable. It does not
accept cursor or limit. The complete matching range streams as one CSV
response, so the 500-row page limit does not apply.
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 uses Transfer-Encoding: chunked. A 90-day export with 100,000
rows does not need to remain in memory at either end. Send it directly to a file
or parser.
90-day range cap
Section titled “90-day range cap”Every endpoint that takes from and to rejects a range over 90 days with
400. If you need a longer history, request shorter ranges, such as one request
per week. Use lifecycle timestamps such as calledAt and completedAt to
calculate wait and service times without requesting the same rows twice.
Rate limits
Section titled “Rate limits”Each token allows 60 requests per minute. The limit applies to the token, not the IP address. Every response includes:
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 has an additional limit of 50 requests per day. The
per-minute limit also applies. You can use the demo API to test a complete
integration by listing queues, requesting tickets, and creating a CSV export.
The daily limit is not enough for production use. If you exceed it, the API
returns 429 with a body that identifies the cause:
{ "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.