ShieldSignup
Getting started

Quickstart

A working ShieldSignup integration in under ten minutes — curl, Node, and Python.

This page gets you from zero to a working assessment in three steps. Examples use process.env.SHIELDSIGNUP_API_KEY, set a 3000ms timeout, and handle all three verdicts. Only email is required — add the client IP for full network and velocity checks in production.

1. Prerequisites

API keys begin with sk_live_ (production) or sk_test_ (sandbox). Use a sk_test_… key for the rest of this page; sandbox keys never deduct from your monthly quota.

2. Make your first request

The base URL for production is https://api.shieldsignup.com/v1.

Fastest path — email only (no IP extraction needed yet):

curl https://api.shieldsignup.com/v1/assess \
  -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"block@example.com"}'

Production path — email plus the end user's public IP:

curl https://api.shieldsignup.com/v1/assess \
  -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"block@example.com","ip":"203.0.113.42"}'

See Getting the client IP for framework examples. ShieldSignup ignores 127.0.0.1, private LAN addresses, and localhost automatically — you still get 200 OK, but with ip_provided: false.

In sandbox mode (sk_test_…), the API matches on the email local-part prefix to give you a deterministic verdict. block@… always returns this shape:

{
  "request_id": "req_01hw5k3mfvx8t2b4ncqw7ydpej",
  "verdict": "block",
  "score": 92,
  "reasons": [
    {
      "code": "email_disposable",
      "signal": "email"
    }
  ],
  "signals": {
    "email": {
      "disposable": false,
      "domain": "example.com",
      "domain_age_days": 3650,
      "mx_valid": true,
      "public_domain": false,
      "role_account": false
    },
    "ip": {
      "address": "203.0.113.42",
      "tor": false,
      "vpn": false,
      "proxy": false,
      "datacenter": false,
      "abuse_score": 3,
      "country_code": "US",
      "asn": "AS15169"
    },
    "velocity": {
      "ip_signups_1h": 1,
      "ip_signups_24h": 1,
      "email_domain_1h": 1,
      "email_domain_24h": 1
    }
  },
  "ip_provided": true,
  "ip_status": "ok",
  "processed_ms": 12,
  "assessed_at": "2026-05-10T10:14:33Z"
}

When you omit ip, the response includes ip_provided: false and omits signals.ip. Sandbox fixtures still match on the email local-part.

Swap block@ for allow@ or challenge@ to exercise the other branches. See Testing for the full fixture matrix.

3. Add it to your signup handler

Node.js (Next.js Server Action / API route)

export async function signup(email: string, ip?: string) {
  const payload: Record<string, string> = { email };
  if (ip) {
    payload.ip = ip;
  }

  const res = await fetch("https://api.shieldsignup.com/v1/assess", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SHIELDSIGNUP_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(3000),
  });

  if (!res.ok) {
    // ShieldSignup returned an error envelope. Decide your fallback policy.
    // See: /docs/api-reference/errors
    return { error: "assessment_unavailable" as const };
  }

  const { verdict, request_id } = (await res.json()) as {
    verdict: "allow" | "challenge" | "block";
    request_id: string;
  };

  if (verdict === "block") {
    return { error: "signup_rejected" as const };
  }

  if (verdict === "challenge") {
    return { challenge: true as const, request_id };
  }

  await createUser(email);
  return { success: true as const, request_id };
}

Python

import os
import requests

API_URL = "https://api.shieldsignup.com/v1/assess"

def assess(email: str, ip: str | None = None) -> dict:
    payload = {"email": email}
    if ip:
        payload["ip"] = ip
    response = requests.post(
        API_URL,
        headers={
            "Authorization": f"Bearer {os.environ['SHIELDSIGNUP_API_KEY']}",
            "Content-Type": "application/json",
        },
        json=payload,
        timeout=3,
    )
    response.raise_for_status()
    return response.json()

def signup(email: str, ip: str | None = None) -> dict:
    result = assess(email, ip)
    verdict = result["verdict"]
    request_id = result["request_id"]

    if verdict == "block":
        return {"error": "signup_rejected"}
    if verdict == "challenge":
        return {"challenge": True, "request_id": request_id}

    create_user(email)
    return {"success": True, "request_id": request_id}

curl (full handler shape)

RESPONSE=$(curl --silent --max-time 3 \
  -H "Authorization: Bearer ${SHIELDSIGNUP_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"${EMAIL}\",\"ip\":\"${IP}\"}" \
  https://api.shieldsignup.com/v1/assess)

VERDICT=$(echo "$RESPONSE" | jq -r '.verdict')

case "$VERDICT" in
  allow)     echo "ok: create the account" ;;
  challenge) echo "challenge: send verification email" ;;
  block)     echo "block: refuse signup" ;;
  *)         echo "error: $RESPONSE" ;;
esac

What's next

On this page