Query API
Run JenQL (SQL) queries and insight queries against your project's data through the API.
The query API runs a query against your project's data and returns the results. It is the best way to get data out of My Journey Analytics programmatically: you can select exactly the columns you need, filter, aggregate and join.
Queries are written in JenQL, our SQL dialect. Write and test a query in the app's SQL editor first, then send the same text through the API.
Requests need a personal API key with the query:read scope.
Run a query
POST /api/projects/<project ID>/query/curl -X POST https://app.myjourneyanalytics.com/api/projects/12345/query/ \
-H "Authorization: Bearer <personal API key>" \
-H "Content-Type: application/json" \
-d '{
"query": {
"kind": "JenQLQuery",
"query": "SELECT event, count() AS total FROM events WHERE timestamp > now() - INTERVAL 7 DAY GROUP BY event ORDER BY total DESC LIMIT 10"
},
"name": "weekly event counts"
}'const res = await fetch("https://app.myjourneyanalytics.com/api/projects/12345/query/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MJ_PERSONAL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: {
kind: "JenQLQuery",
query: "SELECT event, count() AS total FROM events WHERE timestamp > now() - INTERVAL 7 DAY GROUP BY event ORDER BY total DESC LIMIT 10",
},
name: "weekly event counts",
}),
});
const { columns, results } = await res.json();The kind value marks the request as a JenQL query; copy it exactly as shown. The optional name labels the query so you can recognise it later.
The response
{
"columns": ["event", "total"],
"results": [
["$pageview", 18234],
["$autocapture", 9120]
],
"hasMore": false
}Each row in results is a list of values in the same order as columns. types gives each column's data type, and hasMore is true when the LIMIT cut the results short.
What you can query
The main tables are:
| Table | Contains |
|---|---|
events | Every event: event, timestamp, distinct_id, properties and the event's person |
persons | Every person: id, created_at, properties |
sessions | One row per session, with its duration, entry and exit pages |
Read properties with a dot: properties.$current_url on events, or person.properties.email for the person behind an event. The SQL editor lists every table and column available to your project.
SELECT person.properties.email AS email, count() AS orders
FROM events
WHERE event = 'order completed' AND timestamp > now() - INTERVAL 30 DAY
GROUP BY email
ORDER BY orders DESC
LIMIT 20Pass values safely
Do not build queries by pasting user input into the text. Use placeholders in curly brackets and pass the values in values:
{
"query": {
"kind": "JenQLQuery",
"query": "SELECT count() FROM events WHERE event = {event_name} AND distinct_id = {user}",
"values": { "event_name": "order completed", "user": "user_123" }
}
}How many rows you get
A query without a LIMIT returns at most 100 rows. Add your own LIMIT to get more, up to 50,000 rows per query.
To read more than that, run the query in pages ordered by time, using the last timestamp of each page as the start of the next. See timestamp-based pagination.
Fresh or cached results
Results are cached for a short while. Set refresh in the request body to choose what you get:
refresh | Behaviour |
|---|---|
blocking | The default. Use very recent cached results if there are any; otherwise run and wait |
force_blocking | Always run the query and wait |
async | Use very recent cached results, or start the query in the background and return straight away |
force_async | Always start the query in the background |
force_cache | Return cached results only; never run the query |
Long-running queries
For a query that may take a while, run it in the background:
curl -X POST https://app.myjourneyanalytics.com/api/projects/12345/query/ \
-H "Authorization: Bearer <personal API key>" \
-H "Content-Type: application/json" \
-d '{
"refresh": "async",
"client_query_id": "monthly-export-2026-10",
"query": {
"kind": "JenQLQuery",
"query": "SELECT toStartOfDay(timestamp) AS day, count() FROM events WHERE timestamp > now() - INTERVAL 90 DAY GROUP BY day ORDER BY day"
}
}'The response has a query_status with an id (your client_query_id, if you sent one). Check on it until complete is true:
GET /api/projects/<project ID>/query/<query ID>/The status response answers 202 while the query runs and 200 when it is done, with the rows in query_status.results. If the query failed, query_status.error is true and error_message says why.
To cancel a running query:
DELETE /api/projects/<project ID>/query/<query ID>/Insight queries
The same endpoint runs the queries behind insights, such as trends and funnels. For example, daily pageviews over the last 14 days:
{
"query": {
"kind": "TrendsQuery",
"series": [{ "kind": "EventsNode", "event": "$pageview", "math": "total" }],
"dateRange": { "date_from": "-14d" },
"interval": "day"
}
}To see the query behind any insight, read the insight with the insights API and copy its query.source.
Limits and errors
- If too many queries are running at once, the API answers
429. Wait, then retry. - A query with a mistake answers
400, with the problem indetail. - Queries see only the project in the path, and only data your account can see.
See also querying best practice and saved endpoints, which let your apps run a fixed query by name.
Can't find what you need? Contact support