> ## Documentation Index
> Fetch the complete documentation index at: https://fdbad6a-heedeo-e1965c.manicule.dev/llms.txt
> Use this file to discover all available pages before exploring further.
> For every page in one file, fetch https://fdbad6a-heedeo-e1965c.manicule.dev/llms-full.txt (large).
> To search and read these docs as tools, connect the MCP server at https://fdbad6a-heedeo-e1965c.manicule.dev/api/mcp.
> Every agent surface: https://fdbad6a-heedeo-e1965c.manicule.dev/.well-known/llms-manifest.json

# 1. Quick start (/quick-start)

<!-- agent-signals: reading_time_min: 7 · est_tokens: 2339 · updated: 2026-11-11 -->
Related: [Integrating Heedeo](/index.md), [Connect your app](/connect-your-app.md), [2. Serve Heedeo from your own domain (do this before you go live)](/serve-heedeo-from-your-own-domain.md), [3. One server call: evaluate or log](/one-server-call.md), [4. The fields to act on](/the-fields-to-act-on.md), [5. Decide: score, rules or watch](/decide.md)

## Before you start: keys, address and auth

Everything a first call needs, in one place:

* **Keys.** [Sign up](https://app.heedeo.com/review/sign-up) and you get a publishable key (`pk_...`) for the browser and a secret key (`sk_...`) for your server. The secret is shown on that first visit only; **Rotate** under API keys makes a new one.
* **Environment variables.** The package reads `HEEDEO_SECRET_KEY` on the server and the examples read `NEXT_PUBLIC_HEEDEO_KEY` in the browser (in Next, `.env.local`; in Vite, `VITE_HEEDEO_KEY`).
* **Base URL.** The API is at `https://app.heedeo.com`. The package uses it by default.
* **Auth.** Every server call sends `authorization: Bearer sk_...`. A call without it answers 401 `missing_key`.
* **Install the package by its version.** `npm install @heedeo/sdk@0.2.0-beta.1`. A plain `npm install @heedeo/sdk` installs the `latest` tag, which is still `0.1.0-pilot.18`, and that version has no `preload()` and no `verdict.flagged`. Check with `npm ls @heedeo/sdk`.

Check the key before you write any code. This records nothing:

```sh
curl -s -H "authorization: Bearer $HEEDEO_SECRET_KEY" -H "content-type: application/json" \
  -d '{}' https://app.heedeo.com/v1/evaluations
```

A 400 `invalid_request` means the key worked: the call had no action, so it was refused after the key was checked. A 401 means the key is wrong or missing.

## Load the JavaScript agent

Load the JavaScript agent from Heedeo's own address, `https://heedeo.dev`. It works on any website, with no route to add, no DNS to change and no visitor IP for your server to work out. Put the tag in the `<head>` of every page with an action (section 3):

```html
<script src="https://heedeo.dev/i.js?key=pk_YOUR_PUBLISHABLE_KEY" async></script>
```

Or with the package (see the [package quickstart](https://app.heedeo.com/docs/sdk)):

```ts
import { createHeedeo } from "@heedeo/sdk/browser";

export const heedeo = createHeedeo({ publishableKey: process.env.NEXT_PUBLIC_HEEDEO_KEY });
```

With the package, call `heedeo.preload()` when the page with your sign-up and sign-in buttons mounts (0.2.0-beta.1 or later):

```tsx
useEffect(() => heedeo.preload(), []);
```

It loads Heedeo's script ahead of the click and identifies nobody until `identify()`. Loading the script does what the script tag does when a page loads (below: on the default profile, one identifier kept in the browser's cache), so a site that asks for consent before it loads Heedeo calls `preload()` once it has it. Without it the first `identify()` has to load Heedeo at the click, and on a slow phone that can take longer than the wait, so the sign-up goes on with no token. The script tag needs no such call: it loads with the page.

At the action, `identify()` in the browser (below) and send the token to your server, which sends the event with it (section 3). This `heedeo` is the server client, not the browser one above:

```ts
// On your server. The client reads HEEDEO_SECRET_KEY.
import { Heedeo } from "@heedeo/sdk";
const heedeo = new Heedeo();

const verdict = await heedeo.evaluate({ token, action: "signup", email });
if (verdict.flagged) return refuse(); // section 5: what a caught signup meets
```

Without the package, in any language, it is one HTTP request. Only `action` is required:

```sh
curl -s -X POST https://app.heedeo.com/v1/evaluations \
  -H "authorization: Bearer $HEEDEO_SECRET_KEY" \
  -H "content-type: application/json" \
  -H "heedeo-version: 2026-11-08" \
  -d '{"action": "signup", "email": "new-user@example.com", "token": "THE_TOKEN_FROM_IDENTIFY"}'
```

Unlike the key check above, this records a real event. A token that is missing or made up is scored as no token, and `no_token` shows in `reasons`. Section 4 explains each field of the answer, and section 3 lists every field you can send. The [API description](https://app.heedeo.com/docs/api.json) gives the full schema.

`verdict.flagged` is the one thing to read first: true when Heedeo flags the signup, never for a person you pressed Trust on, and never when Heedeo did not answer. No request and no client IP: the event takes the address Heedeo saw when the browser identified. This is the quick start: serve Heedeo from your own domain (section 2) before you go live. Until you list allowed origins on the Domains page (under Advanced, request filtering), any website may load Heedeo with your public API key; with some listed, only those may.

## The script tag and `identify()` at the action

Put the tag on every page with an action, in the `<head>` (in the Next App Router, a plain `<script>` in `app/layout.tsx`'s `<head>`):

```html
<script src="https://heedeo.dev/i.js?key=pk_YOUR_PUBLISHABLE_KEY" async></script>
```

On your own domain, `src` is your endpoint's (section 2).

In TypeScript, declare it once in any module (for example `app/layout.tsx`): `declare global { interface Window { heedeo?: { identify(who?: { userId?: string | null; email?: string | null }): Promise<{ token: string; visitor_id: string; confidence: number }> } } }` (optional, since the script loads `async`).

Loading it identifies nobody. It installs `window.heedeo` and reads the browser so the first call is quick. On the default profile it also asks Heedeo for one identifier, which the browser keeps in its cache; on the EU profile loading stores nothing. What it reads is the collection profile you choose under Settings, Advanced, Collection: every source by default, or the EU profile, which leaves out canvas, audio, fonts, speech voices, text and emoji measurements, media formats and the keyboard layout (section 7).

Call `await heedeo.identify()` in the signup handler, just before you submit.

* Each call is one identification and returns a `token` that lives five minutes and is good for one event.
* It resolves with `{ token, visitor_id, confidence }`, or rejects within 3 seconds, so a handler that awaits it never holds a signup for longer.
* It also keeps the token in a first-party cookie for those five minutes. Send the token this call returned, not the cookie's.
* Identify also sets Heedeo's own visitor cookie, `_idd`, which is HttpOnly.

Clearing cookies or opening a private window does not make a new person: Heedeo still recognises the browser, in most cases. Such a browser first gets a `visitor_id` of its own from `identify()`, because Heedeo is not certain of it yet. It is your event's answer that names the earlier visitor, in `visitor.id`, with both browsers in `visitor.browsers`. So the person's id is the event's `visitor.id`, not the `visitor_id` that `identify()` returns to the page.

Carry the token to your server in the form (a hidden field) or your JSON body. Send the one this call returned, and none when it returned none.

`identify()` can reject (an ad blocker, a network error) or `window.heedeo` can be missing (the script loads `async`). Never stop a signup in the browser for that: submit without a token. Your server still sends the event, which Heedeo scores on the email and the network, and asks for verification where your app has a step (section 3).

```js
async function onSubmit(event) {
  event.preventDefault();
  const form = new FormData(event.currentTarget);
  try {
    const id = await window.heedeo?.identify();
    if (id) form.set("heedeo_token", id.token);
  } catch {} // no token: submit anyway; the server still sends the event, and Heedeo scores it
  await signup(form); // your server action or fetch to your own endpoint
}
```

## Say who is signed in

When the person is signed in, pass who they are to `identify()`, in one line:

```js
const id = await window.heedeo?.identify({ userId: user.id, email: user.email });
```

Send the same `userId` you send as `user_id` on events, and the email if you have it; either is enough, and both are optional. With `@heedeo/sdk` it is `heedeo.identify({ userId, email })`.

What it does: look-alike devices (two identical Macs in one office, a team on one model of phone) can be hard to tell apart once a person clears their cookies. When the browser comes back signed in, Heedeo ranks first the stored browser on which that same account was used before, so the person is linked to their own browser rather than a colleague's.

What it never does:

* It never blocks or penalises. A browser that signs in to many accounts is exactly what Heedeo flags on events; here the account only helps pick among look-alikes. A browser that names no account, or another one, is matched as before.
* It never merges two browsers on its own: a merge still needs the device match and the network to agree, or an identity your server sends on an event. The page can be edited by anyone, so what it says only orders the matches the device already allows, and at most turns a look-alike that differs only by a browser version into a probable link, never a merge.
* It stores neither value. Both are hashed on arrival, as events' are, compared with the identities your events already recorded, and dropped. The observation keeps only which of the two were sent.

Call it with nothing when nobody is signed in, or on a signup page before the account exists; it works as before. A script cached from before this change ignores the argument.

Home shows a hint when no sign-in has reached Heedeo for 7 days: no `login` event and no `identify()` that said who was signed in.
