Docs

Install Halo in 5 minutes.

Halo is a single HTTP endpoint. Call it from your signup route and gate free-trial access on the response.

1. Get an API key

Create a workspace from the dashboard. You'll get a key that looks like hlo_live_.... Store it as a server-side environment variable.

2. Call the /check endpoint on signup

Send whatever you have. All fields are optional but more signal = higher accuracy.

curl -X POST https://halo.dev/api/public/v1/check \
  -H "x-api-key: hlo_live_..." \
  -H "content-type: application/json" \
  -d '{
    "email": "j.smith+trial@gmail.com",
    "phone": "+1 (415) 555-0134",
    "ip": "203.0.113.42",
    "device_fingerprint": "d3a1...",
    "user_agent": "Mozilla/5.0 ...",
    "external_id": "user_9182"
  }'

3. Read the verdict

Halo returns a decision, a stable cluster id, a confidence score (0–100), and the exact signals that fired.

{
  "identity_id": "9c1e...",
  "cluster_id": "a3f2...",
  "cluster_size": 3,
  "confidence": 92,
  "verdict": "flagged",
  "decision": "shadow_logged",
  "mode": "shadow",
  "threshold": 60,
  "signals": ["email_stem", "datacenter_ip", "velocity_burst"],
  "matched_identity_ids": ["b12d...", "44ff..."],
  "value_at_risk_usd": 40,
  "email_type": "disposable",
  "ip_type": "datacenter",
  "phone_type": "voip",
  "velocity": { "last_hour": 4, "last_24h": 11, "distinct_emails_24h": 9 },
  "reasons": [
    "Sequential alias of an existing account (jsmith1 -> jsmith3)",
    "IP belongs to a hosting provider (DigitalOcean), not a home network",
    "4 linked signups in the last hour"
  ]
}
  • clear — no linked accounts. Let them in.
  • review — some signal overlap. Consider soft challenges (email verify, captcha).
  • flagged — high-confidence duplicate. Deny the free trial or route to a paid plan.

4. Segment endpoints (recommended)

Two thin wrappers call /check under the hood and apply your segment's business rule, so your code reads one boolean instead of interpreting a score.

E-commerce — promo code redemption:

curl -X POST https://halo.dev/api/public/v1/promo/verify \
  -H "x-api-key: hlo_live_..." -H "content-type: application/json" \
  -d '{ "email": "j.smith+bf@gmail.com", "device_fingerprint": "d3a1...",
        "promo_code": "WELCOME20", "order_value_usd": 82, "discount_usd": 16 }'

# -> { "allow_promo": false, "confidence": 92, "reason": "This device already
#      redeemed under 2 other identities." }

SaaS — free trial start:

curl -X POST https://halo.dev/api/public/v1/trial/verify \
  -H "x-api-key: hlo_live_..." -H "content-type: application/json" \
  -d '{ "email": "j.smith+2@gmail.com", "device_fingerprint": "d3a1...",
        "plan": "pro-trial", "trial_cost_usd": 40 }'

# -> { "allow_trial": false, "confidence": 88, "reason": "This person already
#      started a trial under 1 other identity." }

5. Shadow mode first

New workspaces start in shadow mode: every event is scored and logged, and allow_promo / allow_trial always return true. Nothing touches revenue. After a week you'll have a measured dollar-recovery number on your dashboard — flip to active blocking and set your confidence threshold when the false-positive rate looks right to you.

6. Browser SDK (device telemetry)

Drop in edgeid.js to collect hardware telemetry a fresh email can't fake. It's ~4KB, dependency-free, runs entirely client-side and never sends raw pixels or audio — only stable hashes.

<script src="https://halo.dev/edgeid.js"></script>
<script>
  const edge = EdgeID.init({ endpoint: "https://halo.dev", apiKey: "hlo_pub_..." });

  // On signup submit — signals are collected and attached automatically.
  const res = await edge.verifyTrial({ email: form.email, plan: "pro-trial" });
  if (!res.allow_trial) showPaidPlans(res.reason);
</script>

Prefer to call the API from your server? Collect on the client and forward the object as device_signals on any endpoint.

await EdgeID.collect()
// {
//   device_fingerprint: "dfp_1f9a2c31",
//   canvas_hash: "8ab31c02",   // 2D text + shape rasterization
//   webgl_hash:  "42f0cd19",   // GPU vendor, renderer, extensions
//   audio_hash:  "c71b0e55",   // OfflineAudioContext compressor output
//   fonts_hash:  "9de4a108",   // installed-font probe
//   screen: "1512x982x30x2", timezone: "America/Los_Angeles",
//   platform: "macOS", hardware_concurrency: 10, device_memory: 8
// }

When two or more of canvas / WebGL / audio / fonts agree with a prior signup, the hardware signal fires — this is what catches incognito windows, cleared cookies and rotated fingerprint cookies. Every probe degrades safely: blocked APIs return null instead of throwing.

7. Signals reference

SignalMeaning
email_normalizedSame email after stripping dots & +aliases, gmail↔googlemail collapse.
phoneSame phone after canonicalization.
deviceSame device fingerprint hash.
hardware2+ of canvas / WebGL / audio / font telemetry match — same physical machine.
ipSame source IP within your workspace.
name_similarSame normalized display name.
voip_phoneBurner line: VoIP / forwarding, toll-free, premium-rate or synthetic digit pattern. Every response also carries phone_type and phone_risk_reasons.
name_fuzzyDisplay name one or two characters off an existing account — 'Jame Smith' vs 'James Smith'.
email_stemNumbered alias family: jsmith1, jsmith2, jsmith3 on the same domain.
disposable_emailThrowaway inbox provider (mailinator, temp-mail, 10minutemail and ~90 more, subdomains included).
generated_emailMachine-generated local part: high entropy, heavy digit ratio, no human structure.
ip_subnet_clusterDifferent exit IPs inside one /24 (or /64) — residential proxy pool or one household.
datacenter_ipSource IP is cloud/hosting space (AWS, GCP, DigitalOcean, Hetzner, OVH…), not a consumer ISP.
proxy_ipKnown commercial VPN pool or Tor exit range.
velocity_burstSignup rate spike on the same device, subnet or alias family — 3+ in an hour or 4+ distinct emails a day.

8. Alert routing

Alerts are deduplicated by kind, severity and direction inside your dedupe window, capped per rolling hour (criticals always keep their slot), and anything at or above your escalation severity is routed to a separate on-call webhook. Suppressed and rate-limited alerts are still recorded in the console, so you can see everything the sweep noticed without being paged for it. Configure all four controls under Dashboard → Slack alerts.

9. EU consent gate

The browser SDK never touches canvas, WebGL, audio or font APIs until consent is resolved. In EU/EEA + UK locales it starts in pending and only sends minimal, non-telemetry context; everywhere else it collects immediately. Your workspace policy (EU only, everywhere, off) is enforced server-side too, using edge geo.

const edge = EdgeID.init({
  endpoint: "https://halo.dev",
  apiKey: "hlo_pub_...",
  consent: "auto" // "auto" (EU-aware, default) | "always" | "never"
});

edge.consent();        // { required: true, state: "pending" }

// From your cookie banner:
acceptBtn.onclick = () => edge.grantConsent();   // telemetry starts now
rejectBtn.onclick = () => edge.denyConsent();    // telemetry stays off

// Deferred requests still score on server-side signals:
// { "telemetry_deferred": true, "consent_granted": false, ... }

Shadow mode keeps working while telemetry is deferred — email normalization, phone canonicalization and IP linkage all run server-side. Consent state is stored per browser and persists across sessions.

10. Calibration & active mode

Label flagged events in the dashboard as confirmed abuse or false positive. The calibration harness sweeps every threshold 0–100 against that labeled set, reports the false-positive rate, recall and precision at each, and recommends the lowest threshold that keeps false positives under 5%. Active blocking is locked until the harness has at least 20 labeled events across both classes and a passing threshold exists.

11. Alerts & data lifecycle

Add a Slack incoming-webhook URL in the dashboard and EdgeID pages your team within five minutes of a flag spike (2x baseline), a ±50% swing in block or allow volume, or the measured false-positive rate drifting above the 5% goal. Repeat alerts of the same kind are suppressed for six hours.

Retention runs daily: at your window (90 days by default) emails, phones, names, IPs, user agents, device signals and every derived fingerprint hash are irreversibly stripped and the row is marked anonymized; at twice the window the row, its orphaned cluster and the alert history are deleted outright.

Rate limits

10,000 free checks per month. Contact hello@halo.dev for higher volume.