ShieldSignup
API reference

Errors

Every error code the ShieldSignup API can return, with the exact response shape and recommended handling.

All ShieldSignup errors share a single envelope. Switch on the error.code value, never on error.message (the message is human-readable and may change between releases).

Error envelope

{
  "error": {
    "code": "invalid_ip",
    "message": "The ip field must be a valid IPv4 or IPv6 address",
    "param": "ip"
  }
}
FieldTypeAlways presentDescription
error.codestringYesMachine-readable error code. Branch on this.
error.messagestringYesHuman-readable description. Display directly to the user only with care — these strings are not localised and may change.
error.paramstringWhen applicableThe field name in your request body that caused the error (e.g. "email", "ip", "request_id"). Omitted for non-validation errors.

Error codes

HTTPerror.codeCauseRecommended handling
400missing_fieldRequired field is missing, JSON body is malformed, or a referenced ID is unknown.Fix the request before retrying. The param field tells you which field is missing.
400invalid_emailemail is present but does not pass the basic format check (local@domain.tld).Reject on your side too — don't retry.
400invalid_ipip is present but is not a valid IPv4/IPv6 address (e.g. "not-an-ip"). Note: 127.0.0.1, private addresses, and localhost return 200 OK with ip_provided: false — they are not errors.Fix the value or omit ip for an email-only assessment. See Getting the client IP.
401unauthorizedMissing or invalid bearer token.Check SHIELDSIGNUP_API_KEY. Do not retry.
401token_revokedThe API key was revoked from the dashboard.Rotate to a new key. Do not retry.
429quota_exceededMonthly assessment limit reached for this plan.Upgrade your plan, wait for the next UTC month reset, or fail open. See Rate limits.
429rate_limitPer-second burst limit exceeded for this plan.Back off using X-RateLimit-Reset. See the retry example in Rate limits.
500internal_errorUnexpected server error (database failure, signal-source failure, etc.).Retry once after ~1s. If it fails again, fail open per your policy.

ShieldSignup does not currently use the 422 or 503 status codes. Validation problems return 400. Transient infra failures surface as 500 or as a connection timeout.

Fail open vs fail closed

When ShieldSignup is unreachable (network timeout, repeated 500s, etc.), your code must decide:

  • Fail open — Allow the signup to proceed. Recommended for most products. Temporary downtime should not block legitimate users.
  • Fail closed — Block the signup until the service recovers. Use only for very high-risk flows.

Document the decision in your code with a comment, so the next person to read it knows it was deliberate.

async function safeAssess(email: string, ip: string) {
  try {
    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({ email, ip }),
      signal: AbortSignal.timeout(3000),
    });

    if (!res.ok) {
      const body = (await res.json().catch(() => null)) as
        | { error?: { code?: string } }
        | null;
      const code = body?.error?.code;

      // 401 unauthorized / token_revoked → fix your config, don't fail open silently
      if (code === "unauthorized" || code === "token_revoked") {
        throw new Error(`ShieldSignup auth failure: ${code}`);
      }

      // ShieldSignup unavailable — fail open to avoid blocking legitimate users.
      return { verdict: "allow" as const };
    }

    return (await res.json()) as { verdict: "allow" | "challenge" | "block" };
  } catch {
    // Timeout or network error — fail open to avoid blocking legitimate users.
    return { verdict: "allow" as const };
  }
}
import os
import requests

def safe_assess(email: str, ip: str) -> dict:
    try:
        response = requests.post(
            "https://api.shieldsignup.com/v1/assess",
            headers={
                "Authorization": f"Bearer {os.environ['SHIELDSIGNUP_API_KEY']}",
                "Content-Type": "application/json",
            },
            json={"email": email, "ip": ip},
            timeout=3,
        )
    except requests.RequestException:
        # Network/timeout — fail open.
        return {"verdict": "allow"}

    if response.status_code == 401:
        # Bad key. Surface this — don't fail open silently.
        body = response.json().get("error", {})
        raise RuntimeError(f"ShieldSignup auth failure: {body.get('code')}")

    if not response.ok:
        # 5xx or 429 — fail open per default policy.
        return {"verdict": "allow"}

    return response.json()

On this page