Documentation

Build secure login flows with LoginLlama's suspicious login detection API. Install it with a single agent prompt, query your analytics over MCP, or wire up the SDK by hand to detect Tor nodes, VPNs, impossible travel and new devices.

Install with one prompt

The fastest way to add LoginLlama is to let your coding agent do it. Paste the prompt below into Claude Code, Cursor, or Codex and it will install the SDK, wire up the risk check, and ask you for an API key. Prefer to do it by hand? The SDK quickstart is right below.

Paste into your agent
Integrate LoginLlama (suspicious login detection) into this project.

1. Detect the language/framework and install the official SDK:
   - Better Auth (any framework):   npm install better-auth-loginllama,
                                    then add loginLlama() to plugins
   - Clerk:                         npm install clerk-loginllama,
                                    then export POST = loginLlamaWebhook()
                                    from a session.created webhook route
   - Auth.js (NextAuth) Credentials: npm install authjs-loginllama,
                                    then wrap authorize in withLoginLlama()
   - Node.js / Next.js / Express:  npm install loginllama
   - Python / Django / Flask:      pip install loginllama
   - PHP / Laravel:                 composer require joshghent/loginllama
2. Read the live API reference and examples from https://loginllama.app/docs (the machine-readable
   summary is at https://loginllama.app/llms.txt).
3. Get an API key with the device flow. POST to https://loginllama.app/api/v1/oauth/device with
   JSON { "client_name": "<your name>", "project_name": "<this repository's name, or
   the website's name for WordPress>" }. Show me verification_uri_complete and wait
   while I approve it. Poll https://loginllama.app/api/v1/oauth/token with { "grant_type":
   "urn:ietf:params:oauth:grant-type:device_code", "device_code": "<device_code>" }
   at the given interval until it returns access_token. If I have no account, the
   approval page asks me to sign up, with 100 free checks and no card.
   Save it as LOGINLLAMA_API_KEY in the environment. Never hard-code the key.
4. After my auth step succeeds, call LoginLlama with the user's identity_key,
   email, IP address and user agent. Branch on the result:
     - risk_score > 5  -> require MFA / extra verification or block
     - otherwise        -> allow the login
   Always FAIL OPEN: if LoginLlama errors or times out, log it and let the user in.
5. Add a short comment linking to https://loginllama.app/docs and show me how to test it locally.
Works in Claude Code Cursor Codex

Ask your analytics (MCP)

LoginLlama ships an MCP server so your agent can query your login analytics in plain language. Connect it once with your API key, then ask questions like “what is my average login risk score for the chrome browser?”. Every tool is scoped to your team and read-only.

Claude Code
claude mcp add --transport http loginllama https://loginllama.app/api/v1/mcp --header "X-API-KEY: YOUR_LOGINLLAMA_API_KEY"
Codex
codex mcp add loginllama --transport http --url https://loginllama.app/api/v1/mcp --header "X-API-KEY: YOUR_LOGINLLAMA_API_KEY"
Cursor
Add to Cursor

Opens Cursor and pre-fills the MCP install.

Or add to any MCP client (.mcp.json)
{
  "mcpServers": {
    "loginllama": {
      "type": "http",
      "url": "https://loginllama.app/api/v1/mcp",
      "headers": {
        "X-API-KEY": "YOUR_LOGINLLAMA_API_KEY"
      }
    }
  }
}

Endpoint: https://loginllama.app/api/v1/mcp · Transport: HTTP · Auth header: X-API-KEY: YOUR_LOGINLLAMA_API_KEY

Then just ask:

  • ›“What is my average login risk score for the chrome browser?”
  • ›“Which browser has the highest risk logins this month?”
  • ›“Show me the 10 most recent high-risk logins.”
  • ›“How many Tor or VPN logins did we see in the last 7 days?”
  • ›“Break down logins by country and average risk score.”
ToolWhat it answers
get_login_statsTotals, average risk, high-risk, bot/Tor/VPN counts
get_average_risk_scoreAverage risk score, filtered by browser/OS/country/device/user
get_login_breakdownGroup logins by browser, OS, device, country, or status
get_recent_loginsThe latest login checks with risk, location and device
get_account_usageRemaining API credits and plan

Quickstart: 5-Minute Integration

Get started with LoginLlama in just a few minutes. This guide will walk you through making your first API call and interpreting the results.

1

Get Your API Key

Your API key authenticates your requests. Keep it secure and never commit it to version control.

2

Make Your First Request

Send a POST request to /api/v1/login/check with the user's login information. The only required field is identity_key (user identifier).

curl -X POST https://loginllama.app/api/v1/login/check \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
    "identity_key": "[email protected]",
    "ip_address": "203.0.113.42",
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)",
    "geo_country": "US",
    "geo_city": "San Francisco"
  }'
3

Interpret the Response

The API returns a risk_score (0-10+) and an array of codes explaining what factors contributed to the score.

{
  "data": {
    "type": "login_check",
    "attributes": {
      "status": "pass",
      "risk_codes": [
        "ip_address_not_used_before"
      ],
      "message": "Login check passed",
      "risk_score": 2,
      "unrecognized_device": false,
      "authentication_outcome": "success"
    }
  },
  "meta": {
    "environment": "production",
    "test": false,
    "email_sent": true
  }
}

Success: Login check passed. The risk_score determines the level of suspicion (0 = safe, 10+ = highly suspicious).

Understanding Risk Scores

  • 0-2: Very low risk - Safe to allow
  • 3-5: Moderate risk - Consider additional verification
  • 6-7: High risk - Recommend 2FA or email verification
  • 8+: Very high risk - Consider blocking
4

Handle the Result

Based on the risk score and your security requirements, decide how to handle the login attempt. You have full control over the thresholds and actions.

// Handle the response
if (result.status === 'error' || result.risk_score > 5) {
  // High risk detected - take action
  console.log('Suspicious login detected:', result.codes);

  // Options:
  // 1. Block the login
  return res.status(403).json({ error: 'Login blocked' });

  // 2. Require additional verification (2FA)
  return res.json({ requireMFA: true });

  // 3. Notify security team
  await notifySecurityTeam(result);
} else {
  // Low risk - allow login
  console.log('Login approved, risk score:', result.risk_score);
  return res.json({ success: true });
}

Next Steps

  • • Read the full API Reference to understand all available parameters
  • • Explore our SDKs for easier integration
  • • Check out Code Examples for common use cases

Authentication

All API requests require authentication using an API key. Include your API key in the X-API-KEY header of every request.

Authentication Header

X-API-KEY: llama_prod_xxxxxxxxxxxxxxxxxxxx

Test mode

A test key never scores a real login. It ignores the IP address, device and history, spends no credits, and picks its answer from tags you add after a + in the identity or email, like Stripe's test cards. Use it to try every branch of your code, or in an identity provider's test tenant where you can choose a user's email but not where they log in from.

IdentityResult
[email protected]Score 0, passes
[email protected]Score 9, fails. Any score from 0 to 10 works.
[email protected]That risk code, scored by its usual weight. Any risk code works.
[email protected]A new device, so a new-login alert email is sent to [email protected] if she is on your team
user_123+new_login_location+risk_7Tags combine. A risk tag sets the score.

Test responses carry meta.test: true. Test logins appear only when the dashboard is in test mode, webhooks still fire, and alert emails go only to members of your team. Test keys are limited to 60 checks a minute.

In an identity provider's test tenant

Auth0, Clerk and similar providers send LoginLlama the real IP of whoever logs in, so you cannot fake a risky location. You can choose each test user's email, and the email is enough. With Auth0:

  1. In a development tenant, set the LoginLlama Action's LOGINLLAMA_API_KEY secret to your test key.
  2. Create a user for each branch you want to see, such as [email protected] (the Action denies the login) and [email protected] (LoginLlama sends a new-login alert).
  3. Log in as each one. Most mail providers deliver + addresses to your normal inbox, and LoginLlama removes the tags before sending, so the alert reaches [email protected] as long as you are on the LoginLlama team.

Switch the dashboard to test mode to see those logins. Use your live key in production, since a test key looks at nothing real and protects nobody.

Security Best Practices

  • •Never commit API keys to version control
  • •Store keys in environment variables
  • •Rotate keys periodically
  • •Use test keys for development and testing
  • •Manage your keys in the API settings

API Reference

Complete reference for the LoginLlama API endpoints, parameters, and responses.

Check Login Endpoint

POST
https://loginllama.app/api/v1/login/check

Analyzes a login attempt and returns a risk score based on IP address, device fingerprint, location, behavioral patterns, and more.

Edge Runtime
Rate Limit: 100/min
Authentication Required

Request Parameters

Send a JSON payload with the following parameters. Only identity_key is required, but providing more data improves detection accuracy.

ParameterTypeRequiredDescription
identity_key
string
Required

Unique identifier for the user (email, username, user ID, etc.)

Validation: 1-255 characters

Example: [email protected]

ip_address
string
Optional

IP address of the login attempt (IPv4 or IPv6)

Validation: 7-45 characters

Example: 203.0.113.42

user_agent
string
Optional

Browser user agent string

Validation: Maximum 1000 characters

Example: Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)...

geo_country
string
Optional

Country name or ISO code

Validation: Maximum 100 characters

Example: United States

geo_city
string
Optional

City name

Validation: Maximum 100 characters

Example: San Francisco

geo_region
string
Optional

State or province, as an ISO 3166-2 subdivision code or its full name. Resolved from the IP address when omitted. Shown for countries where the subdivision is part of the address (US, Canada, Australia).

Validation: Maximum 100 characters

Example: CA

user_time_of_day
string
Optional

User's local time in HH:mm format (24-hour)

Validation: Format: HH:mm (e.g., 14:30)

Example: 14:30

email_address
string
Optional

User email address for notifications (if enabled)

Validation: Valid email format, maximum 320 characters

Example: [email protected]

authentication_outcome
string
Optional

Customer's authentication outcome: "success" (default), "failed", or "pending". Use "pending" for pre-auth checks, then report "success" or "failed" after authentication.

Validation: One of: success, failed, pending

Example: success

privileged
boolean
Optional

Set to true for admins and other accounts that would hurt most if taken over. Any risk signal on them adds privileged_account (+2), so a new network, country and device fails instead of passing, and the account gets 3 failed attempts before after_failure_burst rather than 10.

Validation: true or false (default false)

Example: true

timestamp
string
Optional

Timestamp of the login attempt (ISO 8601 recommended). If omitted, the server time is used.

Validation: Maximum 100 characters

Example: 2026-01-04T13:45:12.123Z

Request Examples

import { LoginLlama } from 'loginllama';

const loginllama = new LoginLlama({
  apiKey: process.env.LOGINLLAMA_API_KEY
});

// Use middleware for automatic IP/User-Agent detection
app.use(loginllama.middleware());

// Pre + Post Auth Pattern - Most complete protection
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // Step 1: Pre-auth check (before verifying credentials)
    const preCheck = await loginllama.check(email, {
      authenticationOutcome: 'pending'  // Mark as pre-auth check
    });

    // Block high-risk attempts before authentication
    if (preCheck.risk_score > 7) {
      return res.status(403).json({ error: 'Login blocked' });
    }

    // Step 2: Authenticate with your system
    const authResult = await authenticate(email, password);

    // Step 3: Report outcome to LoginLlama
    if (authResult.success) {
      await loginllama.reportSuccess(authResult.user.id);
      return res.json({ success: true });
    } else {
      // Report failed auth - helps detect brute force attacks
      await loginllama.reportFailure(email);
      return res.status(401).json({ error: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('LoginLlama error:', error);
    return res.json({ success: true }); // Fail open on errors
  }
});

Response Format

The API returns a JSON response with the following structure:

ParameterTypeRequiredDescription
data
object
Required

Primary data for the response (JSON:API spec)

Example: { type: "login_check", attributes: {...} }

data.type
string
Required

Resource type identifier

Example: "login_check"

data.attributes.status
string
Required

Overall check result

Example: "pass" or "fail"

data.attributes.risk_codes
string[]
Required

Array of risk codes detected

Example: ["ip_address_not_used_before", "new_login_location"]

data.attributes.message
string
Required

Human-readable message

Example: "Login check passed"

data.attributes.risk_score
number
Required

Calculated risk score (0-10+)

Example: 2

data.attributes.unrecognized_device
boolean
Required

Whether an unrecognized device/browser combo was detected

Example: false

data.attributes.authentication_outcome
string
Required

The authentication outcome that was reported

Example: "success", "failed", or "pending"

meta
object
Required

Additional metadata about the check

Example: { environment: "production", email_sent: true }

meta.environment
string
Required

API key environment

Example: "production" or "staging"

meta.email_sent
boolean
Required

Whether a customer notification email was sent

Example: false

Example Responses

{
  "data": {
    "type": "login_check",
    "attributes": {
      "status": "pass",
      "risk_codes": [
        "ip_address_not_used_before"
      ],
      "message": "Login check passed",
      "risk_score": 2,
      "unrecognized_device": false,
      "authentication_outcome": "success"
    }
  },
  "meta": {
    "environment": "production",
    "test": false,
    "email_sent": true
  }
}

Success: Login check passed. The risk_score determines the level of suspicion (0 = safe, 10+ = highly suspicious).

Risk Codes Reference

The codes array contains one or more of the following risk indicators:

CodeDescriptionRisk
known_tor_exit_nodeIP address is a known Tor exit node
+1
known_vpnIP address is from a known VPN provider
+1
ip_address_suspiciousIP address is in suspicious IP list (Firehol + VPN lists)
+1
known_proxyIP address belongs to a cloud or hosting network: a commercial VPN, a proxy or a script
+1
known_botnetIP address is in a known botnet range
+1
ip_address_not_used_beforeThis IP address has not been used by this user before
+1
new_login_locationLogin is from a new city/country combination
+1
device_fingerprint_not_used_beforeThe device/browser combination has not been used before
+1
impossible_travel_detectedUser traveled impossibly fast between locations
+2
geo_mismatch_detectedThe geo_country or geo_city sent does not match where the IP address is
+1 to +2
known_botBot detection identified this as an automated client
+1
behavioral_fingerprint_suspiciousLogin time or pattern is unusual for this user
+1
after_failure_burstMore than 10 failed attempts on this account in 5 minutes, from this IP, within the last hour
+5
ip_password_sprayingThis IP failed on more than 10 of your accounts in 10 minutes, within the last hour
+4
privileged_accountSent with privileged: true, and only when another code fired. Admin accounts are worth more to an attacker, so their signals count for more
+2

SDKs

LoginLlama provides official SDKs for popular programming languages. SDKs handle authentication, error handling, and provide type-safe interfaces.

Node.js

Official

Installation

npm install loginllama@^3.1.0
import { LoginLlama } from 'loginllama';

const loginllama = new LoginLlama({
  apiKey: process.env.LOGINLLAMA_API_KEY
});

// Use middleware for automatic IP/User-Agent detection
app.use(loginllama.middleware());

// Pre + Post Auth Pattern - Most complete protection
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // Step 1: Pre-auth check (before verifying credentials)
    const preCheck = await loginllama.check(email, {
      authenticationOutcome: 'pending'  // Mark as pre-auth check
    });

    // Block high-risk attempts before authentication
    if (preCheck.risk_score > 7) {
      return res.status(403).json({ error: 'Login blocked' });
    }

    // Step 2: Authenticate with your system
    const authResult = await authenticate(email, password);

    // Step 3: Report outcome to LoginLlama
    if (authResult.success) {
      await loginllama.reportSuccess(authResult.user.id);
      return res.json({ success: true });
    } else {
      // Report failed auth - helps detect brute force attacks
      await loginllama.reportFailure(email);
      return res.status(401).json({ error: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('LoginLlama error:', error);
    return res.json({ success: true }); // Fail open on errors
  }
});

Python

Official

Installation

pip install loginllama==2.0.0
from loginllama import LoginLlama, AuthenticationOutcome
from flask import Flask, request

app = Flask(__name__)
loginllama = LoginLlama(api_token=os.environ.get('LOGINLLAMA_API_KEY'))

# Use middleware for automatic IP/User-Agent detection
@app.before_request
def setup_loginllama():
    loginllama.middleware()()

# Pre + Post Auth Pattern
@app.route('/login', methods=['POST'])
def login():
    email = request.form['email']
    password = request.form['password']

    try:
        # Step 1: Pre-auth check
        pre_check = loginllama.check(
            email,
            authentication_outcome='pending'
        )

        if pre_check.risk_score > 7:
            return {'error': 'Login blocked'}, 403

        # Step 2: Authenticate
        auth_result = authenticate(email, password)

        # Step 3: Report outcome
        if auth_result.success:
            loginllama.report_success(auth_result.user.id)
            return {'success': True}
        else:
            loginllama.report_failure(email)
            return {'error': 'Invalid credentials'}, 401
    except Exception as error:
        print(f'LoginLlama error: {error}')
        return {'success': True}  # Fail open on errors

PHP

Official

Installation

composer require joshghent/loginllama:^2.0
<?php
use LoginLlama\LoginLlama;
use LoginLlama\AuthenticationOutcome;

$loginllama = new LoginLlama(getenv('LOGINLLAMA_API_KEY'));

// Pre + Post Auth Pattern
function handleLogin($email, $password) {
    global $loginllama;

    try {
        // Step 1: Pre-auth check
        $preCheck = $loginllama->check($email, [
            'authenticationOutcome' => AuthenticationOutcome::PENDING
        ]);

        if ($preCheck['risk_score'] > 7) {
            return ['error' => 'Login blocked'];
        }

        // Step 2: Authenticate
        $authResult = authenticate($email, $password);

        // Step 3: Report outcome
        if ($authResult->success) {
            $loginllama->reportSuccess($authResult->user->id);
            return ['success' => true];
        } else {
            $loginllama->reportFailure($email);
            return ['error' => 'Invalid credentials'];
        }
    } catch (Exception $error) {
        error_log('LoginLlama error: ' . $error->getMessage());
        return ['success' => true]; // Fail open
    }
}
?>

SDK Features

  • • Type-safe interfaces with auto-completion
  • • Automatic request signing and authentication
  • • Built-in error handling and retries
  • • Support for both async/await and promises
  • • Environment variable support out of the box

Code Examples

Real-world integration examples for common frameworks and use cases.

Express.js Middleware

Add suspicious login detection to your Express.js authentication flow:

import { LoginLlama } from 'loginllama';

const loginllama = new LoginLlama({
  apiKey: process.env.LOGINLLAMA_API_KEY
});

// Use middleware for automatic IP/User-Agent detection
app.use(loginllama.middleware());

// Pre + Post Auth Pattern - Most complete protection
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    // Step 1: Pre-auth check (before verifying credentials)
    const preCheck = await loginllama.check(email, {
      authenticationOutcome: 'pending'  // Mark as pre-auth check
    });

    // Block high-risk attempts before authentication
    if (preCheck.risk_score > 7) {
      return res.status(403).json({ error: 'Login blocked' });
    }

    // Step 2: Authenticate with your system
    const authResult = await authenticate(email, password);

    // Step 3: Report outcome to LoginLlama
    if (authResult.success) {
      await loginllama.reportSuccess(authResult.user.id);
      return res.json({ success: true });
    } else {
      // Report failed auth - helps detect brute force attacks
      await loginllama.reportFailure(email);
      return res.status(401).json({ error: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('LoginLlama error:', error);
    return res.json({ success: true }); // Fail open on errors
  }
});

Next.js API Route

Integrate with Next.js API routes:

// Install: npm install loginllama@^3.1.0
import { LoginLlama } from 'loginllama';
import { NextRequest } from 'next/server';

const loginllama = new LoginLlama({
  apiKey: process.env.LOGINLLAMA_API_KEY!
});

export async function POST(req: NextRequest) {
  const { email } = await req.json();

  try {
    const result = await loginllama.check(email, {
      request: req
    });

    if (result.status === 'error' || result.risk_score > 5) {
      return Response.json(
        { error: 'Suspicious login detected', codes: result.codes },
        { status: 403 }
      );
    }

    return Response.json({ success: true });
  } catch (error) {
    console.error('LoginLlama error:', error);
    return Response.json({ success: true }); // Fail open
    }
}

Next.js Login Protection with LoginLlama

Use a server action to call LoginLlama after auth succeeds, then branch on the risk score before finalizing the login.

Step-by-step integration

1
Create a server action for LoginLlama

Move the LoginLlama call into a server action so API keys never touch the browser.

app/actions/check-login.ts

2
Read IP and user agent from server headers

Use Next.js headers to capture x-forwarded-for and user-agent for higher accuracy.

3
Call check_login with user identity

Send identity_key + email_address + request context. Fail open if the call fails.

4
Invoke the action after auth succeeds

In your login page or auth callback, call the action and branch on risk_score / status.

5
Decide your policy

Block high-risk logins, require MFA for medium-risk, and allow low-risk logins.

// app/actions/check-login.ts
'use server';

import { headers } from 'next/headers';
import { LoginLlama } from 'loginllama';

export async function checkLogin({ user, userAgent }) {
  const loginllama = new LoginLlama({
    apiKey: process.env.LOGINLLAMA_API_KEY
  });

  const requestHeaders = await headers();
  let ip = requestHeaders.get('x-forwarded-for') ||
    requestHeaders.get('x-real-ip') ||
    'Unavailable';

  if (ip.includes(',')) ip = ip.split(',')[0];

  const result = await loginllama.check_login({
    identity_key: user.id,
    email_address: user.email,
    ip_address: ip,
    user_agent: requestHeaders.get('user-agent') || userAgent
  });

  return result;
}

// app/(auth)/login/page.tsx
'use client';

import { checkLogin } from '@/app/actions/check-login';

async function handlePostAuth(user) {
  try {
    const result = await checkLogin({
      user,
      userAgent: window.navigator.userAgent
    });

    if (result.status === 'error' || result.risk_score > 5) {
      // Block login, require MFA, or show verification UX
      return;
    }

    // Continue login
  } catch (error) {
    // Fail open if LoginLlama is unavailable
  }
}

Supabase Auth Integration

Integrate LoginLlama with Supabase Auth using Edge Functions, auth hooks, or server-side API routes:

// supabase/functions/check-login/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? ''
  )

  // Get user from auth header
  const authHeader = req.headers.get('Authorization')!
  const token = authHeader.replace('Bearer ', '')
  const { data: { user } } = await supabase.auth.getUser(token)

  if (!user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    })
  }

  // Get IP and user agent from request
  const ip = req.headers.get('x-forwarded-for') || 'unknown'
  const userAgent = req.headers.get('user-agent') || ''

  // Check with LoginLlama
  const loginLlamaResponse = await fetch(
    'https://loginllama.app/api/v1/login/check',
    {
      method: 'POST',
      headers: {
        'X-API-KEY': Deno.env.get('LOGINLLAMA_API_KEY')!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        identity_key: user.email,
        ip_address: ip,
        user_agent: userAgent,
        email_address: user.email
      })
    }
  )

  const result = await loginLlamaResponse.json()

  // If high risk, you could:
  // 1. Sign out the user
  // 2. Require email verification
  // 3. Log to audit table
  // 4. Send alert to security team

  if (result.risk_score >= 8) {
    // Log suspicious login
    await supabase.from('audit_logs').insert({
      user_id: user.id,
      event: 'suspicious_login_blocked',
      risk_score: result.risk_score,
      risk_codes: result.codes,
      ip_address: ip
    })

    return new Response(
      JSON.stringify({
        error: 'Login blocked due to suspicious activity',
        risk_score: result.risk_score,
        codes: result.codes
      }),
      { status: 403, headers: { 'Content-Type': 'application/json' } }
    )
  }

  return new Response(
    JSON.stringify({ success: true, risk_score: result.risk_score }),
    { headers: { 'Content-Type': 'application/json' } }
  )
})

Setup Steps

1
Create Edge Function

Run: supabase functions new check-login

2
Add API Key Secret

Run: supabase secrets set LOGINLLAMA_API_KEY=your_api_key_here

3
Deploy Function

Run: supabase functions deploy check-login

4
Call from Client

Invoke the function after sign-in using the Supabase client or auth hooks

Auth0 Actions Integration

Use Auth0's Post-Login Actions to check every login attempt with LoginLlama. Actions run in the authentication pipeline and can block logins, require MFA, or add custom claims to tokens.

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  // Only check logins for specific applications (optional)
  // if (event.client.name !== 'My Web App') return;

  try {
    const axios = require('axios');

    // Call LoginLlama API
    const response = await axios.post(
      'https://loginllama.app/api/v1/login/check',
      {
        identity_key: event.user.email,
        ip_address: event.request.ip,
        user_agent: event.request.user_agent,
        geo_country: event.request.geoip?.countryName,
        geo_city: event.request.geoip?.cityName,
        email_address: event.user.email
      },
      {
        headers: {
          'X-API-KEY': event.secrets.LOGINLLAMA_API_KEY,
          'Content-Type': 'application/json'
        },
        timeout: 5000 // 5 second timeout
      }
    );

    const apiResponse = response.data;
    const attrs = apiResponse?.data?.attributes || {};
    const meta = apiResponse?.meta || {};
    const result = {
      status: attrs.status === 'pass' ? 'success' : 'error',
      risk_score: attrs.risk_score || 0,
      codes: attrs.risk_codes || [],
      environment: meta.environment,
      unrecognized_device: attrs.unrecognized_device
    };

    // Store risk score in user metadata for later use
    api.user.setAppMetadata('last_login_risk_score', result.risk_score);
    api.user.setAppMetadata('last_login_codes', result.codes);

    // Add custom claim to token for your application to use
    api.idToken.setCustomClaim('login_risk_score', result.risk_score);
    api.accessToken.setCustomClaim('login_risk_score', result.risk_score);

    // Handle high-risk logins
    if (result.status === 'error' || result.risk_score >= 8) {
      // Block the login
      api.access.deny(
        `Login blocked: Suspicious activity detected (${result.codes.join(', ')})`
      );
      return;
    }

    // Require MFA for medium-risk logins
    if (result.risk_score >= 5) {
      api.multifactor.enable('any', {
        allowRememberBrowser: false
      });

      // Optionally, add a custom claim to show why MFA was required
      api.idToken.setCustomClaim('mfa_reason', 'elevated_risk_score');
    }

    // For new device logins with moderate risk, you could:
    if (result.unrecognized_device && result.risk_score >= 3) {
      // Send a custom email notification (via Auth0 email template)
      // or trigger a webhook to your backend
      console.log('New device login detected for user:', event.user.email);
    }

  } catch (error) {
    // IMPORTANT: Fail open - don't block logins if LoginLlama is down
    console.error('LoginLlama check failed:', error.message);

    // Optionally log to your monitoring service
    // api.redirect.sendUserTo('https://your-app.com/login-error');

    // Allow login to proceed
    return;
  }
};

/**
* Handler that will be invoked when this action is resuming after an external redirect.
* If your onExecutePostLogin function does not perform a redirect, this function can be safely ignored.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
// exports.onContinuePostLogin = async (event, api) => {
// };

Setup Steps

1
Create a New Action

In your Auth0 Dashboard, go to Actions > Library and click "Build Custom"

2
Name Your Action

Name it "LoginLlama Check" and select "Login / Post Login" as the trigger

3
Add Dependencies

Click the module icon and add "axios" as a dependency (or use native fetch in Node.js 18+)

4
Add Your API Key

Click the key icon to add a secret named "LOGINLLAMA_API_KEY" with your API key value

5
Paste the Code

Copy the code example above into the action editor

6
Deploy the Action

Click "Deploy" to save your action

7
Add to Login Flow

Go to Actions > Flows > Login and drag your "LoginLlama Check" action into the flow between "Start" and "Complete"

8
Apply Changes

Click "Apply" to activate the action in your login flow

What This Action Does:
  • •Blocks high-risk logins (risk score ≥ 8) with descriptive error messages
  • •Requires MFA for medium-risk logins (risk score ≥ 5)
  • •Adds risk score to tokens as custom claims for your app to use
  • •Stores risk data in user metadata for auditing
  • •Fails open on errors - never blocks users if LoginLlama is unavailable

Error Handling

The API uses standard HTTP status codes and returns detailed error messages to help you debug issues.

HTTP Status Codes

CodeStatusDescriptionSolution
200
SuccessRequest processed successfullyCheck the response status field for "success" or "error"
400
Bad RequestInvalid request parameters (Zod validation failed)Review request parameters and ensure they match the schema
401
UnauthorizedMissing or invalid API keyCheck that X-API-KEY header is set with a valid API key
402
Payment RequiredNo API credits remainingPurchase more credits or upgrade your plan
429
Too Many RequestsRate limit exceeded (100 requests per minute)Implement exponential backoff and respect X-RateLimit-Reset header
500
Internal Server ErrorAn error occurred processing your requestCheck your request format and try again. Contact support if it persists

Rate Limiting

API requests are rate limited to 100 requests per minute per API key (60 for test keys). Rate limit information is included in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 2026-06-15T14:35:00.000Z

Ready to Get Started?

Sign up for a free account and start protecting your users today.