API docs

Version 1 · base URL https://honeyp.ai

Two steps, about 15 minutes.
  1. Add the browser snippet to your signup page and send its token with the form.
  2. From your server, call POST /v1/signups/assess after creating the account, and give free credits by the returned tier.
New accounts start in shadow mode: every signup is checked and shown in your dashboard, but the answer is always full credits until you switch to enforce.

Keys

Create and revoke keys in your dashboard under API keys.

1. The browser snippet

<script async src="https://honeyp.ai/v1/honeyp.js" data-key="pk_live_YOUR_PUBLISHABLE_KEY"></script>

On submit, ask for a token and send it to your server with the rest of the form. Guard the call: if the script didn't load, the signup must still work.

let honeypToken = null;
try { if (window.Honeyp) honeypToken = await Honeyp.token(); } catch (e) {}
// send honeypToken with the email and password

The snippet collects technical summaries of the signup page (counts and timings), never what people type, and stores nothing on the device. Use each token for one signup: tokens expire after 15 minutes, and a reused token counts against the signup.

2. Check a signup

POST /v1/signups/assess with your secret key, right after you create the account.

curl https://honeyp.ai/v1/signups/assess \
  -H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_id": "usr_123", "email": "new@user.com", "ip": "203.0.113.7",
       "user_agent": "Mozilla/5.0 ...", "client_token": "TOKEN_FROM_THE_BROWSER"}'
FieldRequiredNotes
account_idRecommendedYour ID for the new account (letters, digits, . _ : @ -, up to 128). Needed for labels and outcomes.
emailRecommendedHashed on arrival with a key unique to you; never stored in readable form.
ipRecommendedThe visitor's IP as your server saw it. Shortened to its network before storage.
user_agentRecommendedThe visitor's browser user agent.
client_tokenFor web signupsFrom Honeyp.token(). Omit for API or mobile signups and set signup_method.
signup_methodNoweb (default) or api for signups without a browser.
timezoneFor appsThe device's time zone name, for example Europe/Berlin. Web signups get this from the snippet automatically.
device_idFor appsYour app's own device identifier (for example the iOS identifier for vendor or an Android app-set ID), 8 to 200 characters. Hashed on arrival with a key unique to you. Web signups get this from the snippet automatically.
modeNoshadow to force shadow mode for this call.

Response

{
  "assessment_id": "asm_3f9c…",
  "mode": "enforce",
  "decision": "allow_reduced",
  "credit_tier": {"name": "reduced", "fraction": 0.2},
  "risk_score": 62,
  "confidence": "high",
  "reasons": [{"code": "…", "weight": 45, "detail": "Why this signup was flagged, in plain language"}, …],
  "recommended_step_up": ["email_verification", "passkey"],
  "degraded": false,
  "reasons_level": "full",
  "latency_ms": 3.1
}

Give the account your free credits × credit_tier.fraction. In shadow mode, decision is always allow and the real answer is in shadow_decision.

Base your code on decision and credit_tier: the explanation can be shorter. reasons_level says how much is included. During a self-serve trial it's limited: reasons without weights, and the score rounded to the nearest 10. Paid plans, and accounts we've set up with you, get full. Occasionally it's hidden for a while (no reasons or score); decisions are never affected.

When the trial's checks are used up, when the trial has ended without a paid plan, or if your website already belongs to another honeyp.ai account, we stop checking: you get "mode": "not_checked", decision allow with full credits, and the reason in not_checked. Your signups keep working; nothing is scored or stored. Paid plans are never stopped for going over (that's billed as overage).

Decisions

DecisionCreditsSuggested action
allowFullNothing: a real person.
allow_monitorFullWatch the first 72 hours (send events, below).
allow_reducedReduced (20% by default)Unlock the rest after email verification or a passkey.
reviewHeldHold credits until someone on your team looks; a review task can be created automatically.
blockNoneNo free value. Offer a way to contact you.

Thresholds and the reduced fraction are set per customer: tell us what you'd like and we'll change them (dashboard controls are coming).

Let people ask for a review

When you limit an account, link the person to the appeal page with the check's assessment_id:

https://honeyp.ai/appeal?a=asm_3f9c…

Their email and message go straight to your team by email (and to Slack, Teams or your phone if connected); honeyp.ai keeps no copy. If they're right, label the account legit and report the outcome appeal_upheld. Offering a review route is required by our terms and expected under GDPR.

Fail open

Use a short timeout (we suggest 800 ms) and give full credits if honeyp.ai can't be reached. An outage on our side should never block a real user.

const res = await fetch("https://honeyp.ai/v1/signups/assess", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.HONEYP_SECRET_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ account_id: user.id, email, ip: req.ip,
                         user_agent: req.headers["user-agent"], client_token: honeypToken }),
  signal: AbortSignal.timeout(800),
}).then((r) => r.json()).catch(() => null);
const fraction = res?.credit_tier?.fraction ?? 1;   // unreachable: full credits
await grantCredits(user, FREE_CREDITS * fraction);

3. Tell us what happened (improves accuracy)

POST /v1/labels

Mark an account as abuse or legitimate. Labels tune the decisions for your signups.

{"account_id": "usr_123", "label": "abuse"}      // or "legit"

POST /v1/accounts/outcome

Report what happened later. kind is one of converted, credits_exhausted, active_7d, refunded, chargeback, banned, appeal_upheld; value is an optional amount.

{"account_id": "usr_123", "kind": "converted", "value": 20}

POST /v1/accounts/usage (first 72 hours)

Farmed accounts often look human at signup but burn their free credits within minutes, in waves from one network. Report usage, ideally when an account runs out, and we'll say whether to hold what's left.

{"account_id": "usr_123", "credits_used": 100, "credits_total": 100}
→ {"recommendation": "hold_remaining", "reasons": [{"code": "usage.fast_burn", …}, {"code": "usage.burn_wave", …}],
   "minutes_since_signup": 6.2, "share_used": 1.0}

recommendation is ok, watch or hold_remaining. It's advice: nothing is blocked. After 72 hours the answer is always ok with "window": "closed".

POST /v1/accounts/events

Farms run a script after signup: the same steps at the same moments. Send a few of your own events per account as they happen (for example first_project, used_credits, invited_friend): short names of letters, digits and _ . : -, never personal data. Once an account has three steps after its first 10 seconds, we compare their timing with your other recent signups. People doing the same steps take their own time; accounts that match to within a few seconds are in lockstep.

{"account_id": "usr_123", "event": "first_project"}
→ {"recommendation": "hold_remaining", "reasons": [{"code": "events.lockstep", …}],
   "matching_accounts": ["usr_118", "usr_119", "usr_121"], "events_recorded": 3}

at (Unix seconds) is optional, for events you send later. Up to 50 events are kept per account and follow your retention setting. Lockstep accounts also appear together on the dashboard's Farms card.

Child accounts

If your service has child accounts, such as profiles a parent creates inside a family account, check them through the adult they belong to. We receive nothing about the child. You decide who is a child, from your own age check or a platform's signal such as Apple Family Sharing, Google Play age signals or an Amazon household profile. honeyp.ai never estimates anyone's age.

POST /v1/signups/assess
{"account_type": "child", "parent_account_id": "u_123", "source": "apple_family"}

Scan existing accounts inside your network

Accounts that signed up before honeyp.ai was protecting you can be scanned without exporting anything. The honeyp.ai scanner is a container you run on your own servers. It reads your accounts with one read-only query against your database (PostgreSQL, MySQL, Snowflake, BigQuery or SQLite) or a CSV file, pseudonymises each account on that machine (salted fingerprints, networks and email domains), has honeyp.ai analyse those rows without keeping them, and writes the report there. Emails, IP addresses and account IDs never leave your network.

docker run --rm -v "$PWD/results:/out" \
  -e HONEYP_SOURCE="postgresql://readonly_user@db.internal:5432/app?sslmode=require" \
  -e HONEYP_DB_PASSWORD -e HONEYP_SECRET_KEY -e HONEYP_QUERY_FILE=/out/accounts.sql \
  honeyp-scanner scan

Your query returns one row per account: account_id and created_at, plus email, ip, credits_used and converted if you have them. Run check instead of scan to test the connection first.

To see the results in your dashboard, add --upload. Only what's in upload.json is sent: counts, reasons, and the flagged accounts' IDs, scores and email domains. Review that file before you turn uploads on. The scanner comes with the Growth and Enterprise plans (and the free trial), on request: email support@honeyp.ai and we'll send it with a setup guide for your security team.

Reading your data

Privacy requests

Errors

StatusMeaning
400The body isn't valid JSON.
401Missing, wrong or revoked secret key.
413The request is too large.
422A field is invalid; detail says which.
5xxOur side: fail open and carry on.

Errors look like {"error": "invalid_account_id", "detail": "…"}.

Node.js and Next.js package

@honeyp/node wraps the API with a short timeout and fails open. No dependencies; Node 18+. Until it's on npm, design partners get it from us directly.

import { Honeyp, requestInfo } from "@honeyp/node";

const honeyp = new Honeyp({ secretKey: process.env.HONEYP_SECRET_KEY });   // sk_live_..., server only

const result = await honeyp.assess({ accountId: user.id, email, clientToken, ...requestInfo(req) });
await grantCredits(user, honeyp.credits(result, 100));                       // 100, 20 or 0 by default

Call assess once per new account. A second call for the same person looks like a repeat signup.

Guides

Next.js (App Router)

// app/api/signup/route.js
import { Honeyp, requestInfo } from "@honeyp/node";
const honeyp = new Honeyp({ secretKey: process.env.HONEYP_SECRET_KEY });

export async function POST(req) {
  const { email, password, clientToken } = await req.json();
  const user = await createUser(email, password);                       // your existing signup
  const result = await honeyp.assess({ accountId: user.id, email, clientToken, ...requestInfo(req) });
  await grantCredits(user.id, honeyp.credits(result, 100));
  return Response.json({ ok: true, credits: honeyp.credits(result, 100), verifyToUnlock: result.decision === "allow_reduced" });
}

Supabase

The Before User Created auth hook runs before the account exists. Pass the browser token in the sign-up metadata, then point the hook at an Edge Function.

await supabase.auth.signUp({ email, password, options: { data: { honeyp_token: clientToken } } });
// supabase/functions/honeyp-check/index.ts  (verify the hook's signature as Supabase documents)
import { Honeyp } from "npm:@honeyp/node";
const honeyp = new Honeyp({ secretKey: Deno.env.get("HONEYP_SECRET_KEY") });

Deno.serve(async (req) => {
  const { user, metadata } = await req.json();
  const result = await honeyp.assess({
    accountId: user.id, email: user.email, ip: metadata?.ip_address,
    clientToken: user.user_metadata?.honeyp_token, signupMethod: user.user_metadata?.honeyp_token ? "web" : "api",
  });
  if (result.decision === "block") {
    return Response.json({ error: { http_code: 403, message: "We couldn't create this account. Contact support." } });
  }
  // Save the tier for when you grant credits, e.g. in a honeyp_decisions table, using the service role key.
  await saveDecision(user.id, honeyp.credits(result, 100));
  return Response.json({});
});

Check the hook's payload fields against Supabase's current documentation.

Firebase

A blocking function runs before the user is created and can set custom claims. Firebase's client SDKs can't pass the browser token in, so email, network and browser checks apply without the snippet.

import { beforeUserCreated, HttpsError } from "firebase-functions/v2/identity";
import { Honeyp } from "@honeyp/node";
const honeyp = new Honeyp({ secretKey: process.env.HONEYP_SECRET_KEY });

export const honeypCheck = beforeUserCreated(async (event) => {
  const user = event.data;
  const result = await honeyp.assess({ accountId: user.uid, email: user.email, ip: event.ipAddress,
                                       userAgent: event.userAgent, signupMethod: "api" });
  if (result.decision === "block") throw new HttpsError("permission-denied", "We couldn't create this account.");
  return { customClaims: { free_credits: honeyp.credits(result, 100) } };
});

Clerk

Clerk has no hook before an account is created, so check right after sign-up from your own route and keep the credits in Clerk metadata. The free_credits check makes it safe to call twice.

// app/api/after-signup/route.js  (call it from the page once Clerk's sign-up completes, with the browser token)
import { auth, clerkClient } from "@clerk/nextjs/server";
import { Honeyp, requestInfo } from "@honeyp/node";
const honeyp = new Honeyp({ secretKey: process.env.HONEYP_SECRET_KEY });

export async function POST(req) {
  const { userId } = await auth();
  if (!userId) return new Response("Not signed in", { status: 401 });
  const client = await clerkClient();
  const user = await client.users.getUser(userId);
  if (user.publicMetadata.free_credits !== undefined) return Response.json({ ok: true });   // checked already
  const { clientToken } = await req.json();
  const result = await honeyp.assess({ accountId: userId, email: user.primaryEmailAddress?.emailAddress,
                                       clientToken, ...requestInfo(req) });
  await client.users.updateUserMetadata(userId, { publicMetadata: { free_credits: honeyp.credits(result, 100),
                                                                    honeyp_decision: result.decision } });
  return Response.json({ ok: true });
}

A Python client (HoneypClient) ships with the platform too.

Need a hand? Design partners get a setup session with us. Write to support@honeyp.ai or apply here.