1. Quick start
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):
<script src="https://heedeo.dev/i.js?key=pk_YOUR_PUBLISHABLE_KEY" async></script>Or with the package (see the package quickstart):
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):
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):
const verdict = await heedeo.evaluate({ token, action: "signup", email });
if (verdict.flagged) return refuse(); // section 5: what a caught signup meetsverdict.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>):
<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
tokenthat 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).
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:
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.