# Connect an AI tool to Eyk with MCP Source: https://docs.eykdata.com/agentic/mcp Connect Claude, ChatGPT, Cursor, Claude Code, VS Code, Gemini CLI, Perplexity, Vibe or n8n to your Eyk data through the Eyk MCP server. Connect an AI tool to Eyk through the Eyk MCP server and ask questions about your data in plain language. The tool queries the same metrics you see in Eyk, straight from your BigQuery warehouse. It only sees the data your access in Eyk allows, checked on every query. ## Before you start You need: * An Eyk account that is an approved member of your organization. * The AI tool installed, or open in your browser. * For ChatGPT, Perplexity and Vibe: a paid plan. See the note under each tool. ## Server details | Setting | Value | | - | - | | MCP server URL | `https://mcp.eykdata.com/mcp` | | Transport | Streamable HTTP | | Authentication | Sign in with your Eyk account in the browser. There are no keys to copy. | ## Claude Works in Claude on the web and in the desktop app. On Team and Enterprise plans an Owner must add the connector once via **Organization settings** >> **Connectors** before members can connect. In Claude, go to **Customize** >> **Connectors**. Click **+**, then **Add custom connector**. Name it Eyk and paste the MCP server URL, then click **Add**. ```text theme={null} https://mcp.eykdata.com/mcp ``` Click **Connect** next to Eyk and sign in with your Eyk account. See the [Claude documentation](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). ## ChatGPT Plugins are in beta and need a paid plan. Plus and Pro get read-only tools. Full tool access is currently limited to Business, Enterprise and Edu workspaces. On the web, enable Developer mode under **Settings** >> **Security and login**. On a workspace, an admin enables it first. Go to `chatgpt.com/plugins`, click **+** and name the plugin Eyk. Under Connection, paste the MCP server URL. Sign in with your Eyk account when the OAuth flow opens. ```text theme={null} https://mcp.eykdata.com/mcp ``` Enable the Eyk plugin from a chat. See the [ChatGPT documentation](https://developers.openai.com/apps-sdk/deploy/connect-chatgpt/). ## Cursor Add this block to `~/.cursor/mcp.json` for all projects, or to `.cursor/mcp.json` in the project. ```json theme={null} { "mcpServers": { "eyk": { "url": "https://mcp.eykdata.com/mcp" } } } ``` Open the **Customize** page from the Cursor sidebar. The Eyk server appears under MCPs. Sign in with your Eyk account when the browser opens. See the [Cursor documentation](https://cursor.com/docs/mcp). ## Claude Code Run this command in your project. ```bash theme={null} claude mcp add --transport http eyk https://mcp.eykdata.com/mcp ``` Run `/mcp` inside Claude Code, select Eyk, and complete the browser login with your Eyk account. See the [Claude Code documentation](https://code.claude.com/docs/en/mcp). ## VS Code Works with Copilot in agent mode. VS Code 1.102 or newer is required, because older versions cannot sign in to MCP servers that require authentication. Open the Command Palette and run **MCP: Add Server**. Choose **HTTP (HTTP or Server-Sent Events)** and paste the MCP server URL. Name the server `eyk` and pick **Workspace Settings** or **User Settings**. This is the equivalent configuration: ```json theme={null} { "servers": { "eyk": { "type": "http", "url": "https://mcp.eykdata.com/mcp" } } } ``` Run **MCP: List Servers**, select Eyk and choose **Start Server**. Confirm you trust the server, then sign in with your Eyk account when the browser opens. Open Copilot Chat and switch it to **Agent** mode to use Eyk's tools. See the [VS Code documentation](https://code.visualstudio.com/docs/agent-customization/mcp-servers). ## Gemini CLI Add this block to `~/.gemini/settings.json`, or run `gemini mcp add --transport http eyk https://mcp.eykdata.com/mcp`. ```json theme={null} { "mcpServers": { "eyk": { "httpUrl": "https://mcp.eykdata.com/mcp" } } } ``` Restart the CLI and run `/mcp` to confirm Eyk is listed. Sign in with your Eyk account in the browser popup on first connection. See the [Gemini CLI documentation](https://geminicli.com/docs/tools/mcp-server/). ## Perplexity Custom connectors require a paid plan. In organizations, an admin must allow members to add them. Go to **Account settings** >> **Connectors** and click **+ Custom connector**. In the pop-up, select **Remote**. Fill in the name Eyk and the MCP server URL. Set Authentication to **OAuth** and Transport to **Streamable HTTP**. ```text theme={null} https://mcp.eykdata.com/mcp ``` Check the risk acknowledgement box and click **Add**. Click the Eyk connector card and sign in with your Eyk account. See the [Perplexity documentation](https://www.perplexity.ai/help-center/en/articles/13915507-adding-custom-remote-connectors). ## Vibe Vibe is Mistral's assistant, formerly Le Chat. Custom connectors are an administrator-only feature. On Free, Pro and Student plans the account owner is the administrator. In **Work** mode, open the **Connectors** page. Click **+ Add Connector** and switch to the **Custom MCP Connector** tab. Enter a connector name without spaces and the MCP server URL, then click **Connect**. ```text theme={null} https://mcp.eykdata.com/mcp ``` Complete the OAuth sign-in with your Eyk account. In a chat, open **Tools** and make sure Eyk is enabled. See the [Vibe documentation](https://docs.mistral.ai/vibe/work/connectors/mcp-connectors). ## n8n n8n 1.104 or newer is required for the HTTP Streamable transport. Add an **MCP Client Tool** node to give an AI Agent Eyk's tools. To call the tools as workflow steps instead, add the **MCP Client** node. Set the endpoint to the MCP server URL and pick the **HTTP Streamable** transport. ```text theme={null} https://mcp.eykdata.com/mcp ``` Set Authentication to **OAuth2**, create a credential pointing at Eyk and complete the sign-in. You can reuse the credential across workflows. See the [n8n documentation](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp). ## Any other client Point your MCP client at the MCP server URL over streamable HTTP. ```text theme={null} https://mcp.eykdata.com/mcp ``` Sign in with your Eyk account when prompted. Tools and prompts are discovered automatically. See [Connect to remote MCP servers](https://modelcontextprotocol.io/docs/develop/connect-remote-servers) in the MCP documentation. ## Check it worked Ask the tool: "Which organizations can I access in Eyk?" It answers by calling Eyk's `list_organizations` tool and lists your organizations. Then ask a data question, for example "What was my net sales last month?". ## What the tool can do | Tool | What it does | | - | - | | `list_organizations` | Lists the Eyk organizations you can access. | | `list_metrics` | Lists the measures and dimensions you may query in an organization. | | `query_data` | Runs a query and returns the data. | The tool sees the same data you see in Eyk. Your access is checked on every query. The server also provides an Eyk analytics reference that explains measure semantics and query rules, which tools read on their own. ## Troubleshooting You need an approved membership of an Eyk organization. Ask your organization admin to invite you. ChatGPT, Perplexity and Vibe keep custom connectors behind a paid plan or an admin setting. See the note under the tool. Update to VS Code 1.102 or newer. Update to n8n 1.104 or newer. # Google Ads Source: https://docs.eykdata.com/destinations/google-ads Create conversion actions in Google Ads and connect them to Eyk so server-side conversions reach your campaigns. The Google Ads conversion destination sends server-side event data from Eyk to Google Ads, so your campaigns can optimize on it. ## Before you start You need: * Access to your Google Ads account. * Access to the **Tracking** page in Eyk. Setup has two parts. First you create three conversion actions in Google Ads, then you add the destination in Eyk. ## Part 1: Create conversion actions in Google Ads Go to your Google Ads account and click **Goals** in the left-side menu. Goals menu in Google Ads Scroll down and click **+ Create conversion action**. Create conversion action button On the next page, select **Website**. Select Website as conversion source Enter your website domain and click **Scan**. Scan website domain If you see this note, click **Use Google Ads only**. Otherwise skip this step. Use Google Ads only option Scroll down to **Create conversion actions manually using code** and click **+ Add a conversion action manually**. Add a conversion action manually Open the **Select category** drop down and select **Purchase**. Select Purchase category Click **Conversion action optimization options** to expand it. Select **Secondary action not used for bidding optimization**. Google Ads needs a certain volume of data before a conversion action can optimize bidding. Keep your existing conversion action as primary and run the Eyk action alongside it for about two weeks. Secondary action optimization option For **Conversion name** enter `Purchase (Eyk)`. Conversion name field Keep the default settings under **Value** and **Count**. Under **Value**, **Use different values for each conversion** must be selected. Under **Count**, **Every** must be selected. Value and count settings Under **Enhanced conversions**, make sure **Use enhanced conversions for this conversion action** is selected. Then click **Done** in the bottom left corner. Enhanced conversions setting Click **+ Add a conversion action manually**. For the categories **Add to cart** and **Begin checkout**, repeat the steps from adding a conversion action manually to enabling enhanced conversions. Add another conversion action When **Purchase**, **Add to cart** and **Begin checkout** are all added, click **Save and continue**. Save and continue button On the next page, select **Use Google Tag Manager**. Use Google Tag Manager option Copy these values. You need them in Part 2: * Conversion ID * Conversion label: Purchase (Eyk) * Conversion label: Add to cart (Eyk) * Conversion label: Begin checkout (Eyk) Conversion ID and labels Then click **Done**. ## Part 2: Add the destination in Eyk In Eyk, go to **Tracking** and click **+ Add destination**. Add destination button in Eyk Select **Google Ads conversion** from the list of destinations. Enter the values you copied in Part 1 and click **Start verification**. Google Ads conversion verification screen After you publish this destination, pause or remove all existing Google Ads tracking. Otherwise conversions are tracked twice. # Google Analytics 4 Source: https://docs.eykdata.com/destinations/google-analytics-4 Connect your Google Analytics 4 property to Eyk so it receives client-side and server-side events. The Google Analytics 4 destination sends client-side and server-side event data from Eyk to Google Analytics. ## Before you start You need: * Access to your Google Analytics 4 property. * Access to the **Tracking** page in Eyk. Setup has two parts. First you copy the Measurement ID of your data stream, then you enter it in Eyk. ## Part 1: Get your Measurement ID from Google Analytics Open the GA4 property you want to connect to Eyk. Click **Admin** in the bottom left corner. Admin menu in Google Analytics 4 In the section **Data collection and modification**, click **Data streams**. Data streams section Click the stream. If you have no stream yet, click **Add stream** and create a stream of type **WEB**. Select a data stream Copy the **Measurement ID**. You need it in Part 2. Measurement ID field ## Part 2: Add the destination in Eyk In Eyk, go to **Tracking** and click **+ Add destination**. Add destination button in Eyk Select **Google Analytics 4** from the list of destinations. Enter the Measurement ID from Part 1 and click **Start verification**. Google Analytics 4 verification screen After you publish this destination, pause or remove all existing Google Analytics 4 tracking. Otherwise conversions are tracked twice. # Meta Ads Source: https://docs.eykdata.com/destinations/meta-ads Connect your Meta dataset to Eyk so Facebook and Instagram campaigns receive server-side conversion events. The Meta Ads destination sends client-side and server-side event data from Eyk to Meta. Use it to optimize campaigns on Facebook and Instagram. ## Before you start You need: * Access to Facebook Events Manager for your ads account. * Access to the **Tracking** page in Eyk. Setup has two parts. First you collect three values in Events Manager, then you enter them in Eyk. ## Part 1: Get your credentials from Meta Go to [Facebook Events Manager](https://business.facebook.com/events_manager2) for your ads account. In the **Data sources** list, click the data source you want to connect. Then open the **Settings** tab. Settings tab in Events Manager On the **Settings** tab, copy the **Dataset ID**. You need it in Part 2. Dataset ID field On the **Settings** tab, scroll down to the **Conversion API** section and click **Generate access token**. Copy the access token. You need it in Part 2. Generate access token button Go to the **Test events** tab. Open the section **Confirm that your server's events are set up correctly**. Click **Copy** to copy the test event code. You need it in Part 2. Test event code ## Part 2: Add the destination in Eyk In Eyk, go to **Tracking** and click **+ Add destination**. Add destination button in Eyk Select **Meta Ads** from the list of destinations. Enter the **Dataset ID**, **API access token** and **Test ID** from Part 1 and click **Start verification**. Meta Ads verification screen After you publish this destination, pause or remove all existing Facebook tracking. Otherwise conversions are tracked twice. # OpenAI Ads Source: https://docs.eykdata.com/destinations/openai Send conversion events from Eyk to OpenAI Ads and check that they arrive in OpenAI Ads Manager. The OpenAI Ads destination is only available on Eyk's new tracking setup, which is currently in beta. Ask your Eyk contact person for access. The OpenAI Ads destination sends client-side and server-side event data from Eyk to OpenAI Ads. Use it to measure and optimize campaigns that run in ChatGPT. ## Before you start You need: * Access to the [OpenAI Ads Manager](https://ads.openai.com/) for your account. * The beta tracking setup enabled in your Eyk environment. Setup has two parts. First you collect a pixel ID and create a conversion key in OpenAI Ads Manager. Then you enter both in Eyk. The pixel ID identifies the data source that receives your events. ## Part 1: Get your credentials from OpenAI Go to [OpenAI Ads Manager](https://ads.openai.com/) and open **Tools** >> **Conversions**. On the **Data Source** tab, check whether a data source already exists. If it does, open it, copy its **ID** and skip to the step "Create a conversion key". If the tab reads **You have not set up data sources yet**, continue with the next step. Conversion events and campaigns are bound to the data source they were created for and cannot switch. Reuse your existing data source to keep campaigns and their optimization intact. Click **+ Create** in the top-right corner and select **Data Source**. Create menu with the Data Source option in OpenAI Ads Manager For **Data Source Name** enter `Eyk Pixel` and click **Create**. Create new data source dialog The **Set up your data source** screen opens. Copy the **Pixel ID**. You need it in Part 2. Ignore the setup code and close the screen with the **X** in the top-right corner. You do not need to install the setup code on your website. Eyk's tracking sends the events for you. Set up your data source screen with the Pixel ID field Back on the **Conversions** page, click the key icon next to **+ Create** in the top-right corner. Conversions page with the key icon next to the Create button In the **Conversion keys** panel, click **+ Create new key**. Conversion keys panel with the Create new key button For **Name** enter `Eyk` and click **Create**. Create new conversion key dialog On the **Save your key** screen, click **Copy** and store the key somewhere safe. You need it in Part 2. OpenAI shows this key only once. If you lose it, create a new key. Save your key screen ## Part 2: Add the destination in Eyk In Eyk, go to **Tracking** and click **+ Add destination**. Select **OpenAI Ads** from the list of destinations. Enter the **Pixel ID** and the **Conversions API key** from Part 1, then click **Start verification**. After you publish this destination, pause or remove all existing OpenAI Ads tracking. Otherwise conversions are tracked twice. ## Check it worked In OpenAI Ads Manager, go to **Tools** >> **Conversions** and open the **Event Stream** tab. Events from Eyk appear here shortly after visitors trigger them on your website. If you created a new data source, click **+ Create** >> **Conversion Event** once events appear and select that data source. Campaigns can only optimize on a conversion event. # TikTok Ads Source: https://docs.eykdata.com/destinations/tiktok-ads Connect your TikTok pixel to Eyk so TikTok Ads receives client-side and server-side conversion events. The TikTok Ads destination sends client-side and server-side event data from Eyk to TikTok. ## Before you start You need: * Access to TikTok Business Center for your ads account. * Access to the **Tracking** page in Eyk. Setup has two parts. First you get a Pixel ID and an access token from TikTok, then you add the destination in Eyk. ## Part 1: Get your pixel details from TikTok Go to [TikTok Business Center](https://business.tiktok.com/manage/assets/pixels) and open **Assets** >> **Pixels**. Then create a new pixel or use an existing one. Click **Add a pixel**. Add a pixel in TikTok Business Center Select **Manual setup**. Select **TikTok Pixel + Events API**. Give your pixel a name, like `Eyk - Pixel`. Click **Next**. Select **Enable Automatic Advanced Matching (AAM)** and **Allow automatic event setup**. Leave the other toggles enabled. Click **Next**. Click **Next**. Click **Next**. In the **Choose a template** drop-down, select **E-commerce**. Click **Next**. Click **Generate access token** and copy the access token. You need it in Part 2. Click **Next**. Click **Finish**. On the next screen, copy the **Pixel ID**. You need it in Part 2. Pixel ID after creating a new pixel Click the pixel in the list, then click **Open in Events Manager**. Copy the **Pixel ID**. You need it in Part 2. Pixel ID for an existing pixel Open the **Settings** tab and scroll down to **Data connection**. In the **Events API** section, click **Generate access token**. Copy the access token. You need it in Part 2. Generate access token for an existing pixel Scroll down to **Privacy and safety**. Make sure **Enable Automatic Advanced Matching (AAM)** and **Allow automatic event setup** are toggled ON. ## Part 2: Add the destination in Eyk In Eyk, go to **Tracking** and click **+ Add destination**. Add destination button in Eyk Select **TikTok Ads** from the list of destinations. Enter the **Pixel ID** and **Access token** from Part 1. Select an **Item ID source**. It determines which product identifier Eyk reports to TikTok. If you have an active product feed to TikTok, use the same identifier type as that feed. Click **Start verification**. TikTok Ads verification screen After you publish this destination, pause or remove all existing TikTok tracking. Otherwise conversions are tracked twice. # Data apps Source: https://docs.eykdata.com/eyk-data-apps-setup Build a web app on Eyk data: load the SDK, let people sign in with Eyk, and run queries. An Eyk data app is your own web app that reads live data from Eyk. Examples are a supplier portal, an operations screen and an internal dashboard. 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` or when `getSession()` rejects with `AUTH_REQUIRED`. 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, such as 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 errors, a signed-out visitor sees a sign-in button, sign-in returns to the same page, and a query renders rows. The full checklist is in [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:`, 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 `` of every page that uses Eyk. Replace the App ID with yours. ```html theme={null} ``` 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(); ``` This gives the same result. It is 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. 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. 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(); let session = null; try { // Completes a pending sign-in redirect, then returns the session or null. session = await eyk.getSession(); } catch (e) { // A cancelled or failed sign-in rejects with AUTH_REQUIRED. Treat it as signed out. if (e.name !== "EykError" || e.code !== "AUTH_REQUIRED") throw e; showMessage(e.message); } 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. * If the visitor cancels the sign-in or the provider rejects it, `getSession()` rejects with `AUTH_REQUIRED` and the provider's message. Treat it as signed out and show the sign-in button with the message. * `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`, `state`, `error` and `error_description` 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. Do not redirect to sign-in automatically when `getSession()` returns `null`. A failed or cancelled sign-in then loops back into the redirect. Visitors who are not Eyk members get bounced to a login they cannot complete. Render a button. 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. A cancelled sign-in lands in the signed-out state with a message, not in the error state. ```tsx theme={null} import { useEffect, useState } from "react"; type State = | { status: "loading" } | { status: "error"; message: string } | { status: "signed-out"; sdk: EykSdk; message?: string } | { status: "signed-in"; sdk: EykSdk; user: EykUser }; export function useEyk() { const [state, setState] = useState({ status: "loading" }); useEffect(() => { eykReady() .then(async (sdk) => { try { const session = await sdk.getSession(); setState(session ? { status: "signed-in", sdk, user: session.user } : { status: "signed-out", sdk }); } catch (e) { const err = e as EykError; if (err.name === "EykError" && err.code === "AUTH_REQUIRED") { setState({ status: "signed-out", sdk, message: err.message }); } else { setState({ status: "error", message: String(e) }); } } }) .catch((e) => setState({ status: "error", message: String(e) })); }, []); return state; } export function App() { const eyk = useEyk(); if (eyk.status === "loading") return

Connecting to Eyk…

; if (eyk.status === "error") return

{eyk.message}

; if (eyk.status === "signed-out") { return ( <> {eyk.message &&

{eyk.message}

} ); } return ; } ```
## 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.`, which is 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: ```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" }, }); ``` ## 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, such as no App ID or a 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:` 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. | | A cancelled sign-in shows a dead-end error page. | Treat an `AUTH_REQUIRED` rejection from `getSession()` as signed out and show the sign-in button. | | 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; signOut(): Promise; query(query: EykQuery): Promise<{ rows: Record[] }>; /** 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; 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/`. Embeds on v1 keep working. # Attribution models Source: https://docs.eykdata.com/guides/attribution-models Learn what attribution models are and how each one, from first touch to U-shaped, credits your marketing activity for a purchase. Attribution is the practice of giving credit for a purchase to the marketing activity that led to it. You can attribute a purchase to any channel, campaign or ad you run. Attribution shows which activities produce a return, so you can decide where to invest next. An attribution model is the set of rules that decides how credit for a purchase gets split across the measured touchpoints. Models are often expressed in monetary value. If a purchase is worth EUR 100, the model spreads that value across the measured touchpoints according to its own rules. The right model depends on your business model, your strategy and the questions you are trying to answer. ## First touch and last touch First touch attribution gives all credit for a purchase to the first touchpoint measured. Last touch attribution gives all credit to the last touchpoint before the purchase. Neither model splits credit across touchpoints, but both show which channels create initial awareness and which channels close the sale. ## Linear Linear attribution spreads the credit for a purchase evenly across all measured touchpoints. If a customer clicked five ads before buying, each ad gets 20% of the credit. Linear attribution shows your full marketing mix and which touchpoints move customers through the funnel toward a purchase. ## Time decay Time decay attribution gives more credit to touchpoints closer in time to the purchase. It gives less credit to touchpoints further away. This model assumes that touchpoints later in the funnel have more impact on the purchase, while still crediting earlier touchpoints. Time decay helps you understand and optimize the different phases of the customer journey. ## U-shaped U-shaped attribution gives most of the credit to the first and last touchpoints measured before a purchase. It spreads the remaining credit evenly across the touchpoints in between. Use this model when you have a clear picture of your typical customer journey. It optimizes for both top-of-funnel input and bottom-of-funnel conversion. # Overview Source: https://docs.eykdata.com/guides/custom-data Deliver orders, customers, product variants and refunds from your own backend to Eyk through the Data API or BigQuery hook tables. Custom data is the order, customer, product variant and refund data you deliver to Eyk from your own backend. You use it when your shop does not run on a platform Eyk connects to directly. It reaches Eyk through one of two transports: the [Data API](/guides/custom-data/data-api) or [BigQuery hook tables](/guides/custom-data/bigquery-hook-tables). Both carry the same objects and follow the same rules. One example runs through these pages. Noor & Vos, a fictional home goods shop, sends order `NV-10432`. It holds two vases at EUR 20.00 and one tray at EUR 50.00, a EUR 10.00 coupon and EUR 5.00 shipping, all excluding 21% VAT. The customer is `CUST-8842`, the variants are `VASE-BLU` and `TRAY-OAK`. ## Which transport | | Data API | BigQuery hook tables | | - | - | - | | You send | JSON over HTTPS, up to 100 objects per request | SQL `INSERT` from your own BigQuery project | | Fits when | Your backend can call an API when a record changes | Your data already lives in BigQuery | | Only here | Cost corrections from a second system, such as an ERP | Rate cards for shipping and payment cost | ## Rules that apply to both ### Amounts exclude tax Every amount excludes tax, and `taxes` is the tax on top. When your system stores prices including tax, divide by 1 plus the rate before you send. Noor & Vos stores the vase at EUR 24.20 and sends `unit_price` 20.00. The order fields `subtotal`, `discounts` and `taxes` are the truth for the order. When the lines do not add up to them, Eyk keeps the lines and books the difference on an adjustment line, an extra order line without a product. Send discounts as positive numbers. Refund `amount` is the goods refunded and excludes shipping. ### Ids must match exactly Eyk links records only through the ids you send, compared as strings, including case. An order line's `variant_id` must equal a product variant's `id`. An order's `customer_id` must equal a customer's `id`. Eyk tells new from returning customers by that id. The customer's `email` is optional and adds the link to the tracking pixel and the customer profile. A refund's `order_id` must equal an order's `id`. The purchase event of the Eyk tracking pixel links to the order when its `transaction_id` equals the order's `id` or `order_number`. A link that does not match fails silently: the order still counts, but without the product, the customer or the attribution. ### Only products are items An order item is a line for a product variant in your catalog. Shipping goes in `shipping`, coupons and other discounts in `discounts`. A fee, a service, insurance or a gift is an item with a variant row of its own. An extra parcel is the same order, never a second one. An item whose `variant_id` matches no variant keeps its revenue in every total but has no product in product reports. ### Send a record again to update it Both transports are append-only. Per `id`, the version with the latest `updated_at` wins, so a re-sent record must carry a later `updated_at` to replace the old one. There is no delete. To reverse an order, send a refund. Eyk does not filter on `payment_status`, so leave out orders that never became a sale. Eyk processes everything once a night, and data shows up in reports the next morning. ### Costs make profit Profit is net sales minus cost of goods, shipping cost and payment cost. Cost of goods comes from `unit_cost` on the line, or from `fallback_unit_cost` on the variant. Shipping and payment cost come from `shipping_cost` and `payment_cost` on the order. Without those, Eyk uses a rate card per method in the hook tables, or the fallback in your Eyk settings. A second system that knows the real costs, such as an ERP, corrects them through order updates in the Data API. # BigQuery hook tables Source: https://docs.eykdata.com/guides/custom-data/bigquery-hook-tables Load orders, customers, product variants and refunds into the hook tables of your Eyk BigQuery project with a scheduled query. Load your orders, customers, product variants and refunds into the hook tables with a scheduled query that runs every night. A hook table is an append-only table in your Eyk BigQuery project that Eyk reads in its night run. You write rows into it with a normal `INSERT`. ## Before you start You need: * The [Custom data](/guides/custom-data) page, read once. * A BigQuery project that holds your source data and can run scheduled queries. * The Custom BigQuery source added in Eyk under **Sources**, and write access to the hook tables. Ask your Eyk contact person if you need help with this step. Setup has two parts. First you confirm the tables and shape one entity, then you schedule the inserts. ## Tables | Setting | Value | | - | - | | Dataset | `._import`. Your Eyk contact person gives you the project and dataset name. | | Tables | `eyk_ingest_orders`, `eyk_ingest_customers`, `eyk_ingest_product_variants`, `eyk_ingest_refunds`, `eyk_ingest_shipping_methods`, `eyk_ingest_payment_methods` | | Location | The location of your Eyk project, `EU` unless agreed otherwise. A query in another location cannot insert into it. | | Schedule | Once a day at 00:00 UTC. Eyk processes the tables every night. | | Organization id | Not needed. The dataset is yours. | | Duplicates | Allowed. Per `id` the row with the latest `updated_at` wins. | Open the `_import` dataset in the BigQuery console. All 6 tables are there, with the columns listed under [Columns](#columns). Run a `SELECT` on one of them to confirm you have access. Write a `SELECT` over your catalog that returns one row per variant, with every column cast to the table type. `id` must be the same value your order lines carry in `variant_id`. Categories are arrays of segments, never a joined string. ```sql theme={null} SELECT CAST(variant_id AS STRING) AS id, CAST(sku AS STRING) AS sku, CAST(variant_name AS STRING) AS name, CAST(product_id AS STRING) AS parent_id, CAST(product_name AS STRING) AS parent_name, CAST(purchase_price_ex_vat AS NUMERIC) AS fallback_unit_cost, ARRAY(SELECT s FROM UNNEST(SPLIT(category_breadcrumb, ' > ')) AS s WHERE TRIM(s) != '') AS category_path, CAST(created AS TIMESTAMP) AS created_at, CAST(modified AS TIMESTAMP) AS updated_at FROM `your-project.catalog.variants` ``` Wrap the `SELECT` in an `INSERT` with `LIMIT 10`, run it, and read the rows back from the hook table. Check that names are readable, ids match your order lines, and arrays hold no NULL element. ```sql theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_product_variants` (id, sku, name, parent_id, parent_name, fallback_unit_cost, category_path, created_at, updated_at) SELECT ... LIMIT 10; ``` A NULL element inside `category_path`, `additional_category_paths`, `discount_codes` or `items` fails the nightly load for the whole table. Filter empty segments out before you insert. Repeat the last two steps for the other tables. Orders carry their lines in `items`, an `ARRAY`. Build it with `ARRAY_AGG(STRUCT(...))` over your order lines, grouped by order. Every amount excludes tax. Amounts that include tax inflate gross sales in every report by the tax rate. Divide by 1 plus the rate before you insert. In the BigQuery console, save each `INSERT` as a scheduled query that runs daily at 00:00 UTC over the rows changed in the last 2 days. Remove the `LIMIT`. The 2-day window catches late changes. The same row twice is harmless. ```sql theme={null} WHERE modified >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY) ``` Optional. Insert one row per shipping and payment method with what the method costs you. Profit then includes shipping and payment cost for orders that carry no cost of their own. ```sql theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_shipping_methods` (method_id, method_name, flat_cost, percentage_cost, valid_from) VALUES ('postnl-standard', 'PostNL standard', 4.35, NULL, NULL); ``` Run each `INSERT` once without the date filter, product variants and customers first, then orders, then refunds. Oldest orders first when the history is large. ## Columns Every amount excludes tax. Every timestamp is UTC. Columns marked required are `NOT NULL` in the table. ### eyk\_ingest\_orders | Column | Type | Meaning | | - | - | - | | `id` | STRING | Required. Your order id, stable and never reused. | | `payment_status` | STRING | Required. Payment state as your platform names it, for example `paid`. Stored as sent, not filtered. | | `store_view_id` | STRING | Required. A stable code for the shop, country or store view. Drives the store filter. | | `created_at` | TIMESTAMP | Required. When the order was placed. | | `updated_at` | TIMESTAMP | Required. When the order last changed. The latest version per `id` wins. | | `currency` | STRING | Required. ISO 4217 code, for example `EUR`. | | `subtotal` | NUMERIC | Required. Goods before discounts, shipping and tax. Gross sales. | | `customer_id` | STRING | Required. The `id` of the customer. | | `items` | ARRAY\ | Required. The order lines, see below. | | `order_number` | STRING | The number your customer sees, for example `#1043`. Also links to tracking. | | `fulfillment_status` | STRING | Fulfillment state as your platform names it. | | `store_view_name` | STRING | Label for `store_view_id`. Falls back to the id. | | `sales_channel` | STRING | Where the order was placed, for example `web` or `pos`. Falls back to `Default`. | | `shipping` | NUMERIC | Shipping charged to the customer. | | `shipping_tax` | NUMERIC | The part of `taxes` that is tax on shipping. | | `shipping_discount` | NUMERIC | The part of `discounts` that applies to shipping. | | `taxes` | NUMERIC | Total tax on the order, including tax on shipping. | | `discounts` | NUMERIC | Total discount on the order, including line discounts. Positive number. | | `discount_codes` | ARRAY\ | Coupon codes used. | | `customer_email` | STRING | Email of the customer at order time. Used to identify the customer before the email on the customer record. | | `billing_country_code` | STRING | ISO 3166-1 alpha-2, for example `NL`. | | `shipping_country_code` | STRING | Same format as `billing_country_code`. | | `shipping_cost` | NUMERIC | What shipping cost you, for example the carrier invoice. | | `payment_cost` | NUMERIC | What the payment provider charged you. | | `shipping_method_id` | STRING | Joins `method_id` in `eyk_ingest_shipping_methods`. | | `payment_method_id` | STRING | Joins `method_id` in `eyk_ingest_payment_methods`. | | `total` | NUMERIC | Deprecated. Leave NULL. | Each element of `items` is a `STRUCT` with these fields, in this order: | Field | Type | Meaning | | - | - | - | | `id` | STRING | Required. Your line id, unique within the order. | | `quantity` | INT64 | Required. Units ordered, more than 0. | | `unit_price` | NUMERIC | Required. Price per unit before discounts. | | `unit_cost` | NUMERIC | Cost of goods per unit. Falls back to `fallback_unit_cost` on the variant. | | `discounts` | NUMERIC | Discount on this line, all units together. Positive number. | | `taxes` | NUMERIC | Tax on this line, all units together. | | `variant_id` | STRING | The `id` of the product variant. Without a match the line has no product in product reports. | | `sku` | STRING | Your SKU. | | `product_id` | STRING | Ignored. Eyk takes the parent product from `parent_id` on the variant. | ### eyk\_ingest\_customers | Column | Type | Meaning | | - | - | - | | `id` | STRING | Required. Your customer id, matches `customer_id` on orders. | | `email` | STRING | Links the customer to the tracking pixel and the customer profile. Leave NULL when you do not have it. New versus returning customers and RFM segments work on `id` alone. | | `created_at` | TIMESTAMP | Required. When the account was created. | | `updated_at` | TIMESTAMP | Required. When the record last changed. | | `first_name` | STRING | First name. | | `last_name` | STRING | Last name. | ### eyk\_ingest\_product\_variants One row per sellable variant. A product without variants is one row with no `parent_id`. | Column | Type | Meaning | | - | - | - | | `id` | STRING | Required. Your variant id, matches `variant_id` on order lines. | | `name` | STRING | Required. Variant name, for example `Blue`. | | `created_at` | TIMESTAMP | Required. When the variant was created. | | `updated_at` | TIMESTAMP | Required. When the record last changed. | | `parent_id` | STRING | Your id of the parent product. Groups variants into one product. | | `parent_name` | STRING | Name of the parent product. | | `sku` | STRING | Your SKU. | | `image_url` | STRING | Image of the variant. | | `parent_image_url` | STRING | Image of the parent product. | | `fallback_unit_cost` | NUMERIC | Cost of goods per unit when the order line carries no `unit_cost`. | | `brand_id`, `brand_name` | STRING | Brand. | | `supplier_id`, `supplier_name` | STRING | Supplier. | | `category_path` | ARRAY\ | Main category as segments from root to leaf, for example `['Home', 'Vases']`. Applies to the whole product. | | `additional_category_paths` | ARRAY\>> | Other categories, same format: `[STRUCT(['Sale'] AS path)]`. | | `store_view_id`, `store_view_name` | STRING | Deprecated. Leave NULL. | ### eyk\_ingest\_refunds | Column | Type | Meaning | | - | - | - | | `id` | STRING | Required. Your refund id. | | `order_id` | STRING | Required. The `id` of the refunded order. | | `store_view_id` | STRING | Required. Same code as on the order. | | `created_at` | TIMESTAMP | Required. When the refund was issued. | | `updated_at` | TIMESTAMP | Required. When the record last changed. | | `amount` | NUMERIC | Required. Goods refunded, excluding shipping. | | `shipping` | NUMERIC | Shipping refunded. Not part of `amount`. | | `taxes` | NUMERIC | Tax refunded, including tax on refunded shipping. | | `discounts` | NUMERIC | Discount reversed by the refund. | | `reason` | STRING | Reason text. | | `store_view_name` | STRING | Label for the store. | | `items` | ARRAY\ | Refund lines, see below. Leave empty for a refund on the order as a whole. | Each element of `items` is a `STRUCT` with these fields, in this order: | Field | Type | Meaning | | - | - | - | | `item_id` | STRING | Required. The `id` of the order line being refunded. | | `variant_id` | STRING | The variant refunded. | | `quantity` | INT64 | Required. Units refunded, more than 0. | | `amount` | NUMERIC | Required. Goods refunded for this line. | | `taxes` | NUMERIC | Tax refunded for this line. | | `discounts` | NUMERIC | Discount reversed for this line. | ### eyk\_ingest\_shipping\_methods and eyk\_ingest\_payment\_methods A rate card: what a shipping or payment method costs you, per method, from a date onward. Eyk applies it to orders that name the method and carry no `shipping_cost` or `payment_cost` of their own. | Column | Type | Meaning | | - | - | - | | `method_id` | STRING | Required. Matches `shipping_method_id` or `payment_method_id` on orders. | | `method_name` | STRING | Label. | | `flat_cost` | NUMERIC | Cost per order, spread over the order's item lines. | | `percentage_cost` | NUMERIC | Cost as a percentage of the item gross sales, for example `1.5`. Added to `flat_cost` when both are set. | | `valid_from` | TIMESTAMP | The rate applies from this moment until the next row for the same `method_id`. NULL covers all history before the first dated row. | ## Full insert examples Complete statements for the four main tables. Replace the project and dataset and the source tables. ```sql Orders theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_orders` (id, order_number, payment_status, store_view_id, store_view_name, sales_channel, created_at, updated_at, currency, subtotal, discounts, discount_codes, shipping, shipping_tax, taxes, customer_id, customer_email, billing_country_code, shipping_country_code, shipping_cost, payment_cost, shipping_method_id, payment_method_id, items) SELECT CAST(o.order_id AS STRING), CAST(o.order_number AS STRING), CAST(o.payment_status AS STRING), CAST(o.shop_code AS STRING), CAST(o.shop_name AS STRING), 'web', CAST(o.ordered_at AS TIMESTAMP), CAST(o.modified_at AS TIMESTAMP), 'EUR', CAST(o.goods_ex_vat AS NUMERIC), CAST(o.discount_ex_vat AS NUMERIC), ANY_VALUE(o.coupon_codes), CAST(o.shipping_ex_vat AS NUMERIC), CAST(o.shipping_vat AS NUMERIC), CAST(o.vat_total AS NUMERIC), CAST(o.customer_id AS STRING), o.customer_email, o.billing_country, o.shipping_country, CAST(o.carrier_cost AS NUMERIC), CAST(o.psp_fee AS NUMERIC), o.shipping_method, o.payment_method, ARRAY_AGG(STRUCT( CAST(l.line_id AS STRING) AS id, CAST(l.quantity AS INT64) AS quantity, CAST(l.unit_price_ex_vat AS NUMERIC) AS unit_price, CAST(l.unit_cost AS NUMERIC) AS unit_cost, CAST(l.discount_ex_vat AS NUMERIC) AS discounts, CAST(l.vat AS NUMERIC) AS taxes, CAST(l.variant_id AS STRING) AS variant_id, CAST(l.sku AS STRING) AS sku, CAST(NULL AS STRING) AS product_id )) FROM `your-project.sales.orders` o JOIN `your-project.sales.order_lines` l ON l.order_id = o.order_id WHERE o.modified_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY) GROUP BY o.order_id, o.order_number, o.payment_status, o.shop_code, o.shop_name, o.ordered_at, o.modified_at, o.goods_ex_vat, o.discount_ex_vat, o.shipping_ex_vat, o.shipping_vat, o.vat_total, o.customer_id, o.customer_email, o.billing_country, o.shipping_country, o.carrier_cost, o.psp_fee, o.shipping_method, o.payment_method; ``` ```sql Customers theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_customers` (id, email, first_name, last_name, created_at, updated_at) SELECT CAST(customer_id AS STRING), email, first_name, last_name, CAST(created_at AS TIMESTAMP), CAST(modified_at AS TIMESTAMP) FROM `your-project.crm.customers` WHERE modified_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY); ``` ```sql Product variants theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_product_variants` (id, sku, name, image_url, parent_id, parent_name, parent_image_url, fallback_unit_cost, brand_id, brand_name, category_path, additional_category_paths, created_at, updated_at) SELECT CAST(variant_id AS STRING), CAST(sku AS STRING), variant_name, variant_image_url, CAST(product_id AS STRING), product_name, product_image_url, CAST(purchase_price_ex_vat AS NUMERIC), CAST(brand_id AS STRING), brand_name, ARRAY(SELECT s FROM UNNEST(SPLIT(category_breadcrumb, ' > ')) AS s WHERE TRIM(s) != ''), ARRAY(SELECT STRUCT(SPLIT(p, ' > ') AS path) FROM UNNEST(extra_category_breadcrumbs) AS p WHERE TRIM(p) != ''), CAST(created AS TIMESTAMP), CAST(modified AS TIMESTAMP) FROM `your-project.catalog.variants` WHERE modified >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY); ``` ```sql Refunds theme={null} INSERT INTO `eyk-project.yourstore_import.eyk_ingest_refunds` (id, order_id, store_view_id, created_at, updated_at, amount, shipping, taxes, discounts, reason, items) SELECT CAST(r.refund_id AS STRING), CAST(r.order_id AS STRING), CAST(r.shop_code AS STRING), CAST(r.refunded_at AS TIMESTAMP), CAST(r.modified_at AS TIMESTAMP), CAST(r.goods_ex_vat AS NUMERIC), CAST(r.shipping_ex_vat AS NUMERIC), CAST(r.vat_total AS NUMERIC), CAST(r.discount_ex_vat AS NUMERIC), r.reason, ARRAY_AGG(STRUCT( CAST(l.line_id AS STRING) AS item_id, CAST(l.variant_id AS STRING) AS variant_id, CAST(l.quantity AS INT64) AS quantity, CAST(l.amount_ex_vat AS NUMERIC) AS amount, CAST(l.vat AS NUMERIC) AS taxes, CAST(NULL AS NUMERIC) AS discounts )) FROM `your-project.sales.refunds` r JOIN `your-project.sales.refund_lines` l ON l.refund_id = r.refund_id WHERE r.modified_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY) GROUP BY r.refund_id, r.order_id, r.shop_code, r.refunded_at, r.modified_at, r.goods_ex_vat, r.shipping_ex_vat, r.vat_total, r.discount_ex_vat, r.reason; ``` ## Check it worked Run these on your hook tables the morning after the first scheduled run. Then open the Custom BigQuery source in Eyk under **Sources** and read the **Monitor** tab: it lists orders, refunds, product variants and customers received per day. Data appears in reports the morning after Eyk's night run. ```sql theme={null} -- Rows per day SELECT DATE(inserted_at) AS day, COUNT(*) AS rows_inserted FROM `eyk-project.yourstore_import.eyk_ingest_orders` GROUP BY day ORDER BY day DESC LIMIT 7; -- Order lines whose variant is missing from the catalog. Expected: no rows. SELECT i.variant_id, COUNT(*) AS lines FROM `eyk-project.yourstore_import.eyk_ingest_orders` o, UNNEST(o.items) AS i LEFT JOIN `eyk-project.yourstore_import.eyk_ingest_product_variants` v ON v.id = i.variant_id WHERE v.id IS NULL GROUP BY i.variant_id ORDER BY lines DESC LIMIT 20; -- Refunds whose order is missing. Expected: no rows. SELECT r.id, r.order_id FROM `eyk-project.yourstore_import.eyk_ingest_refunds` r LEFT JOIN `eyk-project.yourstore_import.eyk_ingest_orders` o ON o.id = r.order_id WHERE o.id IS NULL LIMIT 20; ``` ## Troubleshooting The account that runs the scheduled query has no write access to the `_import` dataset, or the query runs in another location than the dataset. Ask your Eyk contact person if you need help with this step. A required column received NULL. Common causes: a refund without order id, an order line without id or quantity. One of the array columns holds a NULL. Filter with `WHERE s IS NOT NULL AND TRIM(s) != ''` inside the `ARRAY(...)` subquery. Your INSERT names a column that does not exist in the table. Compare it with [Columns](#columns). Field order and types inside `STRUCT(...)` must match the table exactly, in the order listed under [Columns](#columns). Name every field with `AS`. Nothing to fix. Eyk keeps one version per `id`, the one with the latest `updated_at`. # Data API Source: https://docs.eykdata.com/guides/custom-data/data-api Send orders, customers, product variants and refunds from your own backend to Eyk over HTTPS, and correct costs from a second system. Send your orders, customers, product variants and refunds to Eyk over HTTPS, one object or a batch per request. Eyk stores every request and processes it in the night run. ## Before you start You need: * The [Custom data](/guides/custom-data) page, read once. * The Custom Data API source added in Eyk under **Sources**. * An API key, created on the **Configure** tab of that source with **Create API key**. The key is shown once. * Your organization id, the UUID after `/org/` in the URL of your Eyk environment. ## Endpoints | Object | Endpoint | Key field | | - | - | - | | Product variant | `POST https://data-api.eykdata.com/v1/product_variants` | `id` | | Customer | `POST https://data-api.eykdata.com/v1/customers` | `id` | | Order | `POST https://data-api.eykdata.com/v1/orders` | `id` | | Refund | `POST https://data-api.eykdata.com/v1/refunds` | `id` | | Order update | `POST https://data-api.eykdata.com/v1/order_updates` | `order_id` | | Product variant update | `POST https://data-api.eykdata.com/v1/product_variant_updates` | `id` | Every request carries the header `X-API-Key` and a JSON body: one object, or an array of up to 100 objects. Every object carries your `organization_id`. Timestamps are ISO 8601 with a timezone offset, for example `2026-03-14T10:15:00Z`. Every field, with its type and meaning, is in the [Data API reference](https://data-api.eykdata.com/docs). Send one product variant with `debug=true`. Eyk validates the object and your key and stores nothing. A `200` means the object is accepted. ```bash theme={null} curl -X POST 'https://data-api.eykdata.com/v1/product_variants?debug=true' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "5f6a0c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b", "id": "VASE-BLU", "sku": "VB-001", "name": "Blue", "parent_id": "VASE", "parent_name": "Ceramic vase", "fallback_unit_cost": 8.50, "category_path": ["Home", "Vases"], "created_at": "2025-06-01T08:00:00Z", "updated_at": "2026-02-20T12:00:00Z" }' ``` Repeat without `debug=true` for every variant your order lines refer to, in batches of up to 100. Then send your customers to `/v1/customers`. `email` is optional on a customer; without it Eyk identifies the customer by `id` only. Refer to the customer and the variants by id. Every amount excludes tax. ```bash theme={null} curl -X POST 'https://data-api.eykdata.com/v1/orders' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "5f6a0c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b", "id": "NV-10432", "order_number": "#1043", "payment_status": "paid", "store_view_id": "nl", "store_view_name": "Noor & Vos NL", "sales_channel": "web", "created_at": "2026-03-14T10:15:00Z", "updated_at": "2026-03-14T10:15:00Z", "currency": "EUR", "subtotal": 90.00, "discounts": 10.00, "discount_codes": ["SPRING10"], "shipping": 5.00, "shipping_tax": 1.05, "taxes": 17.85, "customer": "CUST-8842", "shipping_address": { "country": "NL" }, "items": [ { "id": "NV-10432-1", "quantity": 2, "unit_price": 20.00, "taxes": 8.40, "product_variant": "VASE-BLU" }, { "id": "NV-10432-2", "quantity": 1, "unit_price": 50.00, "discounts": 10.00, "taxes": 8.40, "product_variant": "TRAY-OAK" } ] }' ``` `customer` and `product_variant` also take the full object instead of the id. Then you can skip the separate customer and variant requests. Send each refund to `/v1/refunds` with the `order_id` of the refunded order, the `amount` refunded for goods, and the refunded lines in `items` when you have them. Send a record again, in full, with a later `updated_at` whenever it changes. Per `id` the latest `updated_at` wins. Optional. When a second system, such as an ERP, knows the real shipping cost or cost of goods, send an order update. Only the fields you name change. Per field, the update with the highest `priority` wins, then the latest `occurred_at`. ```bash theme={null} curl -X POST 'https://data-api.eykdata.com/v1/order_updates' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "5f6a0c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b", "order_id": "NV-10432", "occurred_at": "2026-03-16T07:00:00Z", "source_system": "erp", "priority": 10, "shipping_cost": 4.35, "items": [{ "id": "NV-10432-2", "unit_cost": 21.00 }] }' ``` A field that was removed from the API is accepted and ignored, and the response carries a `Warning` header that names it. ## Check it worked Open the Custom Data API source in Eyk under **Sources**. The **Monitor** tab lists orders, refunds, product variants and customers received per day. Data appears in reports the morning after Eyk's night run. ## Troubleshooting A timestamp has no timezone offset. Send `2026-03-14T10:15:00Z` or `2026-03-14T12:15:00+02:00`. The API key does not belong to this `organization_id`. Check the id after `/org/` in your Eyk URL, and the scope chosen when the key was created. The array holds more than 100 objects. Split it. Two lines in one order share an `id`. Line ids must be unique within the order. Reports update once a night. If the order is still missing the next morning, check the Monitor tab for the day you sent it. Then check that its `variant_id` and `customer_id` values match records you sent. # Magento SSH access Source: https://docs.eykdata.com/guides/magento-ssh-access Give Eyk access to your Magento MySQL database over an SSH tunnel when IP whitelisting is not possible. By default, Eyk connects to your Magento MySQL database server through IP whitelisting. If whitelisting is not possible, Eyk connects over an SSH tunnel instead. This guide explains how to give Eyk that SSH access. Follow this guide only if IP whitelisting is not an option for you. If it is, follow the normal steps to connect Eyk to Magento. Eyk authenticates with a public SSH key. You add this key to a user account on the server that runs the database. There are two ways to add it: * Option A: through the admin control panel of your Magento hosting provider. * Option B: directly on the server. Use this option if you have no hosting provider, or your provider has no SSH key management. If your hosting provider offers SSH key management in its admin panel, you must use Option A. Keys added directly on the server are periodically overwritten. First, collect the credentials below. You need them for both options. | Name | Description | | - | - | | SSH address | The IP address or hostname of the server running the MySQL database | | SSH port | The port on the server that is open for SSH connections, for example 22 | | SSH username | The username of a user account on the server that Eyk connects as over SSH. Option A or B below configures access for this user | | Store view IDs | Optional. The store view IDs to collect data for. Leave empty to collect data for all store views. | When Eyk connects over SSH, set the "Database host address" and "Database host port" to the **local** address and port of the database. ## Option A: Use the admin panel of your hosting provider If your Magento hosting provider allows SSH key management through its admin control panel: 1. Log in to the admin control panel of your hosting provider. 2. Go to the SSH key management section. 3. Paste the public SSH key below into the key field. Optionally name the key "Eyk". Save the changes. ```text theme={null} AAAAB3NzaC1yc2EAAAADAQABAAABgQCXqGAIgzLG8qbkD/PavKfNFC3spzI2aYCRexJt8IM/gOwOrnYANrMIB+ZhN1qhJyWqhnqweruIHcQigIAHJ+V4IDmGFhv601YHjMpQob8PnrVq33JNGsIJ2rYnkAOdu3zBKHxUT855WM/4/yd9ZGo0koGK8AA8T8T/Cj1J3JZ2ZPS09kxytLLGBj2Pe+fw5+fA0rUPglQMuCJ6KdgBtdUA0SqVbbitgfLTO5w4lsiuH+KqFjmiy1rkmxTAqIKHIFY4pcDEEi8X8mIAE8eFs6UOZV2kJzaaOf6PeX5b+qelXddqbfHkjI3w/w1V944XIJA9SFwnGpkrJVtgc8Nmcu4TQEKug4Q/zuCG94/uh5NsEubLlYbrMLvs47jBJ2MoIPI+lOc8k23nZbwSxNQ41FpcU4xxnPZSIwewr/usxCziLlT3CZ/EuN9jrikbHMe0AQLcdmXr7NZmiGTHs2E7hX6zrHpHaXs2+FJwJT//ttP5w+nv1KTEHgqKvPCYrYpe3N0= support@eykdata.com ``` ## Option B: Add the key directly on the server If your hosting provider has no SSH key management in its admin panel, or you have no hosting provider, add the SSH key directly on the server. These commands are for Linux servers. Ask your Eyk contact person if you need help with this step. ### Go to the home directory of the user On the server running the MySQL database, go to the home directory of the user that matches the **SSH Username**: ```bash theme={null} cd /home/{{SSH Username}} ``` ### Make sure the .ssh directory exists Run this command from the home directory of the SSH user: ```bash theme={null} mkdir -p .ssh ``` ### Add the Eyk public SSH key to authorized\_keys Run this command from the home directory of the SSH user: ```bash theme={null} echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXqGAIgzLG8qbkD/PavKfNFC3spzI2aYCRexJt8IM/gOwOrnYANrMIB+ZhN1qhJyWqhnqweruIHcQigIAHJ+V4IDmGFhv601YHjMpQob8PnrVq33JNGsIJ2rYnkAOdu3zBKHxUT855WM/4/yd9ZGo0koGK8AA8T8T/Cj1J3JZ2ZPS09kxytLLGBj2Pe+fw5+fA0rUPglQMuCJ6KdgBtdUA0SqVbbitgfLTO5w4lsiuH+KqFjmiy1rkmxTAqIKHIFY4pcDEEi8X8mIAE8eFs6UOZV2kJzaaOf6PeX5b+qelXddqbfHkjI3w/w1V944XIJA9SFwnGpkrJVtgc8Nmcu4TQEKug4Q/zuCG94/uh5NsEubLlYbrMLvs47jBJ2MoIPI+lOc8k23nZbwSxNQ41FpcU4xxnPZSIwewr/usxCziLlT3CZ/EuN9jrikbHMe0AQLcdmXr7NZmiGTHs2E7hX6zrHpHaXs2+FJwJT//ttP5w+nv1KTEHgqKvPCYrYpe3N0= support@eykdata.com" >> .ssh/authorized_keys ``` Your MySQL database server is now configured so that only Eyk can connect to it, over an encrypted connection. # Eyk Docs Source: https://docs.eykdata.com/index Set up first-party tracking for your store and send conversions to your ad platforms with Eyk. Set up first-party, server-side tracking for your store, send conversions to your ad platforms, and build your own apps on Eyk data. Give Eyk access to Shopify, Magento 2 or Firmhouse, and learn about the Eyk event specification and UTM parameters. Connect Google Ads, Google Analytics 4, Meta Ads, TikTok Ads and OpenAI Ads to receive server-side conversions. Connect Magento over SSH, load data from a custom backend, and learn how attribution models credit your marketing. Connect an AI tool through MCP, or build your own app on Eyk data. # Event specification Source: https://docs.eykdata.com/tracking/event-spec JSON payload reference for every event Eyk tracks, client-side and server-side, including the cookie and consent objects. This page lists the JSON payload of every event Eyk tracks, client-side and server-side, with the cookie and consent objects. ## Page load Eyk tracks this event automatically. You do not need to add it to the data layer. Type: client-side ```json theme={null} { "event": "page_view_eyk", "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## View item Type: client-side ```json theme={null} { "event": "view_item_eyk", "ecommerce": { "currency": "EUR", "value": "100", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "100", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## View collection Type: client-side ```json theme={null} { "event": "view_collection_eyk", "ecommerce": { "collection_id": "12345678", "currency": "EUR", "item_list_name": "Products", "items": [ { "image_url": "https://example-store.com/images/gift-card.png", "index": "1", "item_brand": "Snowboard Vendor", "item_category": "Gifts", "item_id": "8404872757547", "item_name": "Gift Card", "item_sku": "SKU-123478", "item_variant": "45556788429099", "price": "10.00", "variant_name": "$10" }, { "image_url": "https://example-store.com/images/ski-wax.png", "index": "2", "item_brand": "GTM-test-store-10", "item_category": "Gifts", "item_id": "9143900537115", "item_name": "Selling Plans Ski Wax", "item_sku": "SKU-1237658", "item_variant": "48564641825051", "price": "24.95", "variant_name": "Selling Plans Ski Wax" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## Add to cart Type: client-side ```json theme={null} { "event": "add_to_cart_eyk", "ecommerce": { "currency": "EUR", "value": "600", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## View cart Type: client-side ```json theme={null} { "event": "view_cart_eyk", "ecommerce": { "value": "600", "currency": "EUR", "cart_quantity": "1", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## Begin checkout Type: client-side ```json theme={null} { "event": "begin_checkout_eyk", "ecommerce": { "value": "600", "currency": "EUR", "cart_quantity": "1", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## Add payment info Type: client-side ```json theme={null} { "event": "add_payment_info_eyk", "ecommerce": { "value": "600", "currency": "EUR", "cart_quantity": "1", "payment_type": "iDeal", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## Add shipping info Type: client-side ```json theme={null} { "event": "add_shipping_info_eyk", "ecommerce": { "value": "600", "currency": "EUR", "cart_quantity": "1", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK" } } ``` ## Purchase Type: client-side ```json theme={null} { "event": "purchase_eyk", "ecommerce": { "cart_quantity": "1", "value": "600", "coupon": "test", "currency": "EUR", "discount": "test", "discount_amount": "90.00", "discount_percentage": "15", "shipping": "0", "sub_total": "510", "tax": "0", "transaction_id": "5937102258459", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK", "new_customer": true } } ``` ## Purchase (webhook) Type: server-side ```json theme={null} { "event": "purchase_webhook_eyk", "ecommerce": { "cart_quantity": "1", "value": "600", "coupon": "test", "currency": "EUR", "discount": "test", "discount_amount": "90.00", "discount_percentage": "15", "shipping": "0", "sub_total": "510", "tax": "0", "transaction_id": "5937102258459", "items": [ { "image_url": "https://example-store.com/images/snowboard-hydrogen.png", "item_brand": "Hydrogen Vendor", "item_category": "Gifts", "item_id": "9143899914523", "item_name": "The Collection Snowboard: Hydrogen", "item_sku": "SKU-76543281", "item_variant": "485646409345615", "price": "600", "quantity": "1", "variant_name": "Default Title" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK", "new_customer": true }, "cookies": { "__eui": "28e7e120-d123-4567-8910...", "_fbc": "fb.1.1707923456.IwAR0...", "_fbp": "fb.1.1707923456.1234567...", "_ga": "GA1.1.123456789.1707923...", "_gcl_aw": "GCL.1707923456.IwAR0_Q...", "_gcl_au": "1.1.1234567890.1707923...", "_uetsid": "28e7e120-d123-4567-8910...", "_uetvid": "48b6c450-a123-4567-8910...", "_epik": "dj0yJnU9UmFuZG9tUGludGVy...", "_ttp": "a1b2c3d4-e5f6-7890-g1h2...", "ttclid": "E.C.P.dje8a90d...", "awin_awc": "12345_1707923456_a1b2c3d...", "__kla_id": "eyIkZXhjaGFuZ2VfaWQiOiAiS...", "_kx": "eyIkZXhjaGFuZ2VfaWQiOiAiS...", "__oppref": "oppref_a1b2c3d4e5f6...", "__obref": "obref_1234567890abcdef..." }, "consent": { "ad_storage": "granted", "analytics_storage": "granted", "ad_user_data": "granted", "ad_personalization": "granted" } } ``` The cookie object holds these cookies: | Cookie | Meaning | | - | - | | `__eui` | Eyk User ID | | `_fbc` | Meta (Facebook/Instagram) Click ID | | `_fbp` | Meta (Facebook/Instagram) Browser ID | | `_ga` | Google Analytics Client ID | | `_gcl_aw` | Google Ads Click ID (GCLID) | | `_gcl_au` | Google Marketing Platform Conversion Linker | | `_uetsid` | Microsoft (Bing) Session ID | | `_uetvid` | Microsoft (Bing) Visitor ID | | `ttclid` | TikTok Click ID | | `_ttp` | TikTok Pixel ID | | `_epik` | Pinterest Click ID | | `awin_awc` | Awin Click ID | | `__kla_id` | Klaviyo Identity Cookie | | `_kx` | Klaviyo Tracking Cookie (exchange ID) | | `__oppref` | OpenAI Ads Click Reference | | `__obref` | OpenAI Ads Browser Reference | The consent object passes the user's explicit choices about data collection, formatted according to Google Consent Mode v2. Accepted values: `granted`, `denied`. ## Subscription start Type: client-side & server-side ```json theme={null} { "event": "subscription_start_eyk", "ecommerce": { "currency": "EUR", "value": "100", "items": [ { "image_url": "https://example-store.com/images/subscription-product.png", "item_brand": "Subscription Vendor", "item_category": "Subscriptions", "item_id": "9143899914524", "item_name": "Monthly Subscription", "item_sku": "SKU-SUB-001", "item_variant": "485646409345616", "price": "100", "quantity": "1", "variant_name": "Monthly Plan" } ] }, "user_data": { "city": "Rotterdam", "country": "NL", "customer_id": "12349878", "email": "example@eykdata.com", "first_name": "Eyk", "last_name": "Data", "phone": "+31612345678", "region": "Zuid-Holland", "street": "Stationsplein", "zip": "3013AK", "new_customer": true } } ``` # Firmhouse Source: https://docs.eykdata.com/tracking/firmhouse Invite Eyk to your Firmhouse project as a project manager so the Eyk team can set up server-side tracking. Invite Eyk to your Firmhouse project so the Eyk team can set up server-side tracking for your store. ## Before you start You need: * Access to the settings of your Firmhouse project. In your Firmhouse project, click **Team members** under **Settings** in the menu on the left. Firmhouse settings menu with Team members highlighted Click **Invite new member** in the top-right corner. In the pop-up, enter the email address [tracking@eykdata.com](mailto:tracking@eykdata.com) and click **Invite**. Firmhouse invite new member pop-up with the email field and Invite button You now see a screen like the one below. Let your contact person at Eyk know that you sent the invite. Continue with the next step after the Eyk team accepts it. Firmhouse team members page showing the pending invite When the Eyk team has accepted the invite, change the role of the [tracking@eykdata.com](mailto:tracking@eykdata.com) user. Click **Edit** to open the pop-up. Firmhouse team members page with the Edit button for the tracking@eykdata.com user Select the role **Project manager** and click **Save changes**. Firmhouse edit member pop-up with the Project manager role selected # Magento 2 Source: https://docs.eykdata.com/tracking/magento-2 Install the Stape plugin and create a Magento admin user for Eyk so the Eyk team can set up server-side tracking. The Eyk team sets up server-side tracking for your Magento 2 store. Before they can start, you install the Stape plugin and create a Magento user for Eyk. ## Before you start You need: * Admin access to your Magento 2 environment. * Someone who can install a plugin from the Adobe Commerce Marketplace: you or your Magento developer. Setup has two parts. First you install the Stape plugin, then you create a Magento user for Eyk. ## Part 1: Install the Stape plugin Eyk partners with [Stape.io](http://Stape.io) for server-side tracking. Install the [Stape Magento plugin](https://commercemarketplace.adobe.com/stape-gtm-server-side-magento-module.html) from the Adobe Commerce Marketplace. If you do not install the plugin yourself, share this link with your Magento developer and ask them to install it. Test your store before you release the plugin to production. The Eyk team checks that the plugin fires all tracking events, not whether the rest of your store still works. Once the Eyk team has access to your Magento admin, they test whether the plugin fires all tracking events. ## Part 2: Create a Magento user for Eyk In your Magento 2 admin, go to **System >> Permissions >> All users** and click **Add New User**. Enter these details: * User Name: eykdata * First Name: Eyk * Last Name: Data * Email: [tracking@eykdata.com](mailto:tracking@eykdata.com) * Password: Choose and enter a secure password Choose a strong password to protect the account. Assign a role to the new Eyk user with at least these **Role Resources**: * Stape plugin resources: all resources of the installed Stape plugin. * **Clean Cache Action** resources: both **Flush Cache Storage** and **Flush Magento Cache**. Share the username and password with your onboarding lead at Eyk through a secure method, such as a password manager. Ask your Eyk contact person if you need help with this step. # Shopify Source: https://docs.eykdata.com/tracking/shopify Give Eyk collaborator access to your Shopify store so the Eyk team can install server-side tracking. Give the Eyk team access to your Shopify admin so they can set up server-side tracking for your store. Shopify grants this access through a collaborator request, which you can revoke at any time. ## Before you start You need: * Access to your Shopify admin. In Shopify admin, click **Settings** in the bottom left. Shopify admin menu with Settings highlighted in the bottom left In the menu on the left, click **Users and permissions**. Shopify settings menu with Users and permissions highlighted Scroll down to the **Collaborators** section and copy the 4-digit **Collaborator Request Code**. Shopify Collaborators section showing the 4-digit Collaborator Request Code Share the collaborator request code with your contact person at Eyk. The Eyk team then sends you a collaborator request. You receive a notification of the new collaborator request from Eyk. Go to **Users and permissions** again, scroll down to the **Collaborators** section and click **Review request**. Shopify Collaborators section with the Review request button On the page with the Eyk collaborator details, add two permissions: 1. Under **Online store**, also tick **Themes** and **Edit code**. 2. Under **Store settings**, also tick **View customer events** and **Manage and add custom pixels**. Shopify collaborator permissions with Online store and Store settings checkboxes Scroll down to the bottom of the page and click **Accept request**. Shopify collaborator request page with the Accept request button After these steps, the Eyk team has the access it needs to set up server-side tracking for your Shopify store. # UTM parameters Source: https://docs.eykdata.com/tracking/utm-parameter-guide Add Eyk UTM parameters to your ads and emails per channel so attribution can identify each traffic source. A UTM parameter is a piece of code that you add to the end of a destination URL. It tells Eyk which marketing activity brought a visitor to your store. The Eyk attribution models use the parameters to identify the traffic source and the campaign or email. Setup differs slightly per channel. This page first explains how to combine Eyk parameters with existing ones, then gives the template for each channel. ## Combine with existing UTMs If your links already carry UTM parameters, add the Eyk parameters to them. Separate each parameter with an `&` symbol. Example: ```text theme={null} eyk_source=EXAMPLE_CHANNEL&eyk_id=EXAMPLE_ID&utm_source=EXAMPLE_CHANNEL ``` In this example, the Eyk parameters `eyk_source` and `eyk_id` are combined with the standard `utm_source` parameter. Paste them together and separate each parameter with an `&` symbol. ## Templates per channel ### Meta For Meta, go to each of your ads and click **Edit**. Place this tracking template in the **URL Parameters** field under the **Tracking** settings of the ad: ```text theme={null} eyk_source={{site_source_name}}&eyk_campaign={{campaign.id}}&eyk_id={{ad.id}} ``` Meta ad tracking template field showing the Eyk URL parameters ### Google Ads For Google Ads, go to **Admin** >> [Account Settings](https://ads.google.com/aw/settings/account) >> **Tracking**. Place this code in the **Tracking template** field: ```text theme={null} {lpurl}?eyk_source=google&eyk_campaign={campaignid}&eyk_id={creative}&eyk_gcl_id={gclid} ``` Google Ads account settings tracking template field showing the Eyk URL parameters Google Ads allows tracking templates at the account, campaign and ad-group levels. An ad-group or campaign-level template overrides an account-level template. Update existing ad-group and campaign-level templates as needed. ### Microsoft Ads For Microsoft Ads, go to **Campaigns** >> **Settings** and open the [Account level options](https://ui.ads.microsoft.com/campaign/vnext/accountsettings) tab. Place this code in the **Tracking template** field: ```text theme={null} {lpurl}?eyk_source=bing&eyk_campaign={CampaignId}&eyk_id={adid} ``` Microsoft Ads allows tracking templates at the account and campaign levels. A campaign-level template overrides an account-level template. Update existing campaign-level templates as needed. ### TikTok Ads For TikTok, add the Eyk tracking parameters to **every ad**. TikTok has no way to bulk edit ads. For each ad, go to **Edit ad** and open the **Destination** section. In the **Destination** section, enter this tracking template in the **URL** field: ```text theme={null} ?eyk_source=tiktok&eyk_campaign=__CAMPAIGN_ID__&eyk_term=__AID__&eyk_id=__CID__ ``` ### Pinterest Ads For Pinterest, go to each ad and scroll down to the Destination URL. Append this code to the **Destination URL**: ```text theme={null} ?eyk_source=pinterest&eyk_campaign={campaignid}&eyk_id={adid} ``` For example, if your destination URL is `https://example.com/product`, the new destination URL with Eyk parameters is: ```text theme={null} https://example.com/product?eyk_source=pinterest&eyk_campaign={campaignid}&eyk_id={adid} ``` Append this code to the Destination URL. Do not replace the URL with it. ### OpenAI Ads For OpenAI Ads, go to [OpenAI Ads Manager](https://ads.openai.com/) and open **Campaigns**. Open the three-dot menu of the campaign and select **Edit campaign**. Place this code in the **Landing page query parameters** field: ```text theme={null} eyk_source=openai_ads&eyk_campaign={campaign_id}&eyk_id={ad_id} ``` OpenAI fills in the placeholders on each click. `{campaign_id}` becomes the campaign ID and `{ad_id}` becomes the ad ID. Campaign and ad names are not available as placeholders. OpenAI Ads allows query parameters at the campaign, ad-group and ad levels. An ad or ad-group-level setting overrides a campaign-level setting. Update existing ad-group and ad-level settings as needed. ### Klaviyo For Klaviyo, go to **Settings >> Other**, click **UTM tracking** and use **Add UTM parameter** to add these parameters: | UTM parameter | Campaign values | Flow value | | - | - | - | | eyk\_source | klaviyo | klaviyo | | eyk\_campaign | Campaign name (campaign id) | Flow message name (flow message id) | | eyk\_id | Campaign id | Flow id | Then set the toggle **Automatically add UTM parameters to links** to **ON** and click **Save**. For existing campaigns, open **Editing** mode for each campaign. Scroll down to the **Tracking** section and set **Include tracking parameters** to **ON**. Set **Customize tracking parameters** to **OFF**. Do the same for existing flows: open each flow in **Edit** mode. For each **Email** and **SMS** action in the flow, go to **Settings** and set **Enable UTM tracking** to **ON**. ### Awin For Awin, you cannot add the parameters yourself. Ask Awin to add them to your program as a click append, a setting that adds parameters to every partner link. Contact your Awin contact or use the support form in the Awin app. Ask for this click append: ```text theme={null} eyk_source=awin&eyk_campaign=!!!sitename!!!&eyk_id=!!!promotype!!! ``` Awin fills in the placeholders on each click. `!!!sitename!!!` becomes the partner's site name. `!!!promotype!!!` becomes the partner's promotion type, for example `Cashback`. The full list of placeholders is in Awin's [Click appends and dynamic parameters](https://help.awin.com/developers/docs/click-appends-dyn-params). ### Other sources You can track any other source in Eyk too. Add these three parameters to the links in your ads, emails or posts. Eyk shows the values exactly as you enter them in your reports, so use readable names that you recognize later. | Parameter | Description | | - | - | | `eyk_source` | The platform or partner the link is placed on, for example `tradedoubler`, `mailchimp` or `snapchat`. Use one name per platform and keep it the same everywhere. | | `eyk_campaign` | The name of the campaign the link belongs to. | | `eyk_id` | The name of the individual ad, email or post within the campaign. | Keep names stable. If you rename a campaign or ad, its traffic shows up as a new row in Eyk. Example: ```text theme={null} eyk_source=snapchat&eyk_campaign=spring_sale&eyk_id=video_15s ```