> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grainql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Export and query data

> Read your events out of Grain with the Query API, from the first key to an export.

The Query API returns your events as JSON over HTTPS. Use it to export data, to build your own
dashboard, or to feed events into another tool. The dashboard at `https://grainql.com/dashboard`
covers one-time questions and live monitoring without code. This page is the task guide. The
field tables, error tables, and limits are on the reference pages, starting with
[Query API](/api-reference/query-api/overview).

Before you start, make sure that you have:

* A tenant on the Builder plan or higher. The Free plan has no Query API.
* The tenant alias from the dashboard. It is not the tenant UUID.
* An HTTP client. Analytics Web has no Query API methods.

## Get an API key

1. Open `https://grainql.com/dashboard/settings`, then **Authentication**.
2. Click **Generate New Secret**.
3. Enter a name, for example `Query API key`.
4. Select the **Query API** permission.
5. Copy the secret. The dashboard shows it one time.
6. Store the secret in an environment variable named `GRAIN_API_KEY`.

The key is independent of the Analytics Web `authStrategy`. It works when the SDK uses `NONE`,
`SERVER_SIDE`, or `JWT`. Read [Authentication](/authentication).

## Your first query

Every request sends the key in the `X-API-Key` header. This request reads ten page views from
January 2024:

```bash theme={null}
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $GRAIN_API_KEY" \
  -d '{
    "event": "page_viewed",
    "after": "2024-01-01",
    "before": "2024-01-31",
    "pagination": { "offset": 0, "size": 10 }
  }'
```

```json theme={null}
[
  {
    "eventName": "page_viewed",
    "userId": "user_123",
    "eventTs": "2024-01-15T10:30:00Z",
    "properties": {
      "page": "/home",
      "referrer": "https://google.com"
    },
    "eventDate": "2024-01-15",
    "insertId": "unique-event-id"
  }
]
```

Every event has the same six fields. `properties` holds what `track` sent. `userId` holds what
`identify` or `setUserId` set. Read [Query events](/api-reference/query-api/query-events) for
the field table and the paging rules.

To select events, add a `filterSet`. This request reads the events of one user:

```bash theme={null}
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $GRAIN_API_KEY" \
  -d '{
    "after": "2024-01-01",
    "before": "2024-01-31",
    "filterSet": [
      { "property": "userId", "comparison": "EQUALS", "value": "user_123" }
    ],
    "pagination": { "offset": 0, "size": 100 }
  }'
```

```json theme={null}
[
  {
    "eventName": "button_clicked",
    "userId": "user_123",
    "eventTs": "2024-01-15T10:31:04Z",
    "properties": {
      "button_name": "signup",
      "page": "/home"
    },
    "eventDate": "2024-01-15",
    "insertId": "unique-event-id"
  }
]
```

A filter can address `properties.<name>`, nested paths such as `properties.user.plan`,
`eventName`, `userId`, and `eventTs`. Read [Filters](/api-reference/query-api/filters) for
the ten operators.

## Count events

When you need a number and not the events, call the count endpoint. It takes the same body
without `pagination`:

```bash theme={null}
curl -X POST https://queryapis.grainql.com/v1/api/query/count/your-tenant-id \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $GRAIN_API_KEY" \
  -d '{
    "event": "page_viewed",
    "after": "2024-01-01",
    "before": "2024-01-31"
  }'
```

```json theme={null}
{ "count": 12543 }
```

Read [Count events](/api-reference/query-api/count-events).

## List event names

To find out which event names the tenant holds, call the list endpoint:

```bash theme={null}
curl https://queryapis.grainql.com/v1/api/query/events/your-tenant-id \
  -H "X-API-Key: $GRAIN_API_KEY"
```

```json theme={null}
["page_viewed", "button_clicked", "form_submitted", "purchase_completed"]
```

Read [List event names](/api-reference/query-api/list-events).

## Common tasks

| Task | Endpoint | Requests |
| - | - | - |
| Export a date range | Query, paged | One per 1000 events |
| A dashboard of totals | Count | One per number |
| A funnel check | Count | One per step |
| An alert on an error rate | Count | Two per check |
| The journey of one user | Query with a `userId` filter | One per 1000 events |

### Export a date range

Page through the query endpoint with `size` `1000`, then write the events to a file. This Node.js
script exports January 2024:

```javascript theme={null}
import { writeFileSync } from 'node:fs';

const url = 'https://queryapis.grainql.com/v1/api/query/your-tenant-id';
const headers = {
  'Content-Type': 'application/json',
  'X-API-Key': process.env.GRAIN_API_KEY,
};
const size = 1000;
const allEvents = [];

for (let offset = 0; ; offset += size) {
  const response = await fetch(url, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      after: '2024-01-01',
      before: '2024-01-31',
      pagination: { offset, size },
    }),
  });
  if (!response.ok) {
    const { error } = await response.json();
    throw new Error(`Export failed: ${response.status} ${error}`);
  }
  const events = await response.json();
  allEvents.push(...events);
  if (events.length < size) break;
}

writeFileSync('events-2024-01.json', JSON.stringify(allEvents));
console.log(`Exported ${allEvents.length} events`);
```

```text theme={null}
Exported 12543 events
```

The file holds one array of event objects in the shape of the query response. Each page is one
request against the rate limit, so an export of 12,543 events takes 13 requests. Read
[Data export](/examples/query-api-data-export) for a full example.

### A dashboard of totals

Send one count per number, in parallel, then divide on the client:

```javascript theme={null}
const url = 'https://queryapis.grainql.com/v1/api/query/count/your-tenant-id';
const headers = {
  'Content-Type': 'application/json',
  'X-API-Key': process.env.GRAIN_API_KEY,
};
const range = { after: '2024-01-01', before: '2024-01-31' };

async function count(body) {
  const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
  const { count } = await response.json();
  return count;
}

const [total, errors, purchases] = await Promise.all([
  count(range),
  count({ ...range, event: 'error_occurred' }),
  count({ ...range, event: 'purchase_completed' }),
]);

console.log({ total, errors, purchases, errorRate: errors / total });
```

```text theme={null}
{ total: 15247, errors: 122, purchases: 318, errorRate: 0.008 }
```

Three counts are three requests. On the Builder plan, three requests take more than one minute.
Cache the numbers, and refresh them on a timer. A dashboard is only as good as the
event names behind it. Read [Tracking checklist](/guides/analytics-dashboard). For a full
example, read [Custom dashboard](/examples/query-api-custom-dashboard).

### A funnel check

Count each step of the funnel with the same date range, then compare the numbers:

```bash theme={null}
curl -X POST https://queryapis.grainql.com/v1/api/query/count/your-tenant-id \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $GRAIN_API_KEY" \
  -d '{
    "event": "page_viewed",
    "after": "2024-01-01",
    "before": "2024-01-31",
    "filterSet": [
      { "property": "properties.page", "comparison": "EQUALS", "value": "/signup" }
    ]
  }'
```

```json theme={null}
{ "count": 4210 }
```

```bash theme={null}
curl -X POST https://queryapis.grainql.com/v1/api/query/count/your-tenant-id \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $GRAIN_API_KEY" \
  -d '{
    "event": "account_created",
    "after": "2024-01-01",
    "before": "2024-01-31"
  }'
```

```json theme={null}
{ "count": 611 }
```

The conversion from the signup page to an account is 611 divided by 4210, or 14.5 percent. The
dashboard shows the same funnel without code. Read [Tracks](/features/tracks).

## Limits

The rate limit applies per API key, and the plan sets it.

| Plan | Requests per minute | Requests per day | Where |
| - | - | - | - |
| Free | Not available | Not available | every endpoint |
| Builder | 2 | 20 | every endpoint |
| Growth | 10 | 200 | every endpoint |
| Scale | 100 | 2,000 | every endpoint |

Custom plans with higher limits exist.

| Limit | Value | Where |
| - | - | - |
| Page size | 100 by default, 1000 at most | `pagination.size` in `POST /{tenantAlias}` |

Rules for the rate limit:

1. Count the requests of a task before you run it. One page and one count are one request each.
2. Queue requests in one place. Do not let several parts of an application call the API at the
   same time.
3. If the API returns `429`, wait `Retry-After` seconds, then retry one time.
4. Cache a number that a dashboard shows often.

Read [Query API](/api-reference/query-api/overview) for the rate limit headers and the error
table.

## Keep the key on the server

<Warning>
  Do not put the API key in browser code. Anyone who reads the page source then holds the key.
</Warning>

1. Store the key in an environment variable, and add the `.env` file to `.gitignore`.
2. When a browser needs data, call the Query API from your own backend route, and return the
   result to the browser.
3. To rotate a key, create a new key in the dashboard, deploy the application with the new key,
   then make sure that the application works, then revoke the old key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.