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

# Eyk data apps setup

> Build your own app on Eyk data. Load the SDK, let people sign in with Eyk, and run queries against your organization's data. Written for coding agents and developers.

Guide version 1.0 · SDK v1 · Updated 2026-09-07

An Eyk data app is your own web app (a supplier portal, an operations screen, an internal dashboard) that reads live data from Eyk. Eyk handles sign-in, authorization, and querying. You build the interface.

## For coding agents

You have this guide and an **App ID** (`app_...`). Do these three things, in order:

1. **Load the SDK** with one script tag carrying the App ID. Wait for `window.eykSdk`.
2. **Add sign-in**: call `getSession()` on page load; show a sign-in button that calls `signIn()` when the session is `null`.
3. **Run queries** with `query()` once a session exists. Discover member names with `listMetrics()`.

Rules that prevent the common failures:

* Use the App ID exactly as given. Never invent, shorten, or hardcode a different one.
* Never call `signIn()` automatically on page load. Only call it from a user action (a button click).
* Never run `query()` before `getSession()` returned a session. A signed-out query rejects with `AUTH_REQUIRED`; it does not redirect.
* Never call the Eyk API directly. The SDK is the whole contract; there are no other endpoints to integrate.
* Never add a backend, proxy, API key, or secret for Eyk. The SDK runs in the browser and holds the user's own session.
* Use member names from `listMetrics()` or from this guide. Do not guess names.

You are done when: the page loads without console errors, a signed-out visitor sees a sign-in button, clicking it goes to Eyk and returns to the same page signed in, and a query renders rows. See [Verification](#verification).

## Before you start

Someone with the **Admin** role in the Eyk organization must do this in Eyk under **Agentic** › **Data apps**. Agents cannot do it.

1. **Register a data app.** Each app gets a name and an App ID (`app_...`). The App ID is public and safe to put in frontend code.
2. **Add allowed origins.** An origin is `scheme://host[:port]` with no path. Sign-in only returns users to a registered origin. `https://` is required except for `http://localhost:<port>`, so add your local development origin too.
3. **Invite the people who will use the app** as members of the organization. Only approved members can sign in to a data app. What they see is governed by their access in Eyk, the same as in the Eyk dashboard.

## Step 1: load the SDK

Add one script tag to the `<head>` of every page that uses Eyk. Replace the App ID with yours.

```html theme={null}
<script async src="https://edge.eykdata.com/sdk/v1/eyk-sdk.js"
        data-app-id="app_YOUR_APP_ID"></script>
```

The SDK sets `window.eykSdk` and dispatches an `eyksdk:ready` event on `window`. Because the script is `async`, the event can fire before your code runs. Always check the global first:

```js theme={null}
function eykReady() {
  if (window.eykSdk) return Promise.resolve(window.eykSdk);
  return new Promise((resolve) =>
    window.addEventListener("eyksdk:ready", () => resolve(window.eykSdk), { once: true })
  );
}

const eyk = await eykReady();
```

<AccordionGroup>
  <Accordion title="Load from JavaScript instead of a script tag">
    Same result, useful in bundled apps where you do not control `index.html`:

    ```js theme={null}
    function loadEykSdk(appId) {
      if (window.eykSdk) return Promise.resolve(window.eykSdk);
      return new Promise((resolve, reject) => {
        window.addEventListener("eyksdk:ready", () => resolve(window.eykSdk), { once: true });
        const script = document.createElement("script");
        script.async = true;
        script.src = "https://edge.eykdata.com/sdk/v1/eyk-sdk.js";
        script.dataset.appId = appId; // renders as data-app-id
        script.onerror = () => reject(new Error("Could not load the Eyk SDK"));
        document.head.appendChild(script);
      });
    }
    ```

    `eykSdk.init({ appId })` also exists for a script loaded without `data-app-id`. Calling `init` with a different App ID clears the current session.
  </Accordion>

  <Accordion title="TypeScript types">
    Type declarations are served next to the script at
    `https://edge.eykdata.com/sdk/v1/eyk-sdk.d.ts`. Download the file into your project and import its types, or copy the [SDK reference](#sdk-reference) below. The file declares `window.eykSdk` and the `eyksdk:ready` event.
  </Accordion>

  <Accordion title="Test environment">
    An App ID belongs to one Eyk environment. Apps registered on the Eyk test platform load the SDK from `https://edge.eykdata.dev/sdk/v1/eyk-sdk.js` instead. The **Manual setup** section of the app's detail page in Eyk always shows the exact script tag for that app.
  </Accordion>
</AccordionGroup>

Do not load the script twice. A second embed is ignored and keeps the live session. Do not bundle or self-host the script: the hosted URL is the contract and receives fixes.

## Step 2: add sign-in

Sign-in is a full-page redirect to Eyk and back to your page. The SDK completes the exchange when the page reloads, inside `getSession()`.

```js theme={null}
const eyk = await eykReady();

// Completes a pending sign-in redirect, then returns the session or null.
const session = await eyk.getSession();

if (!session) {
  showSignInButton(() => {
    // Only from a user action. Never resolves: the page navigates away.
    eyk.signIn({ returnTo: window.location.href });
  });
} else {
  renderApp(session.user); // { email, name }
  showSignOutButton(async () => {
    await eyk.signOut();
    window.location.reload();
  });
}
```

Behavior to rely on:

* `getSession()` resolves `null` when nobody is signed in. It never redirects.
* `signIn()` defaults `returnTo` to the current URL. The origin of `returnTo` must be an allowed origin of the app, or Eyk shows an error page instead of a login.
* After returning, the SDK removes the `code` and `state` parameters from the URL. Your own query parameters survive the round trip.
* A session survives page reloads for about 7 days without a new redirect. Then the next `getSession()` returns `null` and the user signs in again.
* Signing out in one tab signs out the other tabs of the same app.
* Sign-in needs a secure context: `https://`, or `http://localhost`. Inside a cross-origin iframe the redirect flow cannot run.

<Warning>
  Do not redirect to sign-in automatically when `getSession()` returns `null`. A failed or cancelled sign-in then loops back into the redirect, and visitors who are not Eyk members get bounced to a login they cannot complete. Render a button.
</Warning>

<Accordion title="React example">
  The same flow as a hook. `sdk` is available in both signed-out and signed-in states, so the sign-in button can call it.

  ```tsx theme={null}
  import { useEffect, useState } from "react";

  type State =
    | { status: "loading" }
    | { status: "error"; message: string }
    | { status: "signed-out"; sdk: EykSdk }
    | { status: "signed-in"; sdk: EykSdk; user: EykUser };

  export function useEyk() {
    const [state, setState] = useState<State>({ status: "loading" });

    useEffect(() => {
      eykReady()
        .then(async (sdk) => {
          const session = await sdk.getSession();
          setState(session ? { status: "signed-in", sdk, user: session.user } : { status: "signed-out", sdk });
        })
        .catch((e) => setState({ status: "error", message: String(e) }));
    }, []);

    return state;
  }

  export function App() {
    const eyk = useEyk();
    if (eyk.status === "loading") return <p>Connecting to Eyk…</p>;
    if (eyk.status === "error") return <p>{eyk.message}</p>;
    if (eyk.status === "signed-out") {
      return <button onClick={() => eyk.sdk.signIn()}>Sign in with Eyk</button>;
    }
    return <Dashboard sdk={eyk.sdk} user={eyk.user} />;
  }
  ```
</Accordion>

## Step 3: run queries

Ask for measures and dimensions by name. Results come back already filtered to what the signed-in user may see.

```js theme={null}
const { rows } = await eyk.query({
  measures: ["fact_sales_items.orders", "fact_sales_items.net_sales"],
  time_dimensions: [
    { dimension: "fact_sales_items.line_timestamp", granularity: "day", dateRange: "last 30 days" }
  ],
  order: { "fact_sales_items.line_timestamp": "asc" },
});
```

Each row is an object keyed by member name. A time dimension queried with a granularity appears twice: as the base name and as `name.<granularity>` (the start of the bucket). Both are ISO timestamps.

```json theme={null}
[
  {
    "fact_sales_items.line_timestamp.day": "2026-09-01T00:00:00.000",
    "fact_sales_items.line_timestamp": "2026-09-01T00:00:00.000",
    "fact_sales_items.orders": "42",
    "fact_sales_items.net_sales": "3120.5"
  }
]
```

Measure values can arrive as strings. Coerce them with `Number()` before you add or format them.

### Query shape

| Field             | Type                                        | Notes                                                                                                                                                                                                                                                                                                                      |
| ----------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `measures`        | `string[]`                                  | Aggregates such as `fact_sales_items.net_sales`. A query needs at least one measure or dimension.                                                                                                                                                                                                                          |
| `dimensions`      | `string[]`                                  | Group-by fields such as `dim_product_variants.combined_brand`.                                                                                                                                                                                                                                                             |
| `time_dimensions` | `{ dimension, granularity?, dateRange? }[]` | `dimension` is the base name, never with a `.day` suffix. `granularity` is one of `second`, `minute`, `hour`, `day`, `week`, `month`, `quarter`, `year`. `dateRange` is a relative range like `"last 30 days"` or an inclusive `["2026-01-01", "2026-01-31"]` pair. Omit `granularity` to filter by date without grouping. |
| `filters`         | `{ member, operator, values? }[]`           | Operators: `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `endsWith`, `gt`, `gte`, `lt`, `lte`, `set`, `notSet`, `inDateRange`, `notInDateRange`, `beforeDate`, `afterDate`. `values` are always strings.                                                                                                 |
| `order`           | `{ [member]: "asc" \| "desc" }`             | Sort by any member in the query.                                                                                                                                                                                                                                                                                           |
| `limit`           | `number`                                    | 1 to 10000.                                                                                                                                                                                                                                                                                                                |

### Discover member names

`listMetrics()` returns the catalog the signed-in user may query. Use it to find names and to build pickers; do not guess names.

```js theme={null}
const { metrics } = await eyk.listMetrics();
// metrics[i] = { name, title, description, measures: [...], dimensions: [...], joins: [...] }
// each measure/dimension = { name, title, type, description, format }
// format is "currency", "percentage", or null; dimensions also carry default_time_dimension
```

The main metrics:

| Metric              | Use it for                                                                                                                                                                                                |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fact_sales_items`  | Sales, orders, margins, products, customers, countries. Time dimension `fact_sales_items.line_timestamp`. Joins `dim_product_variants` and `dim_customers`.                                               |
| `fact_attributions` | Marketing-attributed sales: channel cost, channel revenue, return on ad spend, per source and campaign. Time dimension `fact_attributions.combined_timestamp`. Does not support product-level dimensions. |
| `fact_sessions`     | Website sessions and traffic.                                                                                                                                                                             |
| `fact_daily_ads`    | Daily ad platform spend and performance.                                                                                                                                                                  |

### Query rules

* **Do not mix measures from different fact metrics in one query.** `fact_sales_items.net_sales` with `fact_attributions.channel_cost` is invalid. Run two queries and combine in your app.
* **Time dimension names are base names.** Wrong: `{ dimension: "fact_sales_items.line_timestamp.month" }`. Right: `{ dimension: "fact_sales_items.line_timestamp", granularity: "month" }`. The suffixed form only appears in result keys.
* **`fact_attributions` uses one attribution model per query.** The default model is applied when you add no filter. To pick another, filter on `dim_attribution_models.display_name` with one value, for example `"Last Touch"`.
* **Members you cannot see read as unknown.** A hidden or restricted member fails with `INVALID_QUERY` and reason `unknown_member`, never with a permission error.
* **Keep queries aggregated.** Pull grouped rows for the period you display, not every order. `limit` caps at 10000 rows.

More examples:

<CodeGroup>
  ```js Sales per brand theme={null}
  const { rows } = await eyk.query({
    measures: ["fact_sales_items.orders", "fact_sales_items.net_sales"],
    dimensions: ["dim_product_variants.combined_brand"],
    time_dimensions: [{ dimension: "fact_sales_items.line_timestamp", dateRange: "last 12 months" }],
    order: { "fact_sales_items.net_sales": "desc" },
    limit: 50,
  });
  ```

  ```js Filtered by brand, per week theme={null}
  const { rows } = await eyk.query({
    measures: ["fact_sales_items.net_sales", "fact_sales_items.net_quantity"],
    time_dimensions: [
      { dimension: "fact_sales_items.line_timestamp", granularity: "week", dateRange: ["2026-06-01", "2026-08-31"] }
    ],
    filters: [{ member: "dim_product_variants.combined_brand", operator: "equals", values: ["ACME"] }],
    order: { "fact_sales_items.line_timestamp": "asc" },
  });
  ```

  ```js Recent orders (row level) theme={null}
  const { rows } = await eyk.query({
    measures: ["fact_sales_items.net_sales", "fact_sales_items.net_quantity"],
    dimensions: [
      "fact_sales_items.order_id",
      "fact_sales_items.order_status",
      "fact_sales_items.line_timestamp",
      "fact_sales_items.shipping_country_code"
    ],
    time_dimensions: [{ dimension: "fact_sales_items.line_timestamp", dateRange: "last 14 days" }],
    order: { "fact_sales_items.line_timestamp": "desc" },
    limit: 500,
  });
  ```

  ```js Marketing channels theme={null}
  const { rows } = await eyk.query({
    measures: ["fact_attributions.channel_cost", "fact_attributions.net_sales", "fact_attributions.orders"],
    dimensions: ["fact_attributions.combined_source"],
    time_dimensions: [
      { dimension: "fact_attributions.combined_timestamp", granularity: "month", dateRange: "last 6 months" }
    ],
    order: { "fact_attributions.combined_timestamp": "asc" },
  });
  ```
</CodeGroup>

## Errors

Every rejected promise from `getSession()`, `signIn()`, `signOut()`, `query()`, and `listMetrics()` is an `EykError` with `name: "EykError"`, a `code`, a `message`, and optional `details` and HTTP `status`. Wrong use of the SDK itself (no App ID, wrong argument type) throws a `TypeError` synchronously instead.

| Code            | Meaning                                                                                                           | What to do                                                                                |
| --------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `AUTH_REQUIRED` | Nobody is signed in, or the session expired.                                                                      | Show the sign-in button. Do not retry the query.                                          |
| `FORBIDDEN`     | The user is signed in but is not an approved member of the app's organization, or the page origin is not allowed. | Tell the user to ask an Eyk admin for access. Check the app's allowed origins.            |
| `INVALID_QUERY` | The query names an unknown member or breaks a rule. `details` is `[{ member, reason }]`.                          | Fix the named member. Reasons include `unknown_member` and `attribution_filter_required`. |
| `RATE_LIMITED`  | Too many requests for this user.                                                                                  | Back off and retry later. Cache results in your app.                                      |
| `NETWORK_ERROR` | The request never got a response, or timed out after 30 seconds.                                                  | Retry with backoff; show an offline state.                                                |
| `SERVER_ERROR`  | Eyk or the analytics engine failed.                                                                               | Retry later; surface a generic error.                                                     |

```js theme={null}
try {
  const { rows } = await eyk.query(q);
} catch (e) {
  if (e.name === "EykError" && e.code === "AUTH_REQUIRED") return showSignInButton();
  if (e.name === "EykError" && e.code === "INVALID_QUERY") console.error(e.details); // [{ member, reason }]
  showError(e.message);
}
```

## Verification

Run through this list before you call the integration done.

1. **Load**: open the page. `window.eykSdk.version` is a string in the console. No SDK errors.
2. **Signed out**: the page shows a sign-in button and does not redirect on its own.
3. **Sign in**: clicking the button goes to Eyk. After signing in, the browser returns to the page you started on, with clean URL parameters, and `getSession()` returns `{ user: { email, name } }`.
4. **Query**: a query returns `{ rows }` with the members you asked for. Numbers render correctly after `Number()`.
5. **Sign out**: after `signOut()`, `getSession()` returns `null` and the sign-in button is back.
6. **Local development**: the same works on `http://localhost:<port>` after that origin is added to the app's allowed origins.

## Common mistakes

| Mistake                                                                  | Fix                                                                                                    |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Sign-in shows "return\_to is not a registered origin for this data app." | Add the page's origin (`scheme://host[:port]`, no path) to the app's allowed origins in Eyk.           |
| The App ID is put in an `.env` file and read on the server.              | The App ID is public and only used in the browser. Put it in the script tag.                           |
| A backend route proxies queries to Eyk with a stored token.              | Not needed and not supported. The SDK queries directly with the user's own session.                    |
| `query()` runs on page load and fails with `AUTH_REQUIRED`.              | Await `getSession()` first and only query when it returns a session.                                   |
| The time dimension is passed as `fact_sales_items.line_timestamp.day`.   | Pass the base name with `granularity: "day"`.                                                          |
| Measures from `fact_sales_items` and `fact_attributions` in one query.   | Split into two queries.                                                                                |
| `signIn()` is awaited and code after it never runs.                      | Expected: the page navigates away. Put post-sign-in logic in the `getSession()` path on the next load. |
| The script is bundled into the app or copied to the project.             | Load it from `https://edge.eykdata.com/sdk/v1/eyk-sdk.js`.                                             |

## SDK reference

```ts theme={null}
interface EykUser {
  email: string;
  name: string | null;
}

interface EykSdk {
  readonly version: string;
  /** Programmatic alternative to data-app-id; a different appId clears the session. */
  init(options: { appId: string }): EykSdk;
  /** Current session, or null. Completes a pending sign-in redirect and cleans the URL. */
  getSession(): Promise<{ user: EykUser } | null>;
  /** Full-page redirect to Eyk sign-in. Never resolves. Call it from a user action. */
  signIn(options?: { returnTo?: string }): Promise<never>;
  signOut(): Promise<void>;
  query(query: EykQuery): Promise<{ rows: Record<string, string | number | boolean | null>[] }>;
  /** The metadata catalog the signed-in user may see. */
  listMetrics(): Promise<{ metrics: EykMetric[] }>;
}

interface EykQuery {
  measures: string[];
  dimensions?: string[];
  time_dimensions?: {
    dimension: string;
    granularity?: "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year";
    dateRange?: string | [string, string];
  }[];
  filters?: {
    member: string;
    operator:
      | "equals" | "notEquals" | "contains" | "notContains" | "startsWith" | "endsWith"
      | "gt" | "gte" | "lt" | "lte" | "set" | "notSet"
      | "inDateRange" | "notInDateRange" | "beforeDate" | "afterDate";
    values?: string[];
  }[];
  order?: Record<string, "asc" | "desc">;
  limit?: number;
}

interface EykMetric {
  name: string;
  title: string;
  description: string | null;
  measures: EykField[];
  dimensions: (EykField & { default_time_dimension: boolean })[];
  joins: { name: string; relationship: string | null }[];
}

interface EykField {
  name: string;
  title: string;
  type: string | null;
  description: string | null;
  format: "currency" | "percentage" | null;
}

interface EykError extends Error {
  name: "EykError";
  code: "AUTH_REQUIRED" | "FORBIDDEN" | "INVALID_QUERY" | "NETWORK_ERROR" | "SERVER_ERROR" | "RATE_LIMITED";
  details?: unknown; // INVALID_QUERY: [{ member, reason }]
  status?: number;   // HTTP status of the failing response, when there was one
}

declare global {
  interface Window { eykSdk?: EykSdk }
  interface WindowEventMap { "eyksdk:ready": CustomEvent<{ version: string }> }
}
```

The script URL, the `data-app-id` attribute, the method names, the query shape, and the error codes above are a frozen v1 contract. Breaking changes ship as `/sdk/v2/`; v1 embeds keep working.

<Note>
  🤝 Questions or stuck on a query? Reach the Eyk team through the in-product chat.
</Note>
