Skip to content
Heedeo

7. Privacy: purpose, footing and what Heedeo never does

Purpose

Heedeo processes device and account evidence for one purpose: to secure the authentication and signup your users ask you for, and to prevent fraud on your service. It does so for you alone: every identifier is hashed separately for your account, nothing is shared between customers or compared across them, nothing is used for advertising, and nothing is sold.

Where that stands

This is how Heedeo is built to fit each regime; it is not legal advice, and your counsel decides your own footing.

  • EU. Reading the browser and setting the visitor cookie is storage and access on the user’s device (the ePrivacy Directive, Article 5(3)). Heedeo’s footing is the exemption for what is strictly necessary to provide the service the user asked for, here the security of the signup or login they started; fraud prevention beyond that rests on legitimate interest under the GDPR (Article 6(1)(f)), which Recital 47 names for preventing fraud. Your page calls identify() only at an action you send to events, and the EU collection profile (below) leaves out the readings that do most to single out a machine.
  • UK. PECR has the same rule for storage and access, and the Data (Use and Access) Act 2025 adds to it an exception for what is strictly necessary to prevent or detect fraud; the rest rests on legitimate interests under the UK GDPR, as in the EU.
  • US. Under the CCPA, as the CPRA amended it, helping to ensure security and integrity and to detect and prevent fraud is a business purpose; Heedeo processes the data for you as your service provider, and never sells or shares it (no cross-context behavioural advertising). The state privacy acts (Virginia, Colorado, Connecticut, Texas and the others) exempt processing to prevent, detect or respond to fraud and security incidents from their opt-outs.

What Heedeo never does

Heedeo never blocks anyone: it answers what it saw and what it suggests, and your code decides on every event. It sets no caps and no rate limits on what your end users do; counts are facts for your rule. It never flags an agent for being an agent: a declared or verified agent’s automation and virtual machine are recorded, never reasons; only a claim nothing backs or a sudden crowd is. Automation with no agent declared weighs 10, which alone never reaches verify. And it never does these:

  • Personal data through the browser. The script reads the browser and nothing else. Emails, phone numbers, account ids, principals and card fingerprints reach Heedeo only from your server, server to server.
  • A cross-customer graph. Every hash is made separately for your account, so the same email at two customers is two unrelated hashes; no link, count, label or score crosses customers, and there is no shared reputation.
  • Raw addresses past the request. The client IP and the email address are used while the event is answered (the IP to read its network and to ask IPinfo and Spur about it, the address to hash it, and to ask Have I Been Pwned only if you turned breach history on), then kept only as below.
  • Fingerprinting in a native app. There is no mobile SDK: Apple forbids fingerprinting in apps whatever the consent. From an app, send events with the evidence your backend holds.
  • Selling or sharing visitor data. Not to anyone, not for advertising, not for another customer’s benefit.
  • A competitor’s device intelligence as an input. Fingerprint, Stytch, Castle and the like are never read. A paid email or phone service’s results reach Heedeo only as evidence you send (section 3).

What Heedeo keeps, and for how long

Heedeo never stores a raw email address, only one-way hashes of it (of its normalised form, and of the address as typed when that differs). It never stores a raw network address, only its approximate network (the /24, or the /48 for IPv6) and one-way hashes of it. Of the headers you send, only the user agent, the languages and the client hints are kept; the rest are read and discarded. The email’s domain, never the address, is kept where a reason names it and with the domain lookups below. What has expired is deleted every hour.

WhatHoldsKept
Identificationsthe browser readings the script sent (under the EU profile, without canvas, audio, fonts, speech voices, text and emoji measurements, media formats and the keyboard layout), the user agent, languages and client hints, the approximate network, what the readings showed of the browser (such as a private window, a virtual machine, an automation tool or a disguised browser), how many times the person used the pointer, the keyboard and the touch screen before the action, and how Heedeo recognised it; when Heedeo is served through the Domains page, or through the @heedeo/sdk proxy on Vercel or in a Cloudflare Worker, how the browser connected: its encryption (with the size and one-way hashes of the settings it offered, among them the JA4 hash Vercel and Cloudflare Enterprise compute), protocol and round trip, and the city of the Cloudflare data centre that answered90 days
Eventsone-way hashes of the email, the account, the principal and the email’s pattern; the approximate network and a one-way hash of the address; the address’s country, region and city, and what IPinfo and Spur said about its network; the user agent, languages and client hints; the evidence you sent, phone and card hashed; the answer30 days; an event a flag points to stays with its flag
Visitorsthe visitor cookie’s id, first and last seen, the readings Heedeo recognises each browser by (most hashed); the person’s browsers, the ids merges retired, and what it did of each action (counts, first and last)no automatic expiry
Actionsper event: the action, its browser and visitor, when, the band the answer suggested, your outcome on it, and the kind of network (home, business, hosting, carrier, relay, Tor)13 months
Identities a browser presentedper browser, one-way hashes of each email, account, principal, phone and card it presented, when first and lastno automatic expiry
Links between browserstwo of your browsers, what they share (as a hash) or that they are probably the same device, and how strongly that links thema shared account, email, principal, phone or card, and probable matches: no automatic expiry; a shared address: 7 days after it was last seen
Merge historywhich visitor each browser belonged to and from when to when, and what joined it (its own cookie, a shared identity as its hash, or a device match and its confidence); the operator who pressed Not them; what later confirmed or contradicted a matchno automatic expiry
Flags, labels and outcomesthe reasons, your notes, your labels (an email as its hash, a network as its /24 or /48), outcomes (an account as its hash), and each decision on a flag with the outcomes it replaced, so it can be undoneno automatic expiry
Phone and card hashesthe phone and card hasheswith their event (30 days), and as long as the identities and links they made
Network lookupsIPinfo’s answer for an address, with the address hashed7 days; 1 day when it says residential proxy
Spur lookupsSpur’s answer for an address, with the address hashed1 day
Mail-server lookupsthe email domain’s mail servers24 hours
Domain-age lookupswhen the email’s domain was registered30 days
Breach lookupsHave I Been Pwned’s earliest breach for an address, with the address hashed30 days
Request idseach request_id of a label write or a report (POST /v1/lists, POST /v1/outcomes), with the write and its answer, so a retry is answered as the first attempt was; once you delete the person a report was on, a one-way hash of its id alone, unless the report’s browser is another of your visitors’ by then (an account they hold too, or the side a split left them): that report stays with them, and so do its id and its answer, which then names them72 hours from the write
Webhook deliverieseach message sent to your endpoints (section 12), as sent, and of your server’s last answer its status code, the time it took and its first 1,000 characters30 days; deleting an endpoint deletes its deliveries

A webhook message holds ids and labels as the API answers them, and the event’s answer without its reasons’ text: never a raw email or network address, a person’s name or an operator’s email. What your server answers is kept as it answered, so leave personal data out of your webhook answers.

A visitor’s profile (its browsers, identities and the counts of what it did) comes from the records above. Undoing a merge (Not them) gives each side its own counts again: Heedeo recounts exactly what happened within the 13 months actions are kept, and nothing older.

Merges

Heedeo decides who is one person, explains it and can undo it; your code stores nothing.

How. Two browsers become one visitor when they present the same account, or the same email and both verified it (evidence.email.verified on an event of each that presented it). An email that is only typed joins no one: anyone can type an address, so it is a weak link in related that lends 50 at most. Merge on, under Settings, Advanced, Weights, chooses which shared identities merge; phone and card can be turned on, though a shared card or phone is also a household’s. Two browsers also become one visitor when Heedeo recognises them as the same device, with confidence at or above your merge threshold. A device match is never certain, so its confidence stays below 1, and people whose machines only look alike (one laptop model across a class, two in a household) stay separate visitors. A shared network alone never merges anyone: a shared address is a link in related, not a merge. No merge crosses customers.

The threshold is 0.86 by default (Settings, Advanced, Weights, Merge threshold, 0.70 to 0.95): how sure Heedeo must be before it counts two browsers as one visitor. Lower catches more repeat signups, and joins more people who are not the same person; the default keeps look-alike devices apart. Raising it splits nothing already merged. Merge reliability (Settings, Advanced, Weights) shows the device merges of the last 90 days by confidence, how many were later confirmed or contradicted, and the precision; a threshold is safe when the precision in its row is at or above it.

The alias. The older visitor keeps its id; the other id becomes an alias that resolves to it forever, listed in visitor.aliases and answered with merged_into, so an id you stored keeps working. A merge never rewrites your events or actions: each keeps the browser and the visitor it had when it was written. A visitor has one outcome of yours, the latest: when both sides of a merge hold one, the newer stands. What joined each browser is shown on the visitor page in words (“same email”, “device match 0.95”) and in visitor.browsers[].evidence, never as a hash.

The undo. A device match can be wrong (two people on one model of phone), so on the person page an operator can press Not them on a browser a device match placed. It becomes a visitor of its own again, each side gets its own counts of the last 13 months back, and no later match puts the two back together. The id stays with the side that presented an email, account, phone or card (or, when both or neither did, the side seen most recently), and an id a merge retired goes with its own browser. Not them refuses while the person has a label, because which side it meant is your call: clear the label, press Not them, then label the right person. An outcome on the visitor stays with the side holding the browser it was reported on, under that side’s id, and the other side goes clean. From then on neither side is listed in the other’s related, or lends it anything. A merge by a shared email or account is not split; turning its kind off under Merge on stops future merges of that kind and splits nothing.

Deleting a person

When someone asks you to delete their data, DELETE /v1/visitors/<visitor_id> with the secret key removes everything Heedeo holds of that person for you, all at once. An id a merge retired deletes the visitor it joined. The answer is the visitor’s id and how many records of each kind were deleted. The answer also says when, as deleted_at. The call is safe to send again: an id you deleted before answers 200 with that id, an empty deleted and the first deleted_at, so a deletion whose answer you lost ends the same way on a second try. An id Heedeo never held for you (a typo, another workspace’s, another kind of id) answers 404, and one of your event ids answers 400 invalid_request saying to send its visitor_id: a deletion request sent with the wrong id never reads as done. Check the status, not only that the call returned.

curl -s -X DELETE -H "authorization: Bearer $HEEDEO_SECRET_KEY" https://app.heedeo.com/v1/visitors/8855…
# {"id":"8855…","deleted_at":"2026-11-11T08:25:01.069+00:00","deleted":{"flag_decisions":1,"flags":1,"actions":12,"outcomes":1,"outcomes_folded":0,"merge_outcomes":0,"event_requests":0,"write_requests":0,"events":12,"farm_group_members":0,"farm_group_pairs":0,"observations":3,"browser_links":4,"browser_edges":2,"browser_visitors":3,"visitor_alias":1,"browsers":2,"visitors":1}}
DeletedWhat it held of the person
flag_decisionsthe decisions on their flags, with the outcomes those decisions replaced; a decision that also covered someone else’s flag stays, without anything of theirs
flagsthe flags on their browsers and events, and a related flag on another browser that points at one of their events
actionswhat they did, 13 months of it
outcomesthe outcomes you reported on their events, their visitor and the accounts only they presented
outcomes_foldedearlier reports on them that a later one replaced when two of their visitors became one
merge_outcomeswhat later confirmed or contradicted the merges of their browsers and into their visitor
event_requeststhe request ids a repeat of their events was answered under
write_requeststhe request ids of your reports on them (POST /v1/outcomes), with the answers kept for a repeat. What stays of each, until 72 hours after the report, is a one-way hash of the id and nothing of the report or of them, so a retry is refused (409) and the report is not written again. A report on an account another of your visitors holds too, or on a browser a split left with another visitor, is not theirs alone: it stays, and its repeat names that visitor
eventstheir events, with the hashes, evidence and answers stored on them, and events with no browser that carry an identity only they presented; GET /v1/evaluations/:id then answers 404 for each
farm_group_memberstheir sign-ups’ places in the nightly farm groups (record-only, for review)
farm_group_pairsthe record-only operator notes pairing one of their browsers with another of a farm group
observationstheir identifications
browser_linksthe hashes of every email, account, principal, phone and card their browsers presented
browser_edgesevery link between one of their browsers and any other, either way
browser_visitorswhich visitor each of their browsers belonged to, and what joined it
visitor_aliasthe ids merges retired into the visitor
browserstheir browsers, with the readings Heedeo recognised them by
visitorsthe visitor and its profile

Your own labels under lists (a visitor id, an email hash or a network you marked block or allow) are kept: they are yours, and a label on an id you block should outlast the data behind it. So are the request ids of your label writes, for their 72 hours. A report on an account another person holds too moves to that person and stays, with its request id, whose repeat then names them; so does a report whose browser a split left with another visitor. Another person’s browser keeps its own records, even where they carry this visitor’s id: from a time the two were one visitor (a browser split off this visitor keeps the events it had while in it), or because an answer about that other person named this visitor as related or as a possible match. A lookup of that id answers 404 from then on. Daily usage counts hold no identifier and stay. So does a record that the person was deleted: their id and each retired one, with the time, and nothing else of them, so a deletion sent again can answer that it was done. A browser of theirs that comes back afterwards is identified afresh, with nothing of what was deleted to match it against. A visitor that is not yours answers 404, and nothing of the other workspace’s is touched, as does what is no visitor id at all. With the package: await heedeo.deleteVisitor(visitorId), the same { data, error } shape, tried a second time on a 5xx or a network error from 0.2.0-beta.1.

A few rows name no browser: an event sent after its token expired, and a report on a user_id that came before any browser presented it. The delete reaches them through the email, account, phone or card they carry, when only this person’s browsers presented it: such an event goes, with its flags, its outcomes and its delivery log messages, and so does such a report. It cannot reach:

  • rows that carry only an identity another of your visitors also presented, such as a login two people share: they could be either person’s, so they stay, and a report on that account stays with the other visitor;
  • rows that carry an identity this person’s browsers never presented: nothing ties them to this person;
  • someone with no visitor id, because you only ever sent events from your backend: there is no visitor to delete;
  • an event you send after the delete, on a token issued before it: it is a new event, with no browser.

Deleting a person also removes, at once, the webhook messages about them that were delivered or failed from your endpoints’ delivery logs, and sends visitor.deleted (section 12) to every endpoint that listens for it, so your app can delete its side too. That message, and any message about them still being sent at that moment, stay in the log until their 30 days are up.

Collection: the full and EU profiles

Under Settings, Advanced, Collection you choose what the script reads. Full, the default, is every source: screen, GPU, the fonts installed, how the browser draws canvas graphics and processes audio, how it measures text and emoji, the speech voices it offers, the audio and video formats it says it can play and record, the keyboard layout where the browser shares it, language and time-zone settings, the browser’s own description of itself, whether an automation tool or the browser’s developer tools are attached to the page, how many times the person moved or clicked the pointer, pressed a key or touched the screen on the page before the action (counts only: never which keys, what was typed or where), and signs that a script disguises the browser: which graphics card a background worker uses, whether the browser’s built-in functions were replaced, and whether the window size and pixel density the page reports are the browser’s own. EU leaves out canvas, audio, fonts, speech voices, text and emoji measurements, media formats and the keyboard layout: the script never reads them. For up to an hour after you switch, a browser that cached the earlier script may still send them, and Heedeo discards them. Switching to EU also removes what Heedeo kept of them, and Heedeo no longer uses them for any visitor. On a large workspace the removal finishes in the background: Settings says it is removing until it is done, and you can choose Full again after that. Under EU, a returning browser is still the same visitor, and browsers are still related by the email, account, phone or card they presented, but Heedeo recognises a browser that cleared its cookies and storage less reliably than under Full. Both profiles read whether an automation tool or developer tools are attached and count the person’s input: these say how the page was used and single out no one. Both also check for a disguise: that says how the browser is set up, not who uses it, and the graphics card is one both profiles already read.

Under both profiles, Heedeo keeps the visitor’s id on the device: in a first-party cookie, _idd, that identify sets for 400 days, with copies the script keeps in the browser’s localStorage and IndexedDB.

For your privacy policy

Add a line like this, naming what you send (leave out the fields you do not send): “To prevent fraud and abuse, and to secure the signup and sign-in you ask us for, we measure technical characteristics of your browser when you sign up or sign in (such as screen, installed fonts and speech voices, language settings and keyboard layout, the media formats it supports, and how it renders text, canvas graphics and audio) through our provider Heedeo, which uses them for that purpose only, for us alone, and never for advertising or sale; your email address and your network address are kept only as one-way hashes, with the network address also as its approximate network. We also share with Heedeo whether your email address is verified and when; your phone number, whether it is verified, its line type and its country; your sign-in provider and what it says of your account (whether your email is verified, your organisation’s domain, Apple’s real-user status, whether the address is a private relay, when the account was created and whether two-factor authentication is on); when your account with us was created, its plan and since when it has been paid; your payment card’s fingerprint, funding type, card verification result, wallet, 3-D Secure result and country, and our payment provider’s risk levels; and the kind, result and time of identity checks we run. Your phone number and card fingerprint are kept only as one-way hashes.” Under the EU profile, write “(such as screen, language settings and how the browser describes itself)” instead of the first parenthesis.

If you turn on breach history, add: “Heedeo checks your email address with Have I Been Pwned to see whether it appeared in known data breaches; the address is not stored.” The email’s domain (never the address) is looked up in the public domain registries to learn its age and in public DNS (Cloudflare’s, then Google’s) for its mail servers; where Heedeo’s server holds the keys, your network address is looked up at IPinfo and at Spur, under both collection profiles.

What Heedeo keeps of each evidence field, with the event, for 30 days (section 3); the two hashes also stay as long as the identities and links they made (above):

Evidence fieldWhat Heedeo keeps
email.verifiedas sent
email.verified_atas sent
phone.numberonly a one-way hash made for your account, never the number
phone.hashyour hash, hashed again for your account
phone.verifiedas sent
phone.line_typeas sent
phone.countryas sent
oauth.provideras sent
oauth.email_verifiedas sent
oauth.hosted_domainas sent (a domain, never an address)
oauth.real_user_statusas sent
oauth.private_relayas sent
oauth.account_created_atas sent
oauth.two_factoras sent
account.created_atas sent
account.planas sent
account.paid_sinceas sent
payment.fundingas sent
payment.card_fingerprintonly a one-way hash made for your account, never the fingerprint
payment.cvc_checkas sent
payment.walletas sent
payment.three_dsas sent
payment.countryas sent
payment.radar_risk_levelas sent
payment.free_trial_abuse_riskas sent
verification.kindas sent
verification.resultas sent
verification.atas sent