# Ittybit webhooks with Laravel queues and events

Handle Ittybit webhook callbacks using Laravel's event system and queued listeners

When Ittybit finishes processing a task, it POSTs a webhook to your application. Instead of doing all the work inline in the controller, you can dispatch a Laravel event and let queued listeners handle each concern independently -- updating the database, generating follow-up tasks, notifying users. The webhook handler stays fast, your queue workers do the heavy lifting, and adding new behavior is just another listener.

## Configuration

Add your Ittybit credentials to `.env`:

```bash
ITTYBIT_API_KEY=your_ittybit_api_key
ITTYBIT_WEBHOOK_SECRET=your_webhook_secret
```

Register them in your services config:

```php
// config/services.php
'ittybit' => [
    'api_key' => env('ITTYBIT_API_KEY'),
    'webhook_secret' => env('ITTYBIT_WEBHOOK_SECRET'),
],
```

## Webhook route

Register the route in `routes/api.php` so it bypasses CSRF verification automatically.

```php
// routes/api.php
use App\Http\Controllers\IttybitWebhookController;

Route::post('/webhooks/ittybit', [IttybitWebhookController::class, 'handle']);
```

## Validate the signature

The controller verifies the HMAC signature, then dispatches an event and returns immediately. The queue handles everything else.

```php
// app/Http/Controllers/IttybitWebhookController.php
namespace App\Http\Controllers;

use App\Events\MediaProcessed;
use Illuminate\Http\Request;

class IttybitWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $secret = config('services.ittybit.webhook_secret');
        $signature = $request->header('Ittybit-Signature');

        $computed = hash_hmac('sha256', $request->getContent(), $secret);

        if (!$signature || !hash_equals($computed, $signature)) {
            abort(401, 'Invalid signature');
        }

        $payload = $request->all();

        MediaProcessed::dispatch($payload);

        return response()->json(['status' => 'ok'], 200);
    }
}
```

## The event

The event is a simple data carrier. Listeners receive it and decide what to do.

```php
// app/Events/MediaProcessed.php
namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MediaProcessed
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public array $payload)
    {
    }
}
```

## Queued listeners

Each listener implements `ShouldQueue` so it runs on your queue worker rather than blocking the webhook response. Generate them with artisan:

```bash
php artisan make:listener UpdateMediaRecord --event=MediaProcessed
php artisan make:listener GenerateThumbnail --event=MediaProcessed
php artisan make:listener NotifyUser --event=MediaProcessed
```

### UpdateMediaRecord

Updates your database with the task output.

```php
// app/Listeners/UpdateMediaRecord.php
namespace App\Listeners;

use App\Events\MediaProcessed;
use App\Models\Media;
use Illuminate\Contracts\Queue\ShouldQueue;

class UpdateMediaRecord implements ShouldQueue
{
    public function handle(MediaProcessed $event): void
    {
        $payload = $event->payload;

        $media = Media::where('task_id', $payload['id'])->first();

        if (!$media) {
            return;
        }

        if ($payload['status'] === 'completed') {
            $media->update([
                'status' => 'completed',
                'output_url' => $payload['output']['url'],
                'metadata' => [
                    'duration' => $payload['output']['duration'] ?? null,
                    'width' => $payload['output']['width'] ?? null,
                    'height' => $payload['output']['height'] ?? null,
                    'format' => $payload['output']['format'] ?? null,
                ],
            ]);
        } else {
            $media->update(['status' => 'failed']);
        }
    }
}
```

### GenerateThumbnail

Chains a follow-up Ittybit task to extract a thumbnail from the completed video.

```php
// app/Listeners/GenerateThumbnail.php
namespace App\Listeners;

use App\Events\MediaProcessed;
use App\Models\Media;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;

class GenerateThumbnail implements ShouldQueue
{
    public function handle(MediaProcessed $event): void
    {
        $payload = $event->payload;

        if ($payload['status'] !== 'completed') {
            return;
        }

        if (($payload['kind'] ?? null) !== 'video') {
            return;
        }

        $media = Media::where('task_id', $payload['id'])->first();

        if (!$media) {
            return;
        }

        $response = Http::withToken(config('services.ittybit.api_key'))
            ->post('https://api.ittybit.com/jobs', [
                'input' => $payload['output']['url'],
                'kind' => 'image',
                'options' => [
                    'start' => 2,
                    'width' => 640,
                    'format' => 'webp',
                ],
            ]);

        $media->update([
            'thumbnail_task_id' => $response->json('id'),
        ]);
    }
}
```

### NotifyUser

Sends a notification when processing finishes.

```php
// app/Listeners/NotifyUser.php
namespace App\Listeners;

use App\Events\MediaProcessed;
use App\Models\Media;
use App\Notifications\MediaReady;
use Illuminate\Contracts\Queue\ShouldQueue;

class NotifyUser implements ShouldQueue
{
    public function handle(MediaProcessed $event): void
    {
        $payload = $event->payload;

        if ($payload['status'] !== 'completed') {
            return;
        }

        $media = Media::where('task_id', $payload['id'])->first();

        if (!$media || !$media->user) {
            return;
        }

        $media->user->notify(new MediaReady($media));
    }
}
```

## Register the listeners

Bind the event to its listeners in your `EventServiceProvider`.

```php
// app/Providers/EventServiceProvider.php
use App\Events\MediaProcessed;
use App\Listeners\UpdateMediaRecord;
use App\Listeners\GenerateThumbnail;
use App\Listeners\NotifyUser;

protected $listen = [
    MediaProcessed::class => [
        UpdateMediaRecord::class,
        GenerateThumbnail::class,
        NotifyUser::class,
    ],
];
```

If you're on Laravel 11+ with automatic event discovery, you can skip this step -- Laravel resolves the listeners from their type-hinted `handle` method signatures.

## Running the queue

Start a queue worker so the listeners actually process:

<CodeGroup labels={["Development", "Production"]}>
```bash
php artisan queue:work
```

```bash
# Supervisor config for production
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
```

</CodeGroup>

## What happens at runtime

1. Ittybit POSTs to `/api/webhooks/ittybit` when a task completes
2. The controller validates the HMAC signature and returns `200` immediately
3. `MediaProcessed` is dispatched onto the queue
4. `UpdateMediaRecord` writes the output URL and metadata to the database
5. `GenerateThumbnail` fires a follow-up Ittybit task for a thumbnail
6. `NotifyUser` sends a notification to the media owner
7. When the thumbnail task completes, another webhook arrives and the cycle repeats

## See also

- [Video uploads with Laravel](/guides/video-upload-pipeline-with-laravel) -- upload pipeline with S3 and Ittybit tasks
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-task processing patterns
- [Ittybit Task API reference](/reference/tasks)