Laravel Third-Party Integrations: Slack, Twilio SMS, HubSpot, and Zapier Webhooks
Connect your Laravel SaaS to the tools your team and customers already use — send Slack alerts, trigger SMS via Twilio, sync leads to HubSpot, and expose a webhook system so users can connect to Zapier and Make.com.
Why Integrations Are a Growth Lever, Not Just a Feature
For B2B SaaS, integrations are not optional extras — they are a key driver of retention and expansion. Products that fit into a customer's existing workflow (Slack, HubSpot, their internal tools) see significantly lower churn than products that exist as silos. Every integration you add is also an SEO and discovery opportunity: customers searching for "slack integration for [your category]" will find you.
This guide covers four of the most-requested integrations for Laravel SaaS products: Slack notifications, Twilio SMS, HubSpot CRM sync, and an outgoing webhook system that lets customers connect to Zapier and Make.com without you building native integrations for every tool.
Slack Notifications
Slack notifications fall into two categories: internal alerts to your team (new signup, payment failed, error spike) and customer-facing notifications to your users' Slack workspaces. Both use the same underlying mechanism — Slack's Incoming Webhooks or the Slack API — but the setup differs.
Internal Team Alerts
Create an Incoming Webhook URL in your Slack workspace (Slack → Apps → Incoming Webhooks). Add it to .env:
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxx
Create a reusable notification channel:
// app/Notifications/NewCustomerSignedUp.php
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\SlackMessage;
class NewCustomerSignedUp extends Notification
{
public function __construct(private Team $team) {}
public function via($notifiable): array { return ['slack']; }
public function toSlack($notifiable): SlackMessage
{
return (new SlackMessage)
->success()
->content("New customer signed up!")
->attachment(function ($attachment) {
$attachment
->title($this->team->name)
->fields([
'Email' => $this->team->owner->email,
'Plan' => $this->team->plan ?? 'Trial',
'Source' => request()->header('referer', 'direct'),
]);
});
}
}
// Dispatch (using a Slack-notifiable route)
Notification::route('slack', config('services.slack.webhook_url'))
->notify(new NewCustomerSignedUp($team));
Customer-Facing Slack Integration
To let your customers connect their Slack workspace to your product, use Slack OAuth — users authorise your app to post to their workspace, and you store the resulting access token:
// routes/web.php
Route::get('/integrations/slack/connect', [SlackIntegrationController::class, 'redirect']);
Route::get('/integrations/slack/callback', [SlackIntegrationController::class, 'callback']);
// SlackIntegrationController
public function redirect()
{
$params = http_build_query([
'client_id' => config('services.slack.client_id'),
'scope' => 'incoming-webhook,chat:write',
'redirect_uri' => route('slack.callback'),
'state' => csrf_token(),
]);
return redirect("https://slack.com/oauth/v2/authorize?{$params}");
}
public function callback(Request $request)
{
$response = Http::post('https://slack.com/api/oauth.v2.access', [
'client_id' => config('services.slack.client_id'),
'client_secret' => config('services.slack.client_secret'),
'code' => $request->code,
'redirect_uri' => route('slack.callback'),
])->json();
auth()->user()->currentTeam->slackIntegration()->updateOrCreate([], [
'access_token' => $response['access_token'],
'webhook_url' => $response['incoming_webhook']['url'],
'channel' => $response['incoming_webhook']['channel'],
'workspace_name' => $response['team']['name'],
]);
return redirect('/integrations')->with('success', 'Slack connected successfully.');
}
Twilio SMS Notifications
SMS is the highest-open-rate notification channel — 98% open rate vs 20% for email. Use it for time-sensitive alerts: appointment reminders, security codes, critical system alerts, and payment failure notices.
composer require twilio/sdk
TWILIO_ACCOUNT_SID=ACxxx
TWILIO_AUTH_TOKEN=xxx
TWILIO_FROM=+44xxxxxxxxxx
Create a Twilio notification channel and a reusable service:
// app/Services/SmsService.php
namespace App\Services;
use Twilio\Rest\Client;
class SmsService
{
public function __construct(private Client $twilio) {}
public function send(string $to, string $message): void
{
$this->twilio->messages->create($to, [
'from' => config('services.twilio.from'),
'body' => $message,
]);
}
}
// Bind in AppServiceProvider
$this->app->singleton(SmsService::class, fn () => new SmsService(
new Client(config('services.twilio.sid'), config('services.twilio.token'))
));
// Use anywhere in your app
app(SmsService::class)->send($user->phone, "Your verification code is: {$code}");
// Or as a Laravel notification channel
class AppointmentReminder extends Notification
{
public function via($notifiable): array { return ['twilio']; }
public function toTwilio($notifiable): string
{
return "Reminder: Your appointment is tomorrow at {$this->appointment->time->format('H:i')}. Reply CANCEL to cancel.";
}
}
HubSpot CRM Sync
If your sales team uses HubSpot, syncing new signups automatically saves hours of manual CRM entry per week. The HubSpot API is REST-based and well-documented. Use Laravel's Http facade to create contacts:
// app/Listeners/SyncNewUserToHubspot.php
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Support\Facades\Http;
class SyncNewUserToHubspot
{
public function handle(UserRegistered $event): void
{
$user = $event->user;
Http::withToken(config('services.hubspot.api_key'))
->post('https://api.hubapi.com/crm/v3/objects/contacts', [
'properties' => [
'email' => $user->email,
'firstname' => $user->first_name,
'lastname' => $user->last_name,
'company' => $user->currentTeam->name,
'lifecyclestage' => 'lead',
'hs_lead_status' => 'NEW',
],
]);
}
}
Register the listener in EventServiceProvider. Queue it so a HubSpot API delay does not slow down your registration flow. When a user upgrades to a paid plan, update their lifecycle stage to customer:
Http::withToken(config('services.hubspot.api_key'))
->patch("https://api.hubapi.com/crm/v3/objects/contacts/{$contactId}", [
'properties' => [
'lifecyclestage' => 'customer',
'amount' => $subscription->price / 100,
],
]);
Outgoing Webhooks: Let Users Connect to Zapier and Make
Building native integrations for every tool your customers use (Zapier, Make.com, Notion, Airtable) is impractical. The better approach: build a generic outgoing webhook system that lets your users register their own endpoint URLs and select which events to receive. Zapier and Make support triggering automations from webhook payloads — so one webhook system unlocks thousands of integrations for your users.
// Schema: webhooks table
Schema::create('webhooks', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('url');
$table->string('secret'); // HMAC signing secret
$table->json('events'); // ["task.created", "task.updated"]
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// app/Services/WebhookDispatcher.php
class WebhookDispatcher
{
public function dispatch(Team $team, string $event, array $payload): void
{
$webhooks = $team->webhooks()->where('is_active', true)->get();
foreach ($webhooks as $webhook) {
if (! in_array($event, $webhook->events)) continue;
$body = json_encode([
'event' => $event,
'timestamp' => now()->toIsoString(),
'data' => $payload,
]);
$signature = hash_hmac('sha256', $body, $webhook->secret);
SendWebhook::dispatch($webhook->url, $body, $signature)->onQueue('webhooks');
}
}
}
// Queued job that delivers the webhook
class SendWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue;
public int $tries = 5;
public array $backoff = [60, 300, 900, 3600]; // retry after 1m, 5m, 15m, 1h
public function __construct(
private string $url,
private string $body,
private string $signature,
) {}
public function handle(): void
{
$response = Http::timeout(10)
->withHeaders([
'Content-Type' => 'application/json',
'X-Webhook-Signature'=> "sha256={$this->signature}",
])
->send('POST', $this->url, ['body' => $this->body]);
if (! $response->successful()) {
throw new \RuntimeException("Webhook delivery failed: HTTP {$response->status()}");
}
}
}
Sign every webhook payload with HMAC-SHA256 using a per-webhook secret. Recipients can verify the signature to confirm the payload came from your server and was not tampered with — this is the pattern used by Stripe, GitHub, and Shopify.
Trigger webhooks wherever your events happen:
// In TaskController::store()
$task = Task::create($validated);
app(WebhookDispatcher::class)->dispatch($team, 'task.created', $task->toArray());
Frequently Asked Questions
How do I test webhooks locally?
Use webhook.site or ngrok. Webhook.site gives you a public URL that displays incoming requests — perfect for verifying your payload format during development. For testing the full delivery flow including retries, use Stripe CLI's webhook forwarding feature (even for non-Stripe webhooks) or set up a local ngrok tunnel and register it as the endpoint URL.
Should I store webhook delivery logs?
Yes, for at least 7 days. Users will eventually ask "why did this automation not trigger?" and you need delivery logs to diagnose the issue. Log the URL, event, payload, response status, response body, and delivery timestamp for every attempt. Surface this in a "Webhook Logs" section of your integrations settings page — it is one of those small features that significantly reduces support volume.
How do I handle HubSpot API rate limits?
HubSpot's API has a rate limit of 100 requests per 10 seconds for free/starter plans. Dispatching all CRM syncs via a queued listener with a rate-limited queue worker (set --sleep=0.1 on the worker) keeps you comfortably within limits. For bulk imports (syncing thousands of existing users to HubSpot), use HubSpot's batch contact upsert API, which processes up to 100 contacts per request.
Senior Full Stack Developer — Laravel, Vue.js, Nuxt.js & AI. Available for freelance projects.
Hire Me for Your Project