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"
}
}| Field | Type | Always present | Description |
|---|---|---|---|
error.code | string | Yes | Machine-readable error code. Branch on this. |
error.message | string | Yes | Human-readable description. Display directly to the user only with care — these strings are not localised and may change. |
error.param | string | When applicable | The field name in your request body that caused the error (e.g. "email", "ip", "request_id"). Omitted for non-validation errors. |
Error codes
| HTTP | error.code | Cause | Recommended handling |
|---|---|---|---|
400 | missing_field | Required 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. |
400 | invalid_email | email is present but does not pass the basic format check (local@domain.tld). | Reject on your side too — don't retry. |
400 | invalid_ip | ip 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. |
401 | unauthorized | Missing or invalid bearer token. | Check SHIELDSIGNUP_API_KEY. Do not retry. |
401 | token_revoked | The API key was revoked from the dashboard. | Rotate to a new key. Do not retry. |
429 | quota_exceeded | Monthly assessment limit reached for this plan. | Upgrade your plan, wait for the next UTC month reset, or fail open. See Rate limits. |
429 | rate_limit | Per-second burst limit exceeded for this plan. | Back off using X-RateLimit-Reset. See the retry example in Rate limits. |
500 | internal_error | Unexpected 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()