Skip to content

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/
bash
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"
  }'
js
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

json
{
  "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:

TableContains
eventsEvery event: event, timestamp, distinct_id, properties and the event's person
personsEvery person: id, created_at, properties
sessionsOne 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.

sql
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 20

Pass values safely

Do not build queries by pasting user input into the text. Use placeholders in curly brackets and pass the values in values:

json
{
  "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:

refreshBehaviour
blockingThe default. Use very recent cached results if there are any; otherwise run and wait
force_blockingAlways run the query and wait
asyncUse very recent cached results, or start the query in the background and return straight away
force_asyncAlways start the query in the background
force_cacheReturn cached results only; never run the query

Long-running queries

For a query that may take a while, run it in the background:

bash
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:

json
{
  "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 in detail.
  • 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