Getting the client IP
How to pass the end user's real IP to POST /v1/assess — and what ShieldSignup ignores automatically.
ShieldSignup scores network and velocity signals only when you send a usable public client IP. The IP must be the address of the person submitting the signup, collected on your server at request time.
Required vs recommended
email is required. ip is optional but strongly recommended
for production. Without a usable IP, assessments use email and domain
signals only.
What ShieldSignup ignores (server-side)
These values are accepted in the JSON body but not used for scoring.
The API returns 200 OK with ip_provided: false and an ip_status that
explains why:
| You send | ip_status | Scoring |
|---|---|---|
| (field omitted or empty) | missing | Email + domain only |
localhost | ignored_localhost | Email + domain only |
127.0.0.1, ::1, 0.0.0.0, :: | ignored_loopback | Email + domain only |
10.x, 192.168.x, 172.16–31.x, link-local | ignored_private | Email + domain only |
Public IPv4/IPv6 (e.g. 203.0.113.42) | ok | Full assessment |
This is intentional: many integrations send 127.0.0.1 during local dev or
their app server's private IP behind a misconfigured proxy. ShieldSignup
normalizes those away instead of treating them as high-risk public addresses.
Malformed values (e.g. "not-an-ip") still return 400 invalid_ip.
Check ip_provided and ip_status in the response while integrating — they
tell you whether network signals ran.
Rules of thumb
- Read the IP on the server when the signup request hits your backend.
- Never call ShieldSignup from the browser with a guessed IP.
- Never send your server's egress IP or
127.0.0.1in production. - Behind a reverse proxy, use your framework's trusted-proxy support — do
not blindly trust the first
X-Forwarded-Forhop.
Next.js (App Router)
In a Route Handler or Server Action:
import { headers } from "next/headers";
export async function getClientIP(): Promise<string | undefined> {
const h = await headers();
const forwarded = h.get("x-forwarded-for");
if (forwarded) {
return forwarded.split(",")[0]?.trim();
}
return h.get("x-real-ip") ?? undefined;
}
export async function assessSignup(email: string) {
const ip = await getClientIP();
const body: Record<string, string> = { email };
if (ip) {
body.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(body),
signal: AbortSignal.timeout(3000),
});
return res.json();
}On Vercel, x-forwarded-for is set by the platform. For self-hosted
Next.js behind nginx or a load balancer, configure the proxy to pass
X-Forwarded-For and only read it from trusted hops.
Express (Node.js)
Enable trust proxy when running behind nginx, Heroku, or a load balancer:
const express = require("express");
const app = express();
app.set("trust proxy", 1); // trust first proxy; adjust for your topology
app.post("/signup", async (req, res) => {
const email = req.body.email;
const ip = req.ip; // respects X-Forwarded-For when trust proxy is set
const assessRes = 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 }),
});
// ...
});Django
def client_ip(request) -> str | None:
forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
if forwarded:
return forwarded.split(",")[0].strip()
return request.META.get("REMOTE_ADDR")
def assess_signup(email: str, request) -> dict:
payload = {"email": email}
ip = client_ip(request)
if ip:
payload["ip"] = ip
# POST to ShieldSignup ...Configure your reverse proxy and Django's
SECURE_PROXY_SSL_HEADER
when terminating TLS at the edge.
Ruby on Rails
def assess_signup(email, request)
payload = { email: email }
ip = request.remote_ip # uses ActionDispatch::RemoteIp when configured
payload[:ip] = ip if ip.present?
# POST to ShieldSignup ...
endSet config.action_dispatch.trusted_proxies to your load balancer CIDRs in
config/environments/production.rb.
Laravel (PHP)
$ip = $request->ip(); // uses TrustProxies middleware
$response = Http::timeout(3)
->withToken(config('services.shieldsignup.key'))
->post('https://api.shieldsignup.com/v1/assess', [
'email' => $email,
'ip' => $ip,
]);Publish and configure app/Http/Middleware/TrustProxies.php for your
hosting provider (AWS ALB, Cloudflare, etc.).
FastAPI (Python)
from fastapi import Request
def client_ip(request: Request) -> str | None:
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
if request.client:
return request.client.host
return NoneWhen using Uvicorn behind a proxy, run with
--proxy-headers --forwarded-allow-ips='*' only for trusted proxies.
Behind common edge providers
| Provider | Header to read (after trusting the edge) |
|---|---|
| Cloudflare | CF-Connecting-IP |
| Vercel / Netlify | x-forwarded-for (first hop after platform) |
| AWS ALB | X-Forwarded-For (with trust proxy / framework config) |
Always restrict which proxies you trust. Spoofed X-Forwarded-For values
from the public internet must not reach your app as the client IP.
Minimum vs recommended payload
Minimum (works immediately — email signals only):
{ "email": "user@example.com" }Recommended (production — full assessment):
{
"email": "user@example.com",
"ip": "203.0.113.42"
}See Assess a signup for the full response
reference including ip_provided and ip_status.