Ittybit webhooks with Laravel queues and events
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:
ITTYBIT_API_KEY=your_ittybit_api_key
ITTYBIT_WEBHOOK_SECRET=your_webhook_secret
Register them in your services config:
// 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.
// 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.
// 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.
// 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:
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.
// 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.
// 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.
// 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.
// 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:
php artisan queue:work# 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 What happens at runtime
- Ittybit POSTs to
/api/webhooks/ittybitwhen a task completes - The controller validates the HMAC signature and returns
200immediately MediaProcessedis dispatched onto the queueUpdateMediaRecordwrites the output URL and metadata to the databaseGenerateThumbnailfires a follow-up Ittybit task for a thumbnailNotifyUsersends a notification to the media owner- When the thumbnail task completes, another webhook arrives and the cycle repeats
See also
- Video uploads with Laravel — upload pipeline with S3 and Ittybit tasks
- Build a user upload pipeline — multi-task processing patterns
- Ittybit Task API reference