Skip to content
For LLMsView as Markdown·

OneHazel Affiliate Tag ​

30-second install

html
<script src="https://app.onehazel.com/tag/v1/oh.js" data-operator-id="<your_operator_id>"></script>

Paste this on your landing / registration page. That's it.

Why you need it ​

Every affiliate platform — ReferOn, Affise, Income Access, and the rest — sends players to your site via a tracking link with attribution parameters in the URL:

https://casino.example.com/?mid=78545_589097&fluid=112e2f77-ab8f-4954-9051-2a6854eddbc0

If those parameters aren't captured at landing and included in the player registration event you send to OneHazel, the affiliate platform's daily reports will show empty attribution columns and they will reject the reports as malformed. Affiliates won't get paid. Your integration breaks silently.

The OneHazel Affiliate Tag is the cheapest way to fix this:

  • One <script> line on your landing page.
  • Captures the parameters into a first-party cookie scoped to your operator.
  • Exposes a helper your registration handler reads on form submit.
  • Threads the values into your existing player ingest payload.

Step-by-step install ​

Security: keep your API key on the server

Never put your oh_live_* API key in client-side code. Anyone can read it out of your browser bundle, your network tab, or View Source. The patterns below all use a server-side proxy — the browser posts the player payload + attribution to your own backend, and your backend forwards it to OneHazel with the secret key attached.

The only thing the browser ever sees is your operator_id (a public identifier, like a GA measurement ID — not a secret).

The pattern is the same everywhere:

  1. Add the tag to your landing / registration page so it loads on every player's first visit. The tag reads URL params at landing and stashes them in a first-party cookie.
  2. Mint an operator API key in your OneHazel dashboard (Settings → API Keys → Create). Store it as a server-only env var — e.g. ONEHAZEL_API_KEY (no NEXT_PUBLIC_ prefix).
  3. Add a server-side proxy route in your app that accepts the player payload from the browser, attaches the API key, and forwards to https://api.onehazel.com/operator-data-api/entities.
  4. Patch your registration handler to read window.OneHazel.getAttribution() and POST the captured params to your proxy route — never directly to OneHazel.

Where's my operator_id?

The tag needs your operator_id as a data-operator-id attribute. Find it on the Settings → Operator Profile card in the OneHazel dashboard, or by inspecting your URL when you're logged into the dashboard (https://app.onehazel.com/operator/...). It's the op_xxxxxxxx slug. Unlike the API key, this is not a secret — it's a public identifier and safe in the browser.

This is the casinoTato pattern — the one we use in production.

1. Mount the tag in your root layout (src/app/layout.tsx):

tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Script
          src="https://app.onehazel.com/tag/v1/oh.js"
          data-operator-id={process.env.NEXT_PUBLIC_ONEHAZEL_OPERATOR_ID}
          strategy="beforeInteractive"
        />
        {children}
      </body>
    </html>
  );
}

strategy="beforeInteractive" ensures the tag fires before React hydrates, so the cookie is set in time for any registration form that mounts immediately. NEXT_PUBLIC_ONEHAZEL_OPERATOR_ID is safe to expose — operator_id is a public identifier.

2. Add a server-side proxy route (src/app/api/onehazel/entities/route.ts):

ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  // Authenticate the caller against YOUR session/cookie before
  // forwarding — your proxy is the trust boundary.
  // const session = await getSession(req);
  // if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });

  const payload = await req.json();

  const upstream = await fetch('https://api.onehazel.com/operator-data-api/entities', {
    method: 'POST',
    headers: {
      // Server-only env var — never exposed to the browser bundle.
      Authorization: `Bearer ${process.env.ONEHAZEL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  const body = await upstream.json().catch(() => ({}));
  return NextResponse.json(body, { status: upstream.status });
}

3. Call the proxy from your registration submit handler:

ts
async function registerPlayer(form: PlayerForm) {
  // 1. Create the player on your own backend
  const player = await myBackend.createPlayer(form);

  // 2. Read OneHazel attribution + POST to your proxy (NOT to OneHazel directly)
  const attribution = (window as any).OneHazel?.getAttribution() || {};
  const params = attribution.params || {};

  await fetch('/api/onehazel/entities', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      entityType: 'player',
      externalId: player.id,
      data: {
        username: form.username,
        country: form.country,
        mid: params.mid,
        fluid: params.fluid,
      },
    }),
  });
}

The browser never sees an oh_live_* key. Your route handler holds it server-side.

Next.js — Pages Router ​

1. Mount the tag in pages/_document.tsx:

tsx
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script
          src="https://app.onehazel.com/tag/v1/oh.js"
          data-operator-id={process.env.NEXT_PUBLIC_ONEHAZEL_OPERATOR_ID}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

2. Add a server-side proxy at pages/api/onehazel/entities.ts:

ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end();

  // Authenticate the caller against YOUR session before forwarding.

  const upstream = await fetch('https://api.onehazel.com/operator-data-api/entities', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ONEHAZEL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(req.body),
  });

  const body = await upstream.json().catch(() => ({}));
  res.status(upstream.status).json(body);
}

Browser-side fetch identical to App Router — POST to /api/onehazel/entities with the player payload + attribution params.

Express / Node ​

Drop the tag in the <head> of your landing layout (<head><script src="..." data-operator-id="op_xxxxxxxx"></script></head>), then add a proxy route on your existing Express app:

js
const fetch = require('node-fetch'); // or built-in fetch on Node 18+

// Server-side proxy. Your client POSTs here; you forward to OneHazel.
app.post('/api/onehazel/entities', requireSession, async (req, res) => {
  const upstream = await fetch('https://api.onehazel.com/operator-data-api/entities', {
    method: 'POST',
    headers: {
      // Server-only env var. Never reaches the browser.
      Authorization: `Bearer ${process.env.ONEHAZEL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(req.body),
  });

  const body = await upstream.json().catch(() => ({}));
  res.status(upstream.status).json(body);
});

In your registration page's client JS, POST to /api/onehazel/entities after reading window.OneHazel.getAttribution(). If you already create the player in a single server-side POST /register handler, you can skip the browser → proxy hop entirely and call OneHazel directly from /register — just make sure the URL params are passed through (e.g. via a hidden form field populated from window.OneHazel.getAttribution() at submit time).

Django / Rails ​

Drop the tag in your application layout (base.html / application.html.erb). On form submit, your client JS reads window.OneHazel.getAttribution() and includes the values in the POST to your server. Your server-side handler then forwards to OneHazel with the API key attached:

python
# Django — settings.py:
# ONEHAZEL_API_KEY = os.environ["ONEHAZEL_API_KEY"]   # server-only env var

import os
import requests
from django.conf import settings

def register_view(request):
    form = PlayerForm(request.POST)
    player = Player.objects.create(...)

    requests.post(
        "https://api.onehazel.com/operator-data-api/entities",
        headers={
            # API key lives in settings.py / env, never in templates.
            "Authorization": f"Bearer {settings.ONEHAZEL_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "entityType": "player",
            "externalId": str(player.id),
            "data": {
                "username": form.cleaned_data["username"],
                "mid": request.POST.get("mid"),
                "fluid": request.POST.get("fluid"),
            },
        },
        timeout=5,
    )
    return JsonResponse({"ok": True})
ruby
# Rails — config/initializers/onehazel.rb:
# Rails.application.config.onehazel_api_key = ENV.fetch("ONEHAZEL_API_KEY")

class RegistrationsController < ApplicationController
  def create
    player = Player.create!(...)

    Net::HTTP.post(
      URI("https://api.onehazel.com/operator-data-api/entities"),
      {
        entityType: "player",
        externalId: player.id.to_s,
        data: {
          username: params[:username],
          mid: params[:mid],
          fluid: params[:fluid],
        },
      }.to_json,
      {
        "Authorization" => "Bearer #{Rails.application.config.onehazel_api_key}",
        "Content-Type"  => "application/json",
      },
    )

    render json: { ok: true }
  end
end

Sanity check — no real affiliate click needed ​

Visit https://your-casino.example/?mid=78545_589097&fluid=112e2f77-ab8f-4954-9051-2a6854eddbc0 in a clean browser. Open DevTools → Application → Cookies. You should see:

  • Cookie name: oh_attr_op_xxxxxxxx
  • Value: URL-encoded JSON with params, sources, firstSeenAt, referrer, landingPath

In the console, type window.OneHazel.getAttribution() and confirm the captured fields are present.

If that works, the tag is wired. Register a fake player and confirm the entity ingest call lands on OneHazel's side (visible in the Connections / Activity view).

Security: where to put the API key ​

The Affiliate Tag is client-side by design — it has to read URL params in the browser at landing time, before any of your backend code runs. But everything after the tag — the call into api.onehazel.com/operator-data-api/entities — belongs on the server. That call carries your oh_live_* API key, which is a secret with quota, billing, and write access to your tenant.

What goes in the browser ​

  • The <script src="https://app.onehazel.com/tag/v1/oh.js" data-operator-id="op_xxxxxxxx"> tag itself.
  • The data-operator-id attribute. Your operator_id is not a secret — it's a public identifier that's already visible in any HTTP request from your dashboard. Compare it to a Google Analytics measurement ID or a Stripe publishable key.
  • A call to window.OneHazel.getAttribution() to read the captured params.
  • A POST from your browser to your own backend carrying the player payload + the attribution params.

What does NOT go in the browser ​

  • Your oh_live_* API key. Ever. Under any circumstances. In any framework.
  • In Next.js: never give it a NEXT_PUBLIC_* prefix. NEXT_PUBLIC_ env vars are inlined into the client bundle at build time — anyone visiting your site can View Source and grep them out in seconds.
  • In Vite / CRA: never give it a VITE_* or REACT_APP_* prefix.
  • Never hardcode it in a JS file shipped to the browser.

The server-side proxy pattern ​

Every example above follows the same shape:

[Browser]   reads attribution, POSTs to /api/onehazel/entities
   │
   â–¼
[Your backend]   adds Bearer oh_live_*, forwards to api.onehazel.com
   │
   â–¼
[OneHazel]   stores the player entity

Your backend route handler is the trust boundary. It can:

  • Authenticate the caller against your own session / auth cookie before forwarding (so a malicious script on your site can't spam fake players through your proxy).
  • Validate / sanitise the payload before it hits OneHazel.
  • Rate-limit per session / IP independently of OneHazel's rate limits.
  • Add server-side context (real client IP, server-side referrer, signed user ID).

If you're already doing player creation server-side (the common case — your DB is the source of truth for players), you don't even need a dedicated proxy route. Just fire the OneHazel call from your existing POST /register handler.

What an attacker can do with a leaked oh_live_* key ​

  • Push arbitrary entities, events, and states into your OneHazel tenant.
  • Burn through your monthly Actions / AI Actions quota.
  • Read decrypted PII via GET /entities/:externalId (the key has full operator scope).
  • Trigger any workflow you've exposed.

If your key leaks, revoke it immediately from Settings → API Keys in the dashboard, then issue a new one.

How it fits together ​

Player clicks affiliate link
        │
        â–¼
[Affiliate platform redirects to your site]
        │  /?mid=…&fluid=…
        â–¼
[Your landing page loads — tag fires]
        │
        ├──► Reads URL params
        ├──► Matches against the affiliate-key registry (per active connector)
        └──► Writes first-party cookie  oh_attr_<operator_id>  (90 days, SameSite=Lax)
                │
                â–¼
Player registers on your site
        │
        â–¼
[Your registration handler reads window.OneHazel.getAttribution()]
        │
        â–¼
[Your existing POST /entities to api.onehazel.com — now carrying mid + fluid]
        │
        â–¼
[OneHazel stores attribution on the player; threads forward to every later event]
        │
        â–¼
[Affiliate platform fires daily /report against OneHazel — gets a CSV with attribution filled in]

Reading the captured values ​

In your registration / signup completion handler — call your own backend, which holds the API key, not OneHazel directly:

javascript
const attribution = window.OneHazel?.getAttribution() || {};
const params = attribution.params || {};

// Browser → your backend (no API key in this fetch).
await fetch('/api/onehazel/entities', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    entityType: 'player',
    externalId: customerId,
    data: {
      // …your existing player fields
      mid: params.mid,
      fluid: params.fluid,
    },
  }),
});

Your backend's /api/onehazel/entities handler attaches Authorization: Bearer ${process.env.ONEHAZEL_API_KEY} and forwards to https://api.onehazel.com/operator-data-api/entities — see the framework recipes above.

The shape of getAttribution():

typescript
{
  params: { [key: string]: string };  // e.g. { mid: "78545_589097", fluid: "112e2f77-…" }
  sources: string[];                  // e.g. ["referon"] or ["utm", "google_ads"]
  firstSeenAt: string;                // ISO timestamp
  referrer: string | null;
  landingPath: string | null;
}

Returns null if nothing has ever been captured for this operator on this browser.

What it captures ​

Connector / sourceURL parametersSource slug
ReferOnmid, fluidreferon
Generic UTMutm_source, utm_medium, utm_campaign, utm_content, utm_termutm
Google Adsgclidgoogle_ads
Meta (Facebook / Instagram) Adsfbclidmeta_ads
TikTok Adsttclidtiktok_ads
Microsoft / Bing Adsmsclkidmicrosoft_ads

Adding a new connector? It's a one-line registry entry + one-line migration. Reach out and we'll wire it.

First-touch wins

Subsequent affiliate clicks don't overwrite an existing attribution. This matches industry-standard affiliate accounting — the affiliate who introduced the player gets credit, not the one whose link they happened to click again. To clear explicitly (for testing or a data-deletion request): window.OneHazel.clearAttribution().

Alternative: include params in your ingest payload yourself ​

If your stack already captures URL parameters server-side (Next.js middleware, Rails / Django / Laravel session, etc.), you don't need the tag. Capture them in your own session, then include them directly in the server-side ingest payload:

javascript
// On your backend — the API key is already server-side here.
await fetch('https://api.onehazel.com/operator-data-api/entities', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.ONEHAZEL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    entityType: 'player',
    externalId: customerId,
    data: {
      mid: capturedFromSession.mid,
      fluid: capturedFromSession.fluid,
    },
  }),
});

OneHazel doesn't care how you capture them — only that they reach us on the ingest call from your server, not from the browser.

Optional: server-side click sync (v1.1.0+) ​

By default the tag is purely client-side — it sets a cookie and your registration handler reads from it. No network call ever leaves the browser.

There's an OPTIONAL second mode (added in tag v1.1.0) that ALSO fires a fire-and-forget POST to https://api.onehazel.com/track-event/v1/event so OneHazel sees the click independently of whether the operator's registration handler ever fires. Two reasons you'd want it:

  1. "Verify install" works. The dashboard panel that pops up after you connect a tracking connector has a "Verify install" button that polls for clicks on your operator. Without server sync, that button has nothing to find — you'd have to wait for your first real signup before knowing the tag works.
  2. Click ledger for debugging. If your registration flow has a bug that silently drops attribution before sending to OneHazel, the server-side ledger still recorded the click. Easier postmortem.

Opt in by adding data-server-sync="true" to the snippet:

html
<script src="https://app.onehazel.com/tag/v1/oh.js"
        data-operator-id="op_xxxxxxxx"
        data-server-sync="true"></script>

What gets sent:

FieldSource
operator_idYour data-operator-id (public, like a GA measurement ID)
anon_idA browser-issued UUID stored alongside the attribution cookie (oh_anon_<operator_id>)
captured.paramsThe URL params recognised from the registry (table above)
captured.sourcesThe source slugs (e.g. referon, utm)
first_seen_atTimestamp the tag captured (ISO8601)
referrer / landing_pathThe browser-reported document.referrer + URL path

What does NOT get sent:

  • Raw IP. The server hashes inbound x-forwarded-for with SHA-256 and stores only the hash for fraud detection. The plaintext IP is never persisted.
  • Cookies from other domains. Same-origin, first-party only.
  • Any other PII. The tag has no access to player names, emails, or form fields. It only reads URL params it recognises.

The server-sync POST is fire-and-forget — failures don't affect cookie behaviour or your registration handler. If you turn it off later (delete the attribute) the cookie path keeps working unchanged.

Privacy ​

  • First-party cookie only. Set on your own domain. No third-party tracking.
  • No fingerprinting. The tag does not read device, browser, or hardware properties.
  • SameSite=Lax — available on top-level navigation (the registration flow), blocked from cross-site iframe leakage.
  • 90-day TTL. Re-capture happens automatically if a player returns via a fresh affiliate click after expiry.
  • Network calls only when you opt in to server sync. With data-server-sync="true" the tag POSTs to api.onehazel.com/track-event/v1/event after capture. Without it, the tag is silent. See above for exactly what's sent.

First-party attribution cookies are generally lawful under "legitimate interest" or "strictly necessary for the service the user requested" carve-outs in EU/UK/US privacy regimes. Confirm with your own counsel for your jurisdiction.

Local development only — never ship to production ​

Local-dev quick-test only — never deploy this

When you're poking at the tag from a local browser console and want to confirm the end-to-end flow without standing up a proxy route first, you can call OneHazel directly with a temporary key:

js
// LOCAL DEV ONLY — paste in DevTools console, never check this in.
const attr = window.OneHazel.getAttribution();
await fetch('https://api.onehazel.com/operator-data-api/entities', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer oh_live_PASTE_TEMP_KEY_HERE',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    entityType: 'player',
    externalId: 'test_player_' + Date.now(),
    data: { mid: attr.params.mid, fluid: attr.params.fluid },
  }),
});

Do not check this into a file the browser will load. Do not write it into a Next.js page, a Vite component, an inline <script> block, or anywhere that ships to your visitors. Anyone who lands on that page can pull the key out of View Source or the network tab. Use a server-side proxy route for everything except this one-off console paste.

Troubleshooting ​

window.OneHazel.getAttribution() returns null
  • Confirm the <script> tag is on the landing page (View Source).
  • Confirm data-operator-id is set. The tag prints a console warning if it's missing.
  • Confirm the URL had a recognised parameter at landing time. Only known keys are captured (see table above).
  • Confirm cookies aren't being blocked (private browsing, third-party blocker).
Cookie set but my handler reads null
  • The cookie is set with Path=/. If your registration page is on a different subdomain than the landing page, the cookie won't be visible. Keep both on the same host.
  • Confirm the tag fired before your handler ran. If your handler is synchronous on page load, place the tag's <script> in <head> and load it synchronously (no defer / async).
Vendor still rejects my reports as malformed
  • Verify the values are in your ingest payload — log the request body.
  • Verify OneHazel received them by inspecting the player entity via the operator API.
  • Verify your connection's brand_guid and outbound API key on the OneHazel dashboard.

What's next ​

The Affiliate Tag is v1 — narrowly scoped to capturing affiliate / ad-platform URL parameters at landing. A broader OneHazel JS SDK is on the roadmap (page-view tracking, event capture, user identification, server-side click correlation). The Affiliate Tag's API (window.OneHazel.getAttribution()) is a stable contract — when the broader SDK ships, this snippet keeps working unchanged.

Versioning ​

  • v1 — current. Pinned at /tag/v1/oh.js. Breaking changes ship at /tag/v2/oh.js so v1 installs keep working.
  • v1.1.0 (2026-05-23) — added optional data-server-sync="true" for the click ledger + "Verify install" UX. Pure additive; no v1.0.0 caller changes behaviour. window.OneHazel.version === '1.1.0'.
  • window.OneHazel.* is a stable namespace. Additions are non-breaking; renames or removals are major-version bumps.