AbuseGraphDocs
Guides

AbuseGraph + Clerk

Score Clerk signups and logins via webhooks — store browser SDK signals in unsafe_metadata for a full score.

Quick path

  1. Add sk_test_…, pk_test_…, and ABUSEGRAPH_SITE to env.
  2. On signup/login UI, run the SDK and store toCheckBody in unsafe_metadata.abusegraph.
  3. Handle user.created and session.created webhooks with createCheckClient.

Keys

KeyEnvUse
SecretABUSEGRAPH_SECRET_KEY (sk_test_… / sk_live_…)Server /api/v1/check
PublishableNEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY (pk_test_…)Browser SDK
SiteABUSEGRAPH_SITEDNS-verified domain

Free includes 1,000 live API checks / month after DNS verify. Test keys never bill.

Env

ABUSEGRAPH_SITE=yourdomain.com
ABUSEGRAPH_CHECK_URL=https://abusegraph.com/api/v1/check
ABUSEGRAPH_SECRET_KEY=sk_test_…
ABUSEGRAPH_SECRET_KEY_TEST=sk_test_…
NEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY=pk_test_…

Use sk_live_… / pk_live_… in production after DNS verify.

Server check

Use @abusegraph/server createCheckClient. Events: signup | login | password_reset | email_change. Optional sessionId.

import { createCheckClient } from "@abusegraph/server"

const abusegraph = createCheckClient({
  secretKey: process.env.ABUSEGRAPH_SECRET_KEY!, // sk_test_…
  site: process.env.ABUSEGRAPH_SITE!,
})

init + collect + evaluate + toCheckBody — NEVER bare sdk.check() alone as the authoritative decision.

import { init } from "@abusegraph/sdk"

const sdk = init({
  publicKey: process.env.NEXT_PUBLIC_ABUSEGRAPH_PUBLISHABLE_KEY!, // pk_test_…
})

export async function collectForAuth(email: string, event: "signup" | "login") {
  const collected = await sdk.collect()
  const edge = await sdk.evaluate(collected, { email })
  const body = sdk.toCheckBody(collected, {
    email,
    event,
    verdictToken: edge.verdictToken.token,
  })
  // Store on the user before Clerk finishes signup
  await clerk.user.update({ unsafeMetadata: { abusegraph: body } })
  return body
}

Clerk webhooks

user.created (signup)

import { createCheckClient } from "@abusegraph/server"

const abusegraph = createCheckClient({
  secretKey: process.env.ABUSEGRAPH_SECRET_KEY!,
  site: process.env.ABUSEGRAPH_SITE!,
})

export async function POST(req: Request) {
  const evt = await req.json()
  if (evt.type !== "user.created") return new Response("ok")

  const email = evt.data.email_addresses?.[0]?.email_address
  const browser = evt.data.unsafe_metadata?.abusegraph ?? {}

  const result = await abusegraph.check({
    email,
    userId: evt.data.id,
    event: "signup",
    ip: req.headers.get("x-forwarded-for") ?? undefined,
    ...browser,
  })

  if (result.verdict === "block") {
    // Ban or delete the Clerk user
  }

  return Response.json(result)
}

session.created (login)

if (evt.type === "session.created") {
  const email = evt.data.user?.email_addresses?.[0]?.email_address
  const result = await abusegraph.check({
    email,
    userId: evt.data.user_id,
    event: "login",
    ip: req.headers.get("x-forwarded-for") ?? undefined,
  })
  if (result.verdict === "block") {
    // Revoke session / ban user
  }
}

Lifecycle events

  • loginsession.created webhook with event: "login".
  • password_reset — call check from your reset handler or a custom webhook with event: "password_reset".
  • email_change — call after Clerk email update with event: "email_change".

Platform

Lists, rules, and linked accounts apply on every check — configure in the console. Optional session: POST /api/v1/session. Native apps: iOS, Android, React Native.

Verify

Create a test user with sk_test_…, confirm a row in Activity, and verify blocked users are removed or banned.