# GraphQL

> Read a site's raw events through GraphQL, with the same keys, limits and plan as the raw event API, and the schema, examples and errors.

Source: https://docs.bifrostorbit.com/insights/developers/graphql/

GraphQL reads the same events as the [raw event API](https://docs.bifrostorbit.com/insights/developers/raw-event-api/), with the same keys. It comes with the Growth plan.

| | Address |
| --- | --- |
| Production | `https://api.bifrostorbit.com/insights/graphql` |
| Staging | `https://api-stage.bifrostorbit.com/insights/graphql` |

`https://app.bifrostorbit.com/insights/api/cdn/graphql` is the same endpoint.

GraphQL only reads. Send events with the script or the [server API](https://docs.bifrostorbit.com/insights/developers/server-api/).

## Requests

Send a key that reads events, starting with `ins_live_`, as `Authorization: Bearer <key>` or `x-api-key: <key>`, and `POST` a JSON body:

| Property | Type | Means |
| --- | --- | --- |
| `query` | string | The GraphQL document. Required. |
| `variables` | object | Values for the query's variables |
| `operationName` | string | Which operation to run when the document has several |

```bash
curl 'https://api.bifrostorbit.com/insights/graphql' \
  -H 'Authorization: Bearer ins_live_…' \
  -H 'Content-Type: application/json' \
  -d '{"query":"query ($site: String!) { events(site: $site, from: \"2026-10-01\", to: \"2026-10-02\", type: \"event\") { items { name at path revenue currency } next } }","variables":{"site":"SITE_KEY"}}'
```

`GET` works too, with `query`, `variables` (as JSON text) and `operationName` in the query string. URL-encode them.

Answers are JSON with `data`, and `errors` when something went wrong:

```json
{ "data": { "events": { "items": [{ "name": "Booked", "at": "2026-10-01T09:14:03.000Z", "path": "/checkout/", "revenue": 1240, "currency": "EUR" }], "next": null } } }
```

Keys belong on your server or data tool, never in a web page, so the endpoint answers servers only.

## Queries

| Query | Arguments | Returns |
| --- | --- | --- |
| `events` | `site: String!`, `from`, `to`, `name`, `type`, `limit: Int`, `cursor` | `EventPage!`. A site's events, oldest first. The arguments work as the parameters of `GET /events`: without `from` and `to`, the last 24 hours; `limit` is 100 by default and at most 1,000. |

## Types

| Type | Fields |
| --- | --- |
| `EventPage` | `items: [Event!]!`, and `next`: pass it as `cursor`, with the same other arguments, for the next page. `null` on the last page. |
| `Event` | `id`, `type`, `name`, `at`, `host`, `path`, `referrer`, `channel`, `utm { source medium campaign term content }`, `country`, `device`, `browser`, `os`, `properties`, `revenue`, `currency`, `contentId`, `label`, `visit`, `person` |

`properties` is `JSON`, which holds the details sent with the event and cannot be selected into. No visitor is identified, as in REST. `person` is set only with [known users](https://docs.bifrostorbit.com/insights/everyone/known-users/) switched on.

## Example

Every booking in October, a page at a time:

```graphql
query Bookings($site: String!, $cursor: String) {
  events(site: $site, name: "Booked", from: "2026-10-01", to: "2026-11-01", limit: 500, cursor: $cursor) {
    items {
      at
      path
      channel
      country
      revenue
      currency
      properties
    }
    next
  }
}
```

```json
{ "site": "SITE_KEY" }
```

Ask again with `cursor` set to `next` until it is `null`.

## Errors

Errors come in `errors`, each with a `message`, a `code` under `extensions`, and the `path` and `locations` of the field that failed when there is one. The codes are the same as in REST:

```json
{
  "data": null,
  "errors": [
    {
      "message": "There is no site with that key in your organisation",
      "locations": [{ "line": 1, "column": 3 }],
      "path": ["events"],
      "extensions": { "code": "unknown_site" }
    }
  ]
}
```

| Status | When |
| --- | --- |
| `400` | No `query`, `variables` that are not a JSON object, a query that does not parse or does not match the schema (`bad_request`), one over the limits (`query_too_deep`, `query_too_complex`), a date or `type` that isn't valid (`invalid`), or a `cursor` this API didn't give out (`invalid_cursor`) |
| `401` | The key is missing, wrong or removed (`unauthorized`) |
| `403` | The key may only send events (`wrong_scope`), or your plan doesn't include the API (`not_in_plan`) |
| `404` | No site with that key in your organisation (`unknown_site`) |
| `413` | A query longer than 20,000 characters (`query_too_large`) |
| `429` | Too many requests (`rate_limited`). Wait a minute. |
| `500` | Something failed on our side (`internal`). Try again. |
| `200` | Success |

## Limits

- Each key may make 60 GraphQL requests a minute, counted apart from the raw event API's 60.
- A query may be at most 20,000 characters long, nest at most 10 levels and ask for at most 300 fields. Each alias and each use of a fragment counts.
- Introspection is on and needs no key, so GraphQL tools can read the schema from the address. Introspection fields do not count towards the limits.
- Every answer has `Cache-Control: no-store`. The newest events can take a few seconds to show up.
