ShieldSignup
Guides

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 sendip_statusScoring
(field omitted or empty)missingEmail + domain only
localhostignored_localhostEmail + domain only
127.0.0.1, ::1, 0.0.0.0, ::ignored_loopbackEmail + domain only
10.x, 192.168.x, 172.16–31.x, link-localignored_privateEmail + domain only
Public IPv4/IPv6 (e.g. 203.0.113.42)okFull 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

  1. Read the IP on the server when the signup request hits your backend.
  2. Never call ShieldSignup from the browser with a guessed IP.
  3. Never send your server's egress IP or 127.0.0.1 in production.
  4. Behind a reverse proxy, use your framework's trusted-proxy support — do not blindly trust the first X-Forwarded-For hop.

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 ...
end

Set 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 None

When using Uvicorn behind a proxy, run with --proxy-headers --forwarded-allow-ips='*' only for trusted proxies.

Behind common edge providers

ProviderHeader to read (after trusting the edge)
CloudflareCF-Connecting-IP
Vercel / Netlifyx-forwarded-for (first hop after platform)
AWS ALBX-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 (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.

On this page