DEV Community

Serhii
Serhii

Posted on Originally published at botservice.biz

Telegram SDK Integration in Laravel with Webhooks and Queues

Telegram SDK Integration in Laravel

This guide shows how to add Telegram Bot API support to a Laravel application without pulling in third‑party SDKs like nutgram or irazasyed. It uses Laravel’s native HTTP client, a webhook endpoint, and a queue for asynchronous message processing.

Why this approach?

  • No magic – all logic lives in plain PHP/Laravel code.
  • Idempotent – each update_id is tracked so duplicate messages don’t cause duplicates.
  • Testable – you can inject fake Telegram updates during testing.
  • Scalable – queue jobs keep your request lifecycle decoupled from the HTTP response time.

1. Configuration

Store your bot credentials in environment variables (never hard‑coded).

# .env
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTU
TELEGRAM_CHAT_ID=12345
Enter fullscreen mode Exit fullscreen mode

In config/telegram.php (optional helper):

// config/telegram.php
return [
    'token' => env('TELEGRAM_BOT_TOKEN'),
    'chat_id' => env('TELEGRAM_CHAT_ID'),
];
Enter fullscreen mode Exit fullscreen mode

2. Webhook Route

Define a route that receives POST data from Telegram:

// routes/web.php
use Illuminate\HttpRequest;
use App\HttpMiddleware  elegramMiddleware;

Route::post('/webhook', function (HttpRequest $request) {
    // Verify the secret token if you added one
    if (!hash_equals(env('TELEGRAM_SECRET'), $request->header('X-Telegram-Bot-Api-Secret'))) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    // Parse the incoming JSON
    $payload = json_decode($request->all(), true);

    // Validate basic shape
    if (!isset($payload['message'])) {
        return response()->json(['error' => 'Missing message'], 400);
    }

    // Dispatch the update to a queue
    \\(function()\)
    ->processUpdate($payload)
    ->onQueue('telegram_updates');

    return response()->json(['status' => 'received']);
});
Enter fullscreen mode Exit fullscreen mode

3. Middleware & Initialization

A simple middleware ensures every request goes through Telegram-specific validation or logging.

// app/Http/Middleware/TelegramMiddleware.php
class TelegramMiddleware extends HttpMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        // You could log, rate-limit, or attach context here
        $context = ['bot_token' => env('TELEGRAM_BOT_TOKEN')];
        return $next();
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it early in Kernel.php:

// App/Kernel.php
protected $middlewareGroups = [
    'web' => [
        \Illuminate\HttpMiddleware
equests(\Illuminate\Http
equests::only(\String::toArray(__MIME_TYPE))),
        \TelegramMiddleware::class,
    ],
];
Enter fullscreen mode Exit fullscreen mode

4. Queue Job – Processing Updates

The core logic runs inside a background job. Each update is checked for duplicates using its update_id.

// app/Jobs/TelegramUpdateJob.php
use Illuminateoundationsusackground::BackgroundJob;
use Illuminate
otifications
otification;
use App
otifications    elegramNotification;

class TelegramUpdateJob extends BackgroundJob
{
    protected $routeName = 'telegram.update';

    public function handle(array $data)
    {
        $updateId = $data['update_id'];
        $chatId   = $data['chat_id'] ?? null;
        $text     = $data['message']['text'] ?? '';
        $type     = $data['message']['type'] ?? null;

        // Idempotency check – skip if already processed
        if ($this->alreadyProcessed($updateId)) {
            return;
        }

        try {
            // Create a notification (or push message directly)
            telegramNotification::create($chatId, $text, $type)->send();
            $this->markAsCompleted();
        } catch (\Exception $e) {
            // Log but do not retry automatically – let the caller decide
            logger::error('Failed to process Telegram update #{$updateId}', $e);
            // Optionally requeue or move to a dead-letter queue
        }
    }

    private function alreadyProcessed(string $updateId): bool
    {
        // Store processed IDs in Redis, DB, or a file.
        // For simplicity, a static array works for demo purposes.
        return isset($processedUpdates[$updateId]);
    }

    // Persist processed IDs (implement according to your storage strategy)
    public function recordProcessed(string $updateId): void
    {
        $processedUpdates[$updateId] = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Storing Processed IDs

Use a lightweight store. Here’s a simple in‑memory array for demonstration (swap for Redis/DB in production):

// config/telegram.php (add)
'processed_ids' => ['redis://localhost:6379/0'] // or [] for pure PHP
Enter fullscreen mode Exit fullscreen mode

Then replace $this->alreadyProcessed() with a Redis SETNX call.

5. Testing with Fake Updates

Unit‑test the job by feeding it synthetic Telegram payloads. Use PHPUnit.

// tests/Feature/TelegramUpdateJobTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;

class TelegramUpdateJobTest extends Tests\TestCase
{
    use RefreshDatabase;

    /** @test */
    public function job_processes_a_text_message()
    {
        $job = new TelegramUpdateJob();
        $payload = [
            'update_id' => '123456789',
            'chat_id' => '987654321',
            'message' => [
                'type' => 'text',
                'text' => 'Hello from Laravel!',
            ],
            'message_type' => 'edit', // optional
        ];

        $job->handle($payload);

        // Verify the notification was attempted
        $this->assertNotNull(telegramNotification::find('987654321'));
    }

    /** @test */
    public function job_skips_duplicate_update()
    {
        $job = new TelegramUpdateJob();
        $payload = [
            'update_id' => '111222333',
            'chat_id' => '987654321',
            'message' => [
                'type' => 'text',
                'text' => 'Duplicate message',
            ],
        ];

        // First run – should succeed
        $job->handle($payload);

        // Second run with same ID – should be skipped
        $job->handle($payload);

        // No exception thrown
        $this->assertTrue(true);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the tests:

php artisan test --filter=TelegramUpdateJobTest
Enter fullscreen mode Exit fullscreen mode

6. Production Notes

Concern Recommendation
Secret verification Compare the X-Telegram-Bot-Api-Secret header against a server-side hash.
Rate limiting Throttle incoming webhook calls per IP or per chat to avoid abuse.
Storage Replace the in‑memory $processed_ids array with Redis SETNX or a database table for durability across restarts.
Error handling Log failures and optionally alert on high failure rates. Consider moving failed updates to a separate queue for manual inspection.
HTTPS Always serve the webhook over HTTPS. Telegram requires it.
Minimum version Ensure your Laravel version supports the latest HTTP client features (Illuminate\Http\Client).

7. Quick Start Summary

  1. Add TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID to .env.
  2. Define the /webhook route and verify the secret header.
  3. Create a TelegramUpdateJob that checks for duplicates and dispatches notifications.
  4. Register the job as a background task (Laravel handles queuing automatically).
  5. Write feature tests that feed synthetic Telegram updates to the job.

With this pattern you have a lightweight, testable, and production-ready Telegram integration that stays within the Laravel ecosystem.

For more details on building Telegram bots and Mini Apps, see BotCreator.

Top comments (0)