vevee.guard.signup()POST /api/v1/guard/signupsk_live_

The main signup guard call. Call it once, when you create the account. It runs the email checks, compares the signup with the app's recent signups, stores this signup as keyed hashes, and returns a recommendation. Requires a secret key (sk_*) and SDK 1.15.0 or later. Never call it on login.

Signature

vevee.guard.signup(p: {
  userId: string;          // your id for the new account, 1 to 200 chars
  email: string;           // max 320 chars
  device?: string;         // the blob from collectDevice() in the browser
  context?: {
    ip?: string;           // max 64 chars; used in memory only, never stored
    ja4?: string;          // TLS fingerprint, max 128 chars
    userAgent?: string;    // max 512 chars
  };
}): Promise<GuardSignupResponse>

Build context with guardContextFromHeaders(req.headers) from @vevee/sdk. It reads the IP from x-vercel-forwarded-for, cf-connecting-ip, x-real-ip, and only then the left-most x-forwarded-for entry, which a client can spoof. Behind any other proxy, pass ip yourself. The device blob comes from collectDevice() in @vevee/sdk/browser; an oversized blob is ignored, not rejected.

Example

// app/api/signup/route.ts
import { guardContextFromHeaders } from '@vevee/sdk';
import { vevee } from '@/lib/vevee'; // createClient({ apiKey: 'sk_live_...' })

export async function POST(req: Request) {
  const { email, device } = await req.json();
  const userId = crypto.randomUUID();

  const verdict = await vevee.guard.signup({
    userId,
    email,
    device,
    context: guardContextFromHeaders(req.headers),
  });

  switch (verdict.recommendation) {
    case 'block':
      return Response.json({ error: 'Use a different email.' }, { status: 400 });
    case 'deny_free':
      await createAccount(userId, email, { freeCredits: 0 });
      break;
    case 'review':
      await createAccount(userId, email);
      await queueReview(userId, verdict.reasons);
      break;
    default:
      await createAccount(userId, email);
  }
  return Response.json({ ok: true });
}

Response

{
  email: {
    email: string;
    normalized: string;          // dots and +tags removed where the provider ignores them
    domain: string;
    valid: boolean;
    deliverable: boolean | null; // null = unknown (DNS timeout)
    disposable: boolean;
    relay: boolean;              // Hide My Email, Firefox Relay and similar
    domainAgeDays: number | null;
    suggestion: string | null;   // 'gmail.com' for 'gmial.com'
    sources: string[];           // diagnostic only, do not branch on it
    risk: 'low' | 'medium' | 'high' | 'unknown';
  };
  device: {
    risk: 'low' | 'medium' | 'high' | 'unknown'; // unknown = no device signal or degraded
    score: number;               // best match score, 0 to 100+
    matches: number;             // earlier signups at or above the medium threshold
  };
  risk: 'low' | 'medium' | 'high' | 'unknown'; // worst of email and device
  mode: 'off' | 'observe' | 'enforce';
  recommendation: 'allow' | 'review' | 'deny_free' | 'block';
  reasons: GuardReason[];        // strongest first
  planId: string | null;         // your deny_free plan, when set and it exists
  degraded: boolean;
  unavailableReason: 'guard_cap_reached' | 'guard_unavailable' | 'guard_disabled' | null;
}

Branch on recommendation and show reasons to your team. In observe mode the recommendation is always allow; the dashboard shows what enforce would have done. The actions behind each reason are set per app on the Signup guard page.

Reasons

ReasonMeaningDefault action
invalid_emailNot an email address (no @, or a malformed domain). Always block.block
blocked_domainThe domain is on your app's block list.block
disposable_emailA throwaway email service (list or approved burner mail server).block
undeliverable_domainA valid address whose domain cannot receive mail.block
email_seenThe same normalized email signed up before on this app.deny_free
device_highStrong match with an earlier signup (score at or above the high threshold, default 70).deny_free
device_mediumPartial match (score at or above the medium threshold, default 40). Identical phones on one Wi-Fi land here, so never set it to deny_free.review
new_domainThe domain is younger than your new-domain window (default 30 days).review
suspect_mail_serverThe domain uses a mail server seen with burner domains, waiting for review by the Vevee team. Once approved it counts as disposable_email.review
relay_emailA privacy relay address.allow
checks_unavailableChecks could not run and your app is set to review in that case.review

Never throws

Every failure (network, 3 second timeout, 4xx, 5xx, a pk_* key) resolves to an unavailable verdict: risk: 'unknown', unavailableReason: 'guard_unavailable', and the recommendation from the guardUnavailable client option (default 'allow'). The error goes to onGuardError if you set it.

const vevee = createClient({
  apiKey: process.env.VEVEE_SECRET_KEY!,
  guardUnavailable: 'allow', // or 'review'
  onGuardError: (err) => console.warn('guard unavailable', err instanceof Error ? err.message : err),
});

A pk_* key is refused inside the SDK without a request, so shipping one means every signup is silently allowed. Watch onGuardError.

Allowance and degraded answers

Each call counts once against the guard allowance (20% of your plan's operations, separate from metering; guard calls never cause workspace_limit_reached). Past the allowance the check runs degraded: disposable lists, burner mail servers, your block list and repeated emails still run, DNS, domain age and device matching are skipped, and the answer carries degraded: true and unavailableReason: 'guard_cap_reached'. Degraded answers do not count. See the guide for the details.

Errors (raw HTTP only)

The SDK never surfaces these; it returns the unavailable verdict instead.

CodeStatusWhen
invalid_key401Missing or revoked key.
requires_secret_key403Called with a pk_* key.
invalid_request400Missing userId, or a field over its length limit.
i
Related: guard.checkEmail() before the OTP, guard.outcome() afterwards, and the Signup guard guide (privacy and consent, going live).