Integration guide

This section covers integrating tirreno into your application.

Why send events to tirreno?

tirreno analyzes user events to detect threats and calculate risk scores. Use cases:

tirreno tracks per-user metrics: devices per day, IPs per day, sessions, events per session.

IP Enrichment API: tirreno provides an API for IP geolocation and threat intelligence. The open-source Community Edition includes an optional IP enrichment pack (2,000 free API requests/month). For high-volume needs, contact tirreno for Enterprise options. See tirreno.com for pricing details.

Integration planning

Application Edition: For internal applications we recommend to use existing integrations. Check the list of available integrations or contact tirreno at team@tirreno.com for further details.

What to track

Fraud VectorHow tirreno DetectsEvents
Stolen credentialsMultiple IPs, unusual locationsaccount_login, account_login_fail
Account sharingConcurrent sessions, device changespage_view, account_login
Fake accountsDisposable emails, VPN/proxyaccount_registration
Data scrapingHigh volume, bot signaturespage_view, page_search
Privilege abuseOff-hours, sensitive operationsaccount_edit, field_edit

Where to integrate

Minimum:

Recommended:

Data you need

DataSourceRequired
User IDAuth systemYes
IP addressRequest headersYes
URLRequest pathYes
TimestampServer time (UTC)Yes
EmailUser profileRecommended
User agentHeadersRecommended

Technical notes

Performance:

Reliability:

Privacy:

Scalability:

Security considerations

When integrating tirreno, follow these security best practices:

  1. Install in private environment Deploy tirreno in a private network with controlled access
  2. Protect your API key Store in environment variables, never in code
  3. Use HTTPS Always send events over encrypted connections
  4. Don't log sensitive data Never include passwords, tokens, or PII in event payloads
  5. Fail open on errors Don't block users if tirreno is temporarily unavailable
  6. Set timeouts Use 3-5 second timeouts to prevent login delays
  7. Validate on your side tirreno is for monitoring, not input validation
  8. Send timestamps in UTC All eventTime values must be in UTC. Configure your tirreno instance timezone during initial setup or in Settings → Time zone. The dashboard will display events in your configured timezone, but all data sent via the API must use UTC

Quick start

Important: tirreno must be integrated on the backend only. Never send events from frontend JavaScript or mobile apps. Client-side code can be inspected, modified, or bypassed entirely — attackers could disable tracking, forge events, or extract your API key. Backend integration ensures event data cannot be tampered with and your API credentials remain secure.

The fastest way to integrate tirreno is using an official tracker library.

cURL (raw API):

curl -X POST https://your-tirreno-instance.com/sensor/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "key=your-api-key" \
  -d "userName=user123" \
  -d "emailAddress=user@example.com" \
  -d "ipAddress=192.168.1.100" \
  -d "url=/login" \
  -d "eventTime=2024-12-08 14:30:00.000" \
  -d "eventType=page_view"

PHP:

Requirements: cURL PHP extension

Installation:

composer require tirreno/tirreno-tracker

Or manually via file download:

require_once("TirrenoTracker.php");

Usage:

<?php

// Load object
require_once("TirrenoTracker.php");

$tirrenoUrl = "https://example.tld/sensor/"; // Sensor URL
$trackingId = "XXX"; // Tracking ID

// Create object
$tracker = new TirrenoTracker($tirrenoUrl, $trackingId);

// Override defaults of required params
$tracker->setUserName("johndoe42")
        ->setIpAddress("1.1.1.1")
        ->setUrl("/login")
        ->setUserAgent("Mozilla/5.0 (X11; Linux x86_64)")
        ->setEventTypeAccountLogin();

// Set optional params
$tracker->setFirstName("John")
        ->setBrowserLanguage("fr-FR,fr;q=0.9")
        ->setHttpMethod("POST");

// Track event
$tracker->track();

Python:

pip install tirreno_tracker

from tirreno_tracker import Tracker

tracker = Tracker('https://your-tirreno-instance.com', 'your-api-key')

# Track a login
event = tracker.create_event()

event.set_user_name(user_id) \
     .set_email_address(user_email) \
     .set_ip_address(ip_address) \
     .set_url(url_path) \
     .set_user_agent(user_agent) \
     .set_event_type_account_login()

tracker.track(event)

Node.js:

npm install @tirreno/tirreno-tracker

const Tracker = require('@tirreno/tirreno-tracker');

const tracker = new Tracker('https://your-tirreno-instance.com', 'your-api-key');

// Track a registration
const event = tracker.createEvent();

event.setUserName(userId)
     .setEmailAddress(userEmail)
     .setIpAddress(ipAddress)
     .setUrl(urlPath)
     .setUserAgent(userAgent)
     .setEventTypeAccountRegistration();

await tracker.track(event);

Event tracking best practices

Which events to track

Essential events (always track):

EventWhen to TrackWhy It Matters
account_loginSuccessful authenticationDetect account takeover
account_login_failFailed login attemptsDetect brute force attacks
account_registrationNew account creationDetect fake account creation
account_password_changePassword updatesDetect account compromise
account_email_changeEmail changesDetect account hijacking

Recommended events:

EventWhen to TrackWhy It Matters
page_viewKey page visitsBehavioral analysis
page_editContent modificationsDetect malicious edits
page_searchSearch queriesDetect reconnaissance
page_error4xx/5xx errorsDetect scanning/attacks
field_editData modificationField audit trail

Data quality guidelines

  1. Consistent user identifiers:
// Good - use permanent ID
$tracker->setUserName($user->id);

// Bad - don't use changing values
$tracker->setUserName($user->email);  // Emails can change
  1. Accurate timestamps:

The tracker libraries automatically set eventTime to the current UTC timestamp with milliseconds when you call track(). For manual timestamp handling, use the format Y-m-d H:i:s.v:

// PHP - include milliseconds
$eventTime = date('Y-m-d H:i:s.v');  // 2024-01-15 10:30:45.123
  1. Real IP addresses:
// Good - handle proxies correctly
function getRealIp(): string {
    $headers = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
    foreach ($headers as $header) {
        if (!empty($_SERVER[$header])) {
            $ips = explode(',', $_SERVER[$header]);
            return trim($ips[0]);
        }
    }
    return $_SERVER['REMOTE_ADDR'];
}

$tracker->setIpAddress(getRealIp());
  1. Complete user agent:
// Good - full user agent
$tracker->setUserAgent($_SERVER['HTTP_USER_AGENT']);

// Bad - truncated
$tracker->setUserAgent(substr($_SERVER['HTTP_USER_AGENT'], 0, 50));

Send all logged-in user events

Track page views and actions from authenticated users.

PHP:

session_start();

if (isset($_SESSION['user_id'])) {
    $tracker->setUserName((string) $_SESSION['user_id'])
            ->setEmailAddress($_SESSION['user_email'])
            ->setIpAddress($_SERVER['REMOTE_ADDR'])
            ->setUrl($_SERVER['REQUEST_URI'])
            ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
            ->setHttpMethod($_SERVER['REQUEST_METHOD'])
            ->setEventTypePageView();

    $tracker->track();
}

Node.js:

if (userId) {
    const event = tracker.createEvent();

    event.setUserName(userId)
         .setEmailAddress(userEmail)
         .setIpAddress(ipAddress)
         .setUrl(urlPath)
         .setUserAgent(userAgent)
         .setHttpMethod(httpMethod)
         .setHttpCode(httpCode.toString())
         .setEventTypePageView();

    await tracker.track(event);
}

Python:

if user_id:
    event = tracker.create_event()

    event.set_user_name(str(user_id)) \
         .set_email_address(user_email) \
         .set_ip_address(ip_address) \
         .set_url(url_path) \
         .set_user_agent(user_agent) \
         .set_http_method(http_method) \
         .set_http_code(str(http_code)) \
         .set_event_type_page_view()

    tracker.track(event)

Protecting the registration

Protect your registration flow from fake accounts, bots, and abuse.

Track registration events

PHP:

$userId = createUser($_POST['email'], $_POST['password'], $_POST['name']);

$tracker->setUserName((string) $userId)
        ->setEmailAddress($_POST['email'])
        ->setFullName($_POST['name'])
        ->setIpAddress($_SERVER['REMOTE_ADDR'])
        ->setUrl('/register')
        ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
        ->setUserCreated(date('Y-m-d H:i:s'))
        ->setEventTypeAccountRegistration();

$tracker->track();

header('Location: /dashboard');

Python:

user_id = create_user(email, password, name)

event = tracker.create_event()

event.set_user_name(str(user_id)) \
     .set_email_address(email) \
     .set_full_name(name) \
     .set_ip_address(ip_address) \
     .set_url('/register') \
     .set_user_agent(user_agent) \
     .set_event_type_account_registration()

tracker.track(event)

Node.js:

const userId = await createUser(email, password, name);

const event = tracker.createEvent();

event.setUserName(userId.toString())
     .setEmailAddress(email)
     .setFullName(name)
     .setIpAddress(ipAddress)
     .setUrl('/register')
     .setUserAgent(userAgent)
     .setEventTypeAccountRegistration();

await tracker.track(event);

Protecting the login

Secure your login flow against brute force attacks and credential stuffing.

Track login events

PHP:

$email = $_POST['email'];
$password = $_POST['password'];

$user = authenticateUser($email, $password);

if (!$user) {
    // Track failed login
    $tracker->setUserName($email)
            ->setEmailAddress($email)
            ->setIpAddress($_SERVER['REMOTE_ADDR'])
            ->setUrl('/login')
            ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
            ->setEventTypeAccountLoginFail();

    $tracker->track();

    die('Invalid credentials');
}

// Track successful login
$tracker->setUserName((string) $user['id'])
        ->setEmailAddress($user['email'])
        ->setIpAddress($_SERVER['REMOTE_ADDR'])
        ->setUrl('/login')
        ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
        ->setEventTypeAccountLogin();

$tracker->track();

session_start();
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard');

Python:

user = authenticate_user(email, password)

if not user:
    # Track failed login
    event = tracker.create_event()

    event.set_user_name(email) \
         .set_email_address(email) \
         .set_ip_address(ip_address) \
         .set_url('/login') \
         .set_user_agent(user_agent) \
         .set_event_type_account_login_fail()

    tracker.track(event)
    # Return error
else:
    # Track successful login
    event = tracker.create_event()

    event.set_user_name(str(user['id'])) \
         .set_email_address(user['email']) \
         .set_ip_address(ip_address) \
         .set_url('/login') \
         .set_user_agent(user_agent) \
         .set_event_type_account_login()

    tracker.track(event)

Node.js:

const user = await authenticateUser(email, password);

if (!user) {
    // Track failed login
    const event = tracker.createEvent();

    event.setUserName(email)
         .setEmailAddress(email)
         .setIpAddress(ipAddress)
         .setUrl('/login')
         .setUserAgent(userAgent)
         .setEventTypeAccountLoginFail();

    await tracker.track(event);
    // Return error
} else {
    // Track successful login
    const event = tracker.createEvent();

    event.setUserName(user.id.toString())
         .setEmailAddress(user.email)
         .setIpAddress(ipAddress)
         .setUrl('/login')
         .setUserAgent(userAgent)
         .setEventTypeAccountLogin();

    await tracker.track(event);
}

Block blacklisted users

Check the blacklist API before allowing login:

$email = $_POST['email'];
$password = $_POST['password'];

// Block known attackers before authentication
if ($blacklistService->isBlacklisted($email)) {
    die('Invalid credentials');
}

$user = authenticateUser($email, $password);

if (!$user) {
    $tracker->setUserName($email)
            ->setEmailAddress($email)
            ->setIpAddress($_SERVER['REMOTE_ADDR'])
            ->setUrl('/login')
            ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
            ->setEventTypeAccountLoginFail();

    $tracker->track();
    die('Invalid credentials');
}

// Also check authenticated user
if ($blacklistService->isBlacklisted((string) $user['id'])) {
    die('Invalid credentials');
}

// Track successful login
$tracker->setUserName((string) $user['id'])
        ->setEmailAddress($user['email'])
        ->setIpAddress($_SERVER['REMOTE_ADDR'])
        ->setUrl('/login')
        ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
        ->setEventTypeAccountLogin();

$tracker->track();

session_start();
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard');

Auto-ban abusive IPs

Use tirreno's IP analysis combined with the blacklist API for automatic protection.

Configure threshold settings

Before implementing auto-ban, configure and test the threshold settings in tirreno:

  1. Go to Rules page in tirreno dashboard
  2. Set Manual review threshold (e.g., 33) users below this score appear in review queue
  3. Set Auto-blacklisting threshold (e.g., 20) users below this score are automatically blacklisted
  4. Click Update to save settings

Middleware for IP-based blocking

PHP:

$ip = $_SERVER['REMOTE_ADDR'];

if ($blacklistService->isBlacklisted($ip)) {
    http_response_code(403);
    die('Access denied');
}

Python:

if blacklist_service.is_blacklisted(ip_address):
    # Return 403 Access denied
    pass

Node.js:

if (await blacklistService.isBlacklisted(ipAddress)) {
    // Return 403 Access denied
}

Field audit trail

Track changes to important user fields for compliance, security, and regulatory requirements. The fieldHistory parameter allows you to send detailed change records.

Field history format

Each field change object has these properties:

PropertyRequiredTypeDescription
field_idYesint/stringUnique identifier for the field
new_valueYesstringNew value
field_nameNostringHuman-readable field name
old_valueNostringPrevious value
parent_idNostringParent record ID (for nested data)
parent_nameNostringParent record name

Note: Missing required fields default to "unknown". All values are converted to strings.

PHP:

function trackFieldChanges($userId, $userEmail, $oldData, $newData, $tracker) {
    $trackableFields = [
        'city' => 'User city',
        'phone' => 'Phone number',
        'address' => 'Address',
        'company' => 'Company name',
    ];

    $changes = [];
    foreach ($trackableFields as $field => $fieldName) {
        $oldValue = $oldData[$field] ?? '';
        $newValue = $newData[$field] ?? '';

        if ($oldValue !== $newValue) {
            $changes[] = [
                'field_id' => crc32($field),
                'field_name' => $fieldName,
                'old_value' => (string) $oldValue,
                'new_value' => (string) $newValue,
                'parent_id' => '',
                'parent_name' => '',
            ];
        }
    }

    if (!empty($changes)) {
        $tracker->setUserName((string) $userId)
                ->setEmailAddress($userEmail)
                ->setIpAddress($_SERVER['REMOTE_ADDR'])
                ->setUrl($_SERVER['REQUEST_URI'])
                ->setUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '')
                ->setEventTypeFieldEdit()
                ->setFieldHistory($changes);

        $tracker->track();
    }
}

// Usage
$oldData = getUserById($userId);
updateUser($userId, $_POST);
trackFieldChanges($userId, $userEmail, $oldData, $_POST, $tracker);

Python:

def track_field_changes(user_id, user_email, old_data, new_data, tracker):
    trackable_fields = {
        'city': 'User city',
        'phone': 'Phone number',
        'address': 'Address',
        'company': 'Company name',
    }

    changes = []
    for field, field_name in trackable_fields.items():
        old_value = old_data.get(field, '')
        new_value = new_data.get(field, '')

        if old_value != new_value:
            changes.append({
                'field_id': hash(field) & 0xffffffff,
                'field_name': field_name,
                'old_value': str(old_value),
                'new_value': str(new_value),
                'parent_id': '',
                'parent_name': '',
            })

    if changes:
        event = tracker.create_event()

        event.set_user_name(str(user_id)) \
             .set_email_address(user_email) \
             .set_ip_address(ip_address) \
             .set_url(url_path) \
             .set_user_agent(user_agent) \
             .set_event_type_field_edit() \
             .set_field_history(changes)

        tracker.track(event)

# Usage
old_data = get_user_by_id(user_id)
update_user(user_id, new_data)
track_field_changes(user_id, user_email, old_data, new_data, tracker)

Node.js:

async function trackFieldChanges(userId, userEmail, oldData, newData, tracker) {
    const trackableFields = {
        city: 'User city',
        phone: 'Phone number',
        address: 'Address',
        company: 'Company name',
    };

    const changes = [];
    for (const [field, fieldName] of Object.entries(trackableFields)) {
        const oldValue = oldData[field] ?? '';
        const newValue = newData[field] ?? '';

        if (oldValue !== newValue) {
            changes.push({
                field_id: hashCode(field),
                field_name: fieldName,
                old_value: String(oldValue),
                new_value: String(newValue),
                parent_id: '',
                parent_name: ''
            });
        }
    }

    if (changes.length > 0) {
        const event = tracker.createEvent();

        event.setUserName(userId.toString())
             .setEmailAddress(userEmail)
             .setIpAddress(ipAddress)
             .setUrl(urlPath)
             .setUserAgent(userAgent)
             .setEventTypeFieldEdit()
             .setFieldHistory(changes);

        await tracker.track(event);
    }
}

// Usage
const oldData = await getUserById(userId);
await updateUser(userId, newData);
await trackFieldChanges(userId, userEmail, oldData, newData, tracker);

Tracking nested/related data:

// For related records (e.g., user addresses)
$changes = [];

foreach ($updatedAddresses as $address) {
    $original = $originalAddresses->find($address->id);

    foreach (['street', 'city', 'zip'] as $field) {
        if ($original->$field !== $address->$field) {
            $changes[] = [
                'field_id' => crc32($field),
                'field_name' => ucfirst($field),
                'old_value' => $original->$field,
                'new_value' => $address->$field,
                'parent_id' => (string) $address->id,      // Link to address record
                'parent_name' => "Address #{$address->id}", // Human-readable reference
            ];
        }
    }
}

Testing your integration

Manual testing checklist

  1. Verify API connectivity:
curl -X POST https://your-tirreno.com/sensor/ \
  -H "Api-Key: your-api-key" \
  -d "userName=test-user-123" \
  -d "emailAddress=test@example.com" \
  -d "ipAddress=203.0.113.50" \
  -d "url=/test" \
  -d "userAgent=Mozilla/5.0 Test" \
  -d "eventTime=2024-12-08 01:01:00.000" \
  -d "eventType=page_view"
  1. Check the Logbook:
  2. Check the Users page:
  3. Verify event types:
________________________________________________________________________________