Quickstart

Create a key and make your first check.

This guide takes you from a new account to your first signup decision. You need a SignupScore account and a secret key.

Create an API key

Open Dashboard → API keys and create a key. Copy it when it appears because you will not be able to view the full key again.

Save the key as a server-side environment variable:

.env
SIGNUPSCORE_API_KEY=ss_your_api_key

Send a check

cURL
curl -s -X POST 'https://signupscore.com/api/v1/check' \
  -H 'Authorization: Bearer ss_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{"input":"name@tempmail.com"}'

This example checks a disposable address. A successful request returns block and explains why:

Response
{
  "request_id": "01J...",
  "action": "block",
  "primary_reason": "DISPOSABLE",
  "signals": {
    "disposable": true,
    "public_provider": false,
    "role_based": false,
    "suspicious": false,
    "privacy_relay": false,
    "dynamic_dns": false,
    "has_mx": true,
    "suggested_domain": null,
    "normalized_email": "name@tempmail.com",
    "mailbox_alias": false,
    "has_spf": true,
    "has_dmarc": false
  }
}

Use the action

Branch on action. Your product still decides what each action should look like for the person signing up.

TypeScript
switch (decision.action) {
  case 'block':
    return denySignup(decision.primary_reason);
  case 'review':
    return askForVerification();
  case 'allow':
    return createAccount();
}
  • allow: continue the signup.
  • review: ask for email verification, pause valuable access, or use your own review flow.
  • block: stop the signup and show a neutral message.

Production-ready helper

A drop-in helper for your backend that fails open: only an explicit block stops a signup. An API error, a timeout, or a missing key resolves to allow, so an outage never breaks your signup flow. Why fail open, and when to fail closed instead, is covered in Actions.

export type EmailAction = 'allow' | 'review' | 'block';

type SignupScoreResponse = {
  request_id: string;
  action: EmailAction;
  primary_reason: string;
  signals: {
    disposable: boolean;
    public_provider: boolean;
    role_based: boolean;
    suspicious: boolean;
    privacy_relay: boolean;
    dynamic_dns: boolean;
    has_mx: boolean | null;
    suggested_domain: string | null;
    normalized_email: string | null;
    mailbox_alias: boolean | null;
    has_spf: boolean | null;
    has_dmarc: boolean | null;
  };
};

// Fail-open: only an explicit "block" stops anything. An API error, timeout,
// missing key, or invalid format resolves to "allow" so an outage never
// breaks signup. Block and review policy lives in the dashboard rules.
export async function classifyEmail(email: string): Promise<EmailAction> {
  try {
    if (!email.includes('@')) {
      console.warn('SignupScore: invalid email format, skipping check');
      return 'allow';
    }

    const apiKey = process.env.SIGNUPSCORE_API_KEY;
    if (!apiKey) {
      console.error('SignupScore: SIGNUPSCORE_API_KEY is not set');
      return 'allow';
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 2500);

    const response = await fetch('https://signupscore.com/api/v1/check', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ input: email }),
      signal: controller.signal
    });

    clearTimeout(timeoutId);

    if (!response.ok) {
      console.error(`SignupScore: check failed with status ${response.status}`);
      return 'allow';
    }

    const data: SignupScoreResponse = await response.json();

    // Email memory is off unless you turn it on in Dashboard > Protection, and
    // mailbox_alias is null while it is off. True means this account already
    // checked the same mailbox spelled differently: foo+trial@gmail.com after
    // foo@gmail.com, or johnsmith@ after john.smith@. Resending the identical
    // address is false.
    //
    // It never changes action, so enabling it refuses nothing on its own. To
    // stop a second trial, pair it with your own users table, which is the
    // only thing that knows whether they already have an account.
    //
    // This needs one column on your side. Save normalized_email as
    // canonical_email when a signup completes, with a unique index, and query
    // THAT. Your users table holds what each person typed, so looking up the
    // canonical form finds nothing unless you stored it, and the unique index
    // is what stops two alias signups racing each other through.
    //
    //   if (data.signals.mailbox_alias) {
    //     const existing = await findUserByCanonicalEmail(data.signals.normalized_email);
    //     if (existing) return 'block';   // already signed up, second trial
    //   }
    //
    // Block on the lookup, never on the flag alone: the flag says a mailbox
    // came back under a new spelling, which is not evidence of an account.

    return data.action;
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      console.warn('SignupScore: check timed out');
    } else {
      console.error('SignupScore: unexpected error', error);
    }
    return 'allow';
  }
}

Call it at final signup submit and branch on the result as above. Each version reads the key from SIGNUPSCORE_API_KEY and gives up after 2.5 seconds. Block and review policy stays in your dashboard rules, so you can tune it without deploying.

Next steps