Skip to content

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:

ini
HUBSPOT_WEBHOOK_SECRET=your-client-secret

The package automatically registers a POST route at hubspot/webhooks (configurable). Point your HubSpot app's webhook subscription URL to this endpoint.

Configuration

php
// 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),
],
OptionDescription
client_secretHubSpot app client secret for signature verification
pathURL path for the webhook endpoint
middlewareMiddleware stack applied to the webhook route
verify_signatureEnable/disable v3 HMAC-SHA256 signature verification
toleranceMaximum age (seconds) for webhook timestamps before rejection
queue.enabledProcess webhooks asynchronously via Laravel queues
queue.connection / queue.queueQueue connection and queue name
hydrate_modelsFetch the full CRM model for each event (skipped for deletions)
onlyWhitelist of subscription types to process (supports wildcards)
exceptBlacklist of subscription types to ignore (supports wildcards)
deduplicatePrevent duplicate event processing using Laravel Cache
deduplicate_ttlCache 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:

EventTriggered By
WebhookReceivedEvery webhook (catch-all)
ContactCreatedcontact.creation
ContactUpdatedcontact.propertyChange
ContactDeletedcontact.deletion
CompanyCreatedcompany.creation
CompanyUpdatedcompany.propertyChange
CompanyDeletedcompany.deletion
DealCreateddeal.creation
DealUpdateddeal.propertyChange
DealDeleteddeal.deletion
DealStageChangeddeal.propertyChange where propertyName is dealstage
TicketCreatedticket.creation
TicketUpdatedticket.propertyChange
TicketDeletedticket.deletion
AssociationCreatedAssociation creation webhooks
AssociationDeletedAssociation deletion webhooks
PropertyChangedFallback for property changes on unsupported object types
php
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:

php
$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(); // false

Filtering Events

Use only and except to control which subscription types are processed. Both support wildcard patterns:

php
// 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:

ini
HUBSPOT_WEBHOOK_QUEUE_ENABLED=true
HUBSPOT_WEBHOOK_QUEUE_CONNECTION=redis
HUBSPOT_WEBHOOK_QUEUE=hubspot-webhooks

When 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:

ini
HUBSPOT_WEBHOOK_DEDUPLICATE=true
HUBSPOT_WEBHOOK_DEDUPLICATE_TTL=3600

Deduplication 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:

php
use Rollogi\LaravelHubspot\Webhooks\ModelResolver;

ModelResolver::register('custom_object', MyCustomModel::class);

To reset all custom mappings back to defaults (useful in tests):

php
ModelResolver::reset();

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:

ini
HUBSPOT_WEBHOOK_VERIFY_SIGNATURE=false

Testing Webhooks

Use WebhookPayloadFactory to generate realistic webhook payloads in your tests:

php
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]);