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:
SIGNUPSCORE_API_KEY=ss_your_api_keySend a check
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:
{
"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.
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
- Read Authentication before using your key in production.
- Choose a failure policy.
- See the full check endpoint and error behavior.