Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Hubspot Laravel Package

tapp/laravel-hubspot

A Laravel integration for HubSpot that provides OAuth authentication, an API client, and convenient facades for managing contacts, companies, deals, and other CRM resources directly from your Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin Laravel service-provider/facade wrapper around HubSpot's official PHP SDK (hubspot/api-client). Its value is container wiring, config, and a fluent entry point — not a reimplementation of the API. Treat it as glue you build on, not a full ORM for HubSpot.

Minimal setup:

composer require tapp/laravel-hubspot
php artisan vendor:publish --provider="Tapp\LaravelHubSpot\HubSpotServiceProvider"

Put a private app access token in .env (HubSpot retired legacy API keys in 2022; the hapikey style config is a dead end):

HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Then resolve the facade:

use Tapp\LaravelHubSpot\Facades\HubSpot;

$contact = HubSpot::contacts()->create([
    'properties' => [
        'email'     => $user->email,
        'firstname' => $user->first_name,
        'lastname'  => $user->last_name,
    ],
]);

Where to look first, in order:

  1. config/hubspot.php after publishing — confirm the exact env key names and whether it exposes a base URL/sandbox switch.
  2. The facade class in src/Facades/ — that's the real public API surface. Read it before trusting any README example.
  3. The service class it resolves — this tells you whether you get raw SDK objects back or an extra fluent layer.
  4. composer show hubspot/api-client — the SDK version pins which HubSpot API versions (v3 vs v4 associations) are actually reachable.
  5. HubSpot's own docs for the endpoint you're calling. The package won't document behavior it merely proxies.

First real use case: contact upsert on user registration. Create a queued listener on Registered that writes the Laravel user to a HubSpot contact. That exercises config, auth, error handling, and queueing in one place before you scale to deals, companies, or custom objects.

Implementation Patterns

Wrap it, don't sprinkle it. With 0 dependents and 2 stars, the facade is a volatile dependency. Define App\Contracts\Crm with upsertContact(), logDeal(), etc., implement it with HubSpotAdapter, and bind it in a service provider. Swapping to the raw SDK or direct HTTP later becomes a one-file change.

// app/Providers/AppServiceProvider.php
$this->app->bind(Crm::class, HubSpotCrm::class);

Upsert, never blind-create. HubSpot returns 409 CONFLICT when a contact with the same email exists. Email is the practical unique key. The reliable pattern is search-then-update, or use the idProperty query parameter on the create endpoint so HubSpot performs the dedupe for you:

HubSpot::contacts()->create([
    'properties' => ['email' => $user->email, 'firstname' => $user->first_name],
], ['idProperty' => 'email']);

Fall back to a search by email plus update($id, ...) if your SDK version doesn't accept the second argument — verify against vendor/hubspot/api-client.

Queue every write. HTTP calls to HubSpot inside a request cycle are a latency and availability tax you don't need to pay. One job per aggregate write, with $tries = 5 and an exponential backoff(), is the standard shape:

class SyncContactToHubSpot implements ShouldQueue
{
    use InteractsWithQueue, Queueable;
    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];

    public function __construct(public User $user) {}

    public function handle(Crm $crm): void
    {
        $crm->upsertContact($this->user);
    }
}

Model events for reads, explicit calls for writes. Observers (User::updated) are convenient for incremental sync, but they fire on every save() including irrelevant column changes. Gate them on isDirty(['email', 'first_name', 'last_name']), or dispatch from explicit domain events instead. This one decision prevents most of the "why are we hammering the API" incidents.

Batch for backfills, not loops. Seeding 20k contacts one call at a time will hit rate limits within seconds and take hours. HubSpot exposes batch create/update endpoints (100 records per request) on the CRM v3 API — if the facade doesn't surface them, drop down to the underlying SDK client via whatever raw client accessor the service exposes. Chunk with ->chunk(100) and Bus::batch().

Rate limit at the queue level, not per call. Private-app tokens get a fixed request budget per 10 seconds. Give the HubSpot queue a dedicated connection/worker with --max-jobs or a RateLimited job middleware so a backfill can't starve your transactional syncs. Keep the batch backfill on a separate queue name from user-registration syncs.

Reading data. List endpoints are cursor-paginated with a hard limit of 100. Always loop on the after cursor rather than assuming one page:

$after = null;
do {
    $page = HubSpot::contacts()->all(['limit' => 100] + ($after ? ['after' => $after] : []));
    // process $page->getResults()
    $after = $page->getPaging()?->getNext()?->getAfter();
} while ($after);

Webhooks for inbound. Laravel is the natural receiver for HubSpot webhook subscriptions. Verify the v3 signature (X-HubSpot-Signature-v3 HMAC over method + URI + body + timestamp) in a middleware, reject anything older than ~5 minutes, and return 200 fast — HubSpot retries and will disable a subscription that keeps failing. Do the actual processing in a job.

Custom properties. Create them in HubSpot's UI or via the properties API first, then write them by internal name (lifecyclestage, not Lifecycle Stage). Unknown property names return a 400 with a validation error listing the offending name — which is a much better debugging experience than silent drops.

Testing. The SDK uses Guzzle, so Http::fake() does nothing. Two workable options: bind a fake Crm implementation in the container and assert your jobs call it, or point the client at a local mock via a base-URL config override if the package exposes one. The first is cheaper and is another argument for the interface wrapper.

Gotchas and Tips

The config key names move. Different versions of this package have shipped with HUBSPOT_API_KEY versus HUBSPOT_ACCESS_TOKEN, and services.hubspot versus a package-local hubspot file. After composer update, read config/hubspot.php rather than assuming. And always php artisan config:clear — a cached config with an old env key is the single most common "it works locally" failure here.

ApiException is your error type, and its message is useless. Catch HubSpot\Client\Crm\Contacts\ApiException (namespace varies by domain) and inspect $e->getCode() and json_decode($e->getResponseBody()). HubSpot puts the actionable detail in message and errors[].context. Log the body, not the exception string. Branch explicitly on 409 (duplicate), 429 (rate limited), 400 (bad property), and 401 (token revoked or wrong scopes — private apps need CRM scopes enabled individually).

429s need honoring, not retrying blindly. HubSpot returns Retry-After. Read it and release() the job by that many seconds. Fixed backoff arrays will just burn your retry budget during a backfill.

Property values are strings, always. Numbers, booleans, dates — all coerced. Multi-value properties (like hs_additional_emails) are semicolon-joined. Sending true versus "true" gives you different behavior on boolean fields. Sending an empty string '' clears a property; omitting the key leaves it untouched. That distinction matters in update paths and is a frequent source of surprise data loss.

Associations are the weak spot. The v4 associations API (association labels, default vs. custom association types) is newer than most wrappers. If the facade gives you associations() methods, verify they target v3 or v4 before relying on labels — and expect to call the SDK's association classes directly for anything nuanced. Deals-to-contacts is exactly where "just use the wrapper" breaks down.

The facade resolves a singleton. The underlying client is constructed once per request/worker. Long-running queue workers (queue:work without --once) keep the same Guzzle client and connection pool alive for hours; if you rotate the token, workers need queue:restart. Also set explicit Guzzle timeouts (connect ~5s, total ~30s for batches) — the SDK defaults are not tuned for batch writes over slow links.

Sandbox accounts use a separate token. HubSpot sandboxes are a distinct portal with their own private app. Wire it as HUBSPOT_ACCESS_TOKEN per environment and never let a staging deploy write to production CRM. Add a boot-time guard in non-production if you can't trust the config.

Pin the version and watch the SDK, not the wrapper. The package's release cadence is unlikely to track HubSpot's API deprecations. Add hubspot/api-client to your own composer.json as a direct dependency so its version is explicit in your lockfile, and subscribe to HubSpot's developer changelog. When the wrapper lags, HubSpot::api() (or whatever raw-client accessor exists) is your escape hatch — confirm it returns the SDK's API classes, not an opaque Guzzle client.

Extension points, in order of usefulness: publish and override the config; bind your own adapter around the facade; resolve the service class and decorate it; and finally, replace the facade call site with a direct SDK instantiation. Design your code so reaching step four costs you one class.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource