Laravel
Laravel

Introduction

Integrate Botbye bot protection into your Laravel application using middleware. This guide shows how to protect your Laravel routes with minimal configuration.

Installation

Install the SDK via Composer:

1
composer require botbye/botbye-php-sdk

You also need a PSR-18 HTTP client. Guzzle is the most common choice for Laravel:

1
composer require guzzlehttp/guzzle

Configuration

Register the Botbye client in your AppServiceProvider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Providers;

use Botbye\Client\BotbyeClient;
use Botbye\Client\BotbyeConfig;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(BotbyeClient::class, function ($app) {
            $config = new BotbyeConfig(
                // Use your project server-key
                serverKey: '00000000-0000-0000-0000-000000000000'
            );

            $httpClient = new Client(['timeout' => 2.0]);
            $factory = new HttpFactory();

            return new BotbyeClient(
                config: $config,
                httpClient: $httpClient,
                requestFactory: $factory,
                streamFactory: $factory,
            );
        });
    }

    public function boot(): void
    {
        //
    }
}

Usage

1. Create a middleware to evaluate requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Http\Middleware;

use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeValidationEvent;
use Botbye\Model\Headers;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BotbyeMiddleware
{
    public function __construct(private BotbyeClient $botbye) {}

    public function handle(Request $request, Closure $next): Response
    {
        $headers = Headers::fromArray($request->headers->all());

        // Extract the token from wherever you pass it: query param, header, body, etc.
        $token = $request->query('botbye_token', '');

        $response = $this->botbye->evaluate(new BotbyeValidationEvent(
            ip: $request->ip(),
            token: $token,
            headers: $headers->jsonSerialize(),
            requestMethod: $request->method(),
            requestUri: $request->getRequestUri(),
        ));

        if ($response->isBlocked()) {
            abort(403, 'Access denied');
        }

        return $next($request);
    }
}

2. Add the middleware to app/Http/Kernel.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middlewareAliases = [
        // ... other middleware
        'botbye' => \App\Http\Middleware\BotbyeMiddleware::class,
    ];
}

3. Apply the middleware to specific routes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

use Illuminate\Support\Facades\Route;

// Protect a single route
Route::post('/api/checkout', [CheckoutController::class, 'process'])
    ->middleware('botbye');

// Protect a group of routes
Route::middleware(['botbye'])->group(function () {
    Route::post('/api/login', [AuthController::class, 'login']);
    Route::post('/api/register', [AuthController::class, 'register']);
    Route::post('/api/checkout', [CheckoutController::class, 'process']);
});

// Protect all API routes
Route::middleware(['api', 'botbye'])->prefix('api')->group(function () {
    // Your API routes
});

There are three event types — validate, risk, and full — each suited for a different layer of your application.

validate — edge-level bot check

Use at the edge — middleware, route handler — when you just want to know: was this request made by a bot? No user or domain context needed. This is the middleware shown above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Http\Middleware;

use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeValidationEvent;
use Botbye\Model\Headers;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BotbyeMiddleware
{
    public function __construct(private BotbyeClient $botbye) {}

    public function handle(Request $request, Closure $next): Response
    {
        $headers = Headers::fromArray($request->headers->all());

        // Extract the token from wherever you pass it: query param, header, body, etc.
        $token = $request->query('botbye_token', '');

        $response = $this->botbye->evaluate(new BotbyeValidationEvent(
            ip: $request->ip(),
            token: $token,
            headers: $headers->jsonSerialize(),
            requestMethod: $request->method(),
            requestUri: $request->getRequestUri(),
        ));

        if ($response->isBlocked()) {
            abort(403, 'Access denied');
        }

        return $next($request);
    }
}

risk — domain-level risk scoring

Use inside services that already know the user: auth, payments, account management, etc. The purpose shifts from "is this a bot?" to "is something suspicious happening for this user?" — credential stuffing, account takeover, account sharing, logins from a new geo.

event and user are the key fields here — they define what action is being performed and who is performing it, which is what drives the risk score.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace App\Services;

use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeRiskScoringEvent;
use Botbye\Model\BotbyeUserInfo;
use Botbye\Model\EventStatus;
use Botbye\Model\Headers;
use Illuminate\Http\Request;

class BotbyeRiskService
{
    public function __construct(private BotbyeClient $botbye) {}

    public function evaluateLogin(Request $request, string $userId, string $email, bool $loginSucceeded): void
    {
        $headers = Headers::fromArray($request->headers->all());

        $response = $this->botbye->evaluate(new BotbyeRiskScoringEvent(
            ip: $request->ip(),
            headers: $headers->jsonSerialize(),
            user: new BotbyeUserInfo(
                accountId: $userId,
                email: $email,
            ),
            eventType: 'login',
            eventStatus: $loginSucceeded ? EventStatus::SUCCESSFUL : EventStatus::FAILED,
            botbyeResult: null,
        ));

        if ($response->decision === 'BLOCK') {
            // Lock account, trigger MFA, send alert, etc.
        }
    }
}

Linking validate and risk events

When the same request is evaluated at two layers — for example, once at the edge (validate) and then again inside a domain service (risk) — BotBye can link both events and display them as a single event in the dashboard.

Step 1 — edge layer (middleware or route handler): run validate and capture the result:

1
2
3
4
5
6
7
8
9
10
11
<?php
// e.g. in BotbyeMiddleware::handle()
$edgeResponse = $this->botbye->evaluate(new BotbyeValidationEvent(
    ip: $request->ip(),
    token: $request->query('botbye_token', ''),
    headers: Headers::fromArray($request->headers->all())->jsonSerialize(),
    requestMethod: $request->method(),
    requestUri: $request->getRequestUri(),
));
$edgeBotbyeResult = $edgeResponse->botbyeResult;
// Pass $edgeBotbyeResult downstream — a request attribute, function argument, shared context, etc.

Step 2 — domain service (auth, payment, account management): pass it as botbyeResult in the risk call:

1
2
3
4
5
6
7
8
9
10
<?php
// e.g. in AuthService or BotbyeRiskService
$riskResponse = $this->botbye->evaluate(new BotbyeRiskScoringEvent(
    ip: $request->ip(),
    headers: Headers::fromArray($request->headers->all())->jsonSerialize(),
    user: new BotbyeUserInfo(accountId: $userId, email: $email),
    eventType: 'login',
    eventStatus: $loginSucceeded ? EventStatus::SUCCESSFUL : EventStatus::FAILED,
    botbyeResult: $edgeBotbyeResult,
));

botbyeResult is null when absent — in that case, omit or pass null and the events will be recorded independently.

full — edge check and domain scoring in one call

Use when you have all context at once: raw request, token, user, and event. A login endpoint is a typical example — it receives the HTTP request and immediately knows the user and outcome.

Equivalent to running validate and risk in a single call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

namespace App\Http\Controllers;

use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeFullEvent;
use Botbye\Model\BotbyeUserInfo;
use Botbye\Model\EventStatus;
use Botbye\Model\Headers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    public function __construct(private BotbyeClient $botbye) {}

    public function login(Request $request): JsonResponse
    {
        $email = $request->input('email');
        $password = $request->input('password');

        $user = User::where('email', $email)->first();
        $loginSucceeded = $user && Hash::check($password, $user->password);

        $headers = Headers::fromArray($request->headers->all());

        $result = $this->botbye->evaluate(new BotbyeFullEvent(
            ip: $request->ip(),
            token: $request->query('botbye_token', ''),
            headers: $headers->jsonSerialize(),
            requestMethod: $request->method(),
            requestUri: $request->getRequestUri(),
            user: new BotbyeUserInfo(
                accountId: $user?->id ?? 'unknown',
                email: $email,
            ),
            eventType: 'login',
            eventStatus: $loginSucceeded ? EventStatus::SUCCESSFUL : EventStatus::FAILED,
        ));

        if ($result->isBlocked()) {
            abort(403, 'Access denied');
        }

        // Proceed with login
        return response()->json(['status' => 'success']);
    }
}

Advanced Configuration

Custom HTTP Client

Configure a custom HTTP client with specific timeouts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace App\Providers;

use Botbye\Client\BotbyeClient;
use Botbye\Client\BotbyeConfig;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(BotbyeClient::class, function ($app) {
            $config = new BotbyeConfig(
                serverKey: '00000000-0000-0000-0000-000000000000',
            );

            $httpClient = new Client([
                'timeout' => 2.0,
                'connect_timeout' => 1.0,
            ]);

            $factory = new HttpFactory();

            return new BotbyeClient(
                config: $config,
                httpClient: $httpClient,
                requestFactory: $factory,
                streamFactory: $factory,
            );
        });
    }
}

Logging Integration

Integrate with Laravel's logging system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace App\Providers;

use Botbye\Client\BotbyeClient;
use Botbye\Client\BotbyeConfig;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(BotbyeClient::class, function ($app) {
            $config = new BotbyeConfig(
                serverKey: '00000000-0000-0000-0000-000000000000'
            );

            $httpClient = new Client(['timeout' => 2.0]);
            $factory = new HttpFactory();

            // Use Laravel's PSR-3 compatible logger
            return new BotbyeClient(
                config: $config,
                httpClient: $httpClient,
                requestFactory: $factory,
                streamFactory: $factory,
                logger: Log::channel('botbye'),
            );
        });
    }
}

Configure the log channel in config/logging.php:

1
2
3
4
5
6
7
8
9
10
'channels' => [
    // ... other channels

    'botbye' => [
        'driver' => 'daily',
        'path' => storage_path('logs/botbye.log'),
        'level' => 'warning',
        'days' => 14,
    ],
],

Best Practices

1. Selective Protection - Only apply middleware to routes that need protection

2. Custom Fields - You can pass user context for better analysis

3. Logging - Monitor Botbye errors and blocked requests

Testing

Mock the Botbye client in your tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

namespace Tests\Feature;

use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeEvaluateResponse;
use Botbye\Model\Decision;
use Tests\TestCase;

class ProtectedRouteTest extends TestCase
{
    public function test_allowed_request()
    {
        $mockClient = $this->mock(BotbyeClient::class);

        $mockClient->shouldReceive('evaluate')
            ->once()
            ->andReturn(new BotbyeEvaluateResponse(
                decision: Decision::ALLOW,
                riskScore: 0.05,
                signals: [],
                scores: ['bot' => 0.05],
            ));

        $response = $this->post('/api/checkout', [
            'item' => 'test',
        ]);

        $response->assertStatus(200);
    }

    public function test_blocked_request()
    {
        $mockClient = $this->mock(BotbyeClient::class);

        $mockClient->shouldReceive('evaluate')
            ->once()
            ->andReturn(new BotbyeEvaluateResponse(
                decision: Decision::BLOCK,
                riskScore: 0.95,
                signals: ['AutomationTool'],
                scores: ['bot' => 0.95],
            ));

        $response = $this->post('/api/checkout', [
            'item' => 'test',
        ]);

        $response->assertStatus(403);
    }
}

Examples of BotBye API responses

Blocked (bot detected):

1
2
3
4
5
6
7
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "BLOCK",
  "risk_score": 0.95,
  "scores": { "bot": 0.95 },
  "signals": ["AutomationTool"]
}

Allowed:

1
2
3
4
5
6
7
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "ALLOW",
  "risk_score": 0.05,
  "scores": { "bot": 0.05, "ato": 0.02 },
  "signals": []
}

Challenge:

1
2
3
4
5
6
7
8
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "CHALLENGE",
  "risk_score": 0.65,
  "scores": { "bot": 0.65 },
  "signals": ["SuspiciousFingerprint"],
  "challenge": { "type": "captcha", "token": "..." }
}

Invalid server-key:

1
2
3
4
{
  "decision": "ALLOW",
  "error": { "message": "[BotBye] Bad Request: Invalid Server Key" }
}