# Signup check

`POST /v1/signup/check` · **3 credits** (+5 with `verify_mailbox`)

This is the one to use on a signup form. It runs the email, IP and phone checks together, adds checks that only make sense in combination, and gives you **one verdict** for the person.

## Request

Send any combination of the fields. At least one is required.

| Field | Type | Description |
|---|---|---|
| `email` | string | The email they signed up with. |
| `ip` | string | Their IP address, as your server sees it. See [which IP to send](/docs/ip-check#which-ip-to-send). |
| `phone` | string | Their phone number, if you collect one. |
| `phone_country` | string | Two-letter country, for numbers typed without `+country`. |
| `verify_mailbox` | boolean | Also verify the mailbox over SMTP (+5 credits). |

```bash [curl]
curl https://veqen-app.com/v1/signup/check \
  -H "Authorization: Bearer $VEQEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@mailinator.com", "ip": "185.220.101.1", "phone": "+14155552671"}'
```

```python [Python]
def screen_signup(email, ip, phone=None):
    r = requests.post(
        "https://veqen-app.com/v1/signup/check",
        headers={"Authorization": f"Bearer {VEQEN_API_KEY}"},
        json={"email": email, "ip": ip, "phone": phone},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["risk"]
```

```javascript [JavaScript]
async function screenSignup({ email, ip, phone }) {
  const r = await fetch("https://veqen-app.com/v1/signup/check", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.VEQEN_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ email, ip, phone }),
  });
  if (!r.ok) throw new Error(`Veqen ${r.status}`);
  return (await r.json()).risk;
}
```

## Response

The response holds the full result of each individual check (`email`, `ip`, `phone`, or `null` when you didn't send that field) plus the combined `risk`:

```json
{
  "email": { "email": "jane@mailinator.com", "disposable": true, "risk": { "...": "..." } },
  "ip": { "ip": "185.220.101.1", "flags": { "tor": true, "hosting": false }, "risk": { "...": "..." } },
  "phone": { "e164": "+14155552671", "line_type": "fixed_line_or_mobile", "risk": { "...": "..." } },
  "risk": {
    "score": 99,
    "level": "high",
    "verdict": "refuse",
    "reasons": [
      { "code": "disposable_domain", "impact": 90, "source": "email", "message": "mailinator.com hands out throwaway inboxes." },
      { "code": "tor_exit_node", "impact": 85, "source": "ip", "message": "This address is a Tor exit node." },
      { "code": "automation_pattern", "impact": 40, "source": "signup", "message": "A throwaway inbox from a server or Tor network is the classic bot-signup pattern." },
      { "code": "phone_ip_country_mismatch", "impact": 15, "source": "signup", "message": "The phone number is from US but the visitor is connecting from DE." }
    ]
  }
}
```

Each reason says which input it came from in `source`: `email`, `ip`, `phone`, or `signup` for the cross-checks.

### Cross-checks

| Code | Impact | When |
|---|---|---|
| `automation_pattern` | 40 | A throwaway email arriving from a datacenter or Tor network. |
| `phone_ip_country_mismatch` | 15 | The phone number's country differs from the IP's country. Common for travellers and VPN users, so it's a nudge, not a block. |

## How the score is combined

Signals combine like independent odds, so two medium signals add up to a stronger one, but never jump straight to 100:

```
score = 100 × (1 − (1 − a/100) × (1 − b/100) × …)
```

Two 50-point signals give 75. A 90-point signal alone gives 90.

## Recommended handling

```javascript
const risk = await screenSignup({ email, ip, phone });
if (risk.verdict === "refuse") {
  return res.status(400).json({ error: "We couldn't verify these details. Please use a permanent email address." });
}
const user = await createUser({ email, needsVerification: risk.verdict === "review" });
```

- **Don't** tell people which signal tripped. It helps abusers tune around you.
- **Do** log `risk.reasons` with the user record. It makes support and later reviews easy.
- **Fail open.** If the call errors or times out, create the account and require email confirmation.
