Webhooks
Receive and process HubSpot webhook notifications with signature verification, typed events, optional model hydration, and queue support. The feature is entirely opt-in -- it activates when you set a webhook client secret.
Setup
Add your HubSpot app's client secret to .env:
HUBSPOT_WEBHOOK_SECRET=your-client-secretThe package automatically registers a POST route at hubspot/webhooks (configurable). Point your HubSpot app's webhook subscription URL to this endpoint.
Configuration
// config/hubspot.php
'webhooks' => [
'client_secret' => env('HUBSPOT_WEBHOOK_SECRET'),
'path' => env('HUBSPOT_WEBHOOK_PATH', 'hubspot/webhooks'),
'middleware' => ['api'],
'verify_signature' => (bool) env('HUBSPOT_WEBHOOK_VERIFY_SIGNATURE', true),
'tolerance' => (int) env('HUBSPOT_WEBHOOK_TOLERANCE', 300),
'queue' => [
'enabled' => (bool) env('HUBSPOT_WEBHOOK_QUEUE_ENABLED', false),
'connection' => env('HUBSPOT_WEBHOOK_QUEUE_CONNECTION'),
'queue' => env('HUBSPOT_WEBHOOK_QUEUE'),
],
'hydrate_models' => (bool) env('HUBSPOT_WEBHOOK_HYDRATE_MODELS', true),
'only' => [],
'except' => [],
'deduplicate' => (bool) env('HUBSPOT_WEBHOOK_DEDUPLICATE', false),
'deduplicate_ttl' => (int) env('HUBSPOT_WEBHOOK_DEDUPLICATE_TTL', 3600),
],| Option | Description |
|---|---|
client_secret | HubSpot app client secret for signature verification |
path | URL path for the webhook endpoint |
middleware | Middleware stack applied to the webhook route |
verify_signature | Enable/disable v3 HMAC-SHA256 signature verification |
tolerance | Maximum age (seconds) for webhook timestamps before rejection |
queue.enabled | Process webhooks asynchronously via Laravel queues |
queue.connection / queue.queue | Queue connection and queue name |
hydrate_models | Fetch the full CRM model for each event (skipped for deletions) |
only | Whitelist of subscription types to process (supports wildcards) |
except | Blacklist of subscription types to ignore (supports wildcards) |
deduplicate | Prevent duplicate event processing using Laravel Cache |
deduplicate_ttl | Cache TTL (seconds) for deduplication keys |
Listening for Webhook Events
All webhook events live under the Rollogi\LaravelHubspot\Events\Webhook namespace. Every webhook dispatches WebhookReceived (catch-all) plus a specific typed event:
| Event | Triggered By |
|---|---|
WebhookReceived | Every webhook (catch-all) |
ContactCreated | contact.creation |
ContactUpdated | contact.propertyChange |
ContactDeleted | contact.deletion |
CompanyCreated | company.creation |
CompanyUpdated | company.propertyChange |
CompanyDeleted | company.deletion |
DealCreated | deal.creation |
DealUpdated | deal.propertyChange |
DealDeleted | deal.deletion |
DealStageChanged | deal.propertyChange where propertyName is dealstage |
TicketCreated | ticket.creation |
TicketUpdated | ticket.propertyChange |
TicketDeleted | ticket.deletion |
AssociationCreated | Association creation webhooks |
AssociationDeleted | Association deletion webhooks |
PropertyChanged | Fallback for property changes on unsupported object types |
use Illuminate\Support\Facades\Event;
use Rollogi\LaravelHubspot\Events\Webhook\ContactCreated;
use Rollogi\LaravelHubspot\Events\Webhook\DealStageChanged;
use Rollogi\LaravelHubspot\Events\Webhook\WebhookReceived;
// Listen for all webhooks
Event::listen(WebhookReceived::class, function (WebhookReceived $event): void {
logger()->info(sprintf(
'Webhook: %s (object %d)',
$event->event->subscriptionType,
$event->event->objectId,
));
});
// Listen for specific events
Event::listen(ContactCreated::class, function (ContactCreated $event): void {
$contact = $event->model; // Hydrated Contact model (or null if hydration disabled)
$webhookEvent = $event->event; // WebhookEvent value object
});
// Deal stage changes include previous/new stage info
Event::listen(DealStageChanged::class, function (DealStageChanged $event): void {
$previousStage = $event->previousStage(); // e.g. 'qualifiedtobuy'
$newStage = $event->newStage(); // e.g. 'closedwon'
});The WebhookEvent Value Object
Every webhook event carries a WebhookEvent value object with the parsed payload:
$event->event->eventId; // int
$event->event->portalId; // int
$event->event->objectId; // int
$event->event->subscriptionType; // e.g. 'contact.creation'
$event->event->propertyName; // e.g. 'email' (for property changes)
$event->event->propertyValue; // e.g. 'new@example.com'
$event->event->occurredAt; // Carbon instance
$event->event->raw; // Original array payload
// Helper methods
$event->event->objectType(); // 'contact'
$event->event->action(); // 'creation'
$event->event->isCreation(); // true
$event->event->isDeletion(); // false
$event->event->isPropertyChange(); // falseFiltering Events
Use only and except to control which subscription types are processed. Both support wildcard patterns:
// config/hubspot.php
'webhooks' => [
// Only process contact and deal events
'only' => ['contact.*', 'deal.*'],
// Or exclude specific types
'except' => ['ticket.*'],
],Queue Support
Enable queue processing for high-throughput webhook handling:
HUBSPOT_WEBHOOK_QUEUE_ENABLED=true
HUBSPOT_WEBHOOK_QUEUE_CONNECTION=redis
HUBSPOT_WEBHOOK_QUEUE=hubspot-webhooksWhen queue mode is enabled, each webhook event is dispatched as a ProcessWebhookJob with 3 retries and exponential backoff (10s, 60s, 300s). The endpoint returns 200 immediately while events are processed asynchronously.
Deduplication
HubSpot may send the same event multiple times. Enable deduplication to skip already-processed events:
HUBSPOT_WEBHOOK_DEDUPLICATE=true
HUBSPOT_WEBHOOK_DEDUPLICATE_TTL=3600Deduplication uses Laravel Cache to track seen event IDs. Each event is checked and marked as seen during processing, ensuring idempotent handling regardless of sync or queued execution.
Custom Object Types
Register custom HubSpot object types so webhook events can resolve them to models:
use Rollogi\LaravelHubspot\Webhooks\ModelResolver;
ModelResolver::register('custom_object', MyCustomModel::class);Signature Verification
The middleware verifies HubSpot's v3 signature (X-HubSpot-Signature-v3) using HMAC-SHA256 with your client secret. It also validates the request timestamp against the configured tolerance (default: 300 seconds) to prevent replay attacks.
Disable verification for local development:
HUBSPOT_WEBHOOK_VERIFY_SIGNATURE=falseTesting Webhooks
Use WebhookPayloadFactory to generate realistic webhook payloads in your tests:
use Rollogi\LaravelHubspot\Testing\WebhookPayloadFactory;
$payload = WebhookPayloadFactory::contactCreation();
$payload = WebhookPayloadFactory::contactDeletion();
$payload = WebhookPayloadFactory::contactPropertyChange('email', 'new@example.com');
$payload = WebhookPayloadFactory::companyCreation();
$payload = WebhookPayloadFactory::dealCreation();
$payload = WebhookPayloadFactory::dealStageChange('closedwon', 'qualifiedtobuy');
$payload = WebhookPayloadFactory::ticketCreation();
// Override any field
$payload = WebhookPayloadFactory::contactCreation(['objectId' => 12345]);