Getting Started

Track Signups and Logins

Add two fields to your validate call and Fidro remembers what it has seen: repeat signups from one IP, address patterns, users appearing from new countries.

5 min read Last updated October 2, 2026 View as Markdown

A plain /api/validate call answers one question: does this email and IP look risky right now? It does not know that the same IP registered two accounts in the last ten minutes, or that this user has only ever logged in from Germany.

Give it that memory by telling Fidro what is happening. Two optional fields on the same endpoint:

Field What to send
event signup, login, checkout, password_reset, check, or custom:<name>
user_id Your own identifier for the person, once you have one

A call with either field is recorded as an event, counted against your account's history, and checked against the anomaly rules. A call with neither behaves exactly as before.

1. Tag your signup

curl -X POST https://fidro.io/api/validate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "ip": "203.0.113.1",
    "event": "signup"
  }'

The response gains two blocks under data:

{
  "risk_score": 0,
  "recommendation": "allow",
  "data": {
    "velocity": {
      "ip_signups_1h": 1,
      "ip_signups_24h": 1,
      "subnet_signups_1h": 1,
      "email_pattern_matches_24h": 1
    },
    "anomalies": []
  }
}

Counts include the call you just made, so 1 means "this is the first". On the third signup from that IP inside an hour the counts read 3, ip_signup_burst fires, 40 points are added to the score and the recommendation is at least challenge:

{
  "risk_score": 40,
  "recommendation": "challenge",
  "message": "No risk factors detected. Velocity: ip signup burst.",
  "data": {
    "checks": { "ip_signup_burst": true },
    "anomalies": [
      {
        "code": "ip_signup_burst",
        "severity": "high",
        "detail": "3 signups from 203.0.113.1 in the last hour."
      }
    ]
  }
}

You do not need to change how you read the response. risk_score and recommendation already include the anomaly, so existing threshold logic keeps working. The extra fields are there when you want the reason.

2. Tag logins with a user_id

Once the account exists, send its ID on every login:

$result = Http::withToken(config('services.fidro.api_key'))
    ->post('https://fidro.io/api/validate', [
        'email' => $user->email,
        'ip' => $request->ip(),
        'event' => 'login',
        'user_id' => (string) $user->id,
    ])->json();

if ($result['recommendation'] !== 'allow') {
    // Ask for a second factor, email a login alert, or hold the session
}

With a user_id Fidro can see the person across IPs and countries. After three events from one country, a login from another adds new_country_for_user. Two countries inside an hour adds impossible_travel and floors the recommendation at challenge. Five different user IDs logging in from one IP in a day adds ip_many_users.

The user_id is opaque to Fidro. Use whatever you already have (a database ID, a UUID) and keep it stable; it is the key everything else hangs off.

3. Other events

  • checkout and password_reset are recorded and counted like logins, so a password reset from a brand new country for an established user is flagged the same way.
  • custom:<name> (lowercase letters, digits and underscores, up to 24 characters) records anything else you want in the history: custom:invite_accepted, custom:api_key_created.
  • Sending only a user_id with no event records a check.

What the recommendation means now

Recommendation Score Suggested action
allow 0 to 49 Proceed
challenge 50 to 79, or any floored anomaly Add friction: email verification, a second factor, a CAPTCHA, or a review queue
block 80 and up Reject

"Challenge" is deliberate. A third signup from one IP in an hour is often a shared office or a family, so the right move is a verification step rather than a refusal. Block outright on specific checks (Tor, your blocklist) if your product warrants it.

Testing it

Test keys keep their own history, separate from live. To reproduce a burst, make three signup calls from the same IP with a test key. To reproduce impossible_travel, send the same user_id twice with different country_code values; in test mode country_code stands in for the IP's country. Details in Using Test Mode.

Good to know

  • Each event is one request against your monthly quota, the same as a stateless call.
  • Events are kept for 7 days on Free, 90 days on Starter and 365 days on Pro and Enterprise, then deleted.
  • Every event and its anomalies show up in the audit log.
  • Email addresses are matched by pattern as well as exactly: sam+1@, s.a.m@ and sam42@ on the same domain count as one pattern for email_pattern_burst.