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

Hubspot Php Laravel Package

hubspot/hubspot-php

Official HubSpot PHP SDK for integrating HubSpot CRM APIs into PHP applications. Provides a clean client for contacts, companies, deals, tickets, marketing, and more, with simple authentication and request handling.

Deep Wiki
Context7

Getting Started

1. Install

composer require hubspot/hubspot-php

The package is a generated OpenAPI client — there is no facade, no config file, and almost no documentation beyond the HubSpot REST reference. Pin the version in composer.json; minor releases have shipped breaking auth changes.

2. Create a Private App in HubSpot (Settings → Integrations → Private Apps), grant the narrowest scopes you need (crm.objects.contacts.read, etc.), and copy the access token. API keys are gone — v5 only speaks OAuth / private-app tokens.

3. Wire it into the container once, as a singleton:

// app/Providers/HubSpotServiceProvider.php
use HubSpot\Factory;

$this->app->singleton(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class, fn () => null);
$this->app->singleton(\HubSpot\Discovery\Discovery::class, function () {
    return Factory::createWithAccessToken(config('services.hubspot.token'), [
        'config' => ['http_errors' => true],
    ]);
});

Add HUBSPOT_TOKEN to config/services.php ('token' => env('HUBSPOT_ACCESS_TOKEN')).

4. First call — list a page of contacts and confirm properties come back:

$client = app(\HubSpot\Discovery\Discovery::class);

$page = $client->crm()->contacts()->basicApi()->getPage(
    limit: 10,
    properties: ['email', 'firstname', 'lastname']
);

foreach ($page->getResults() as $contact) {
    dump($contact->getId(), $contact->getProperties());
}

Where to look first: vendor/hubspot/hubspot-php/src/Client/. The namespace mirrors the API path exactly — Client/Crm/Contacts/Api/BasicApi.php is POST/GET /crm/v3/objects/contacts. Open the class, look at the method signature, and you have the query parameters and return type. This is faster than reading HubSpot's docs for anything except semantics. Start with crm()->{object}()->basicApi(), then searchApi() and batchApi() once you outgrow it.

Implementation Patterns

Wrap the client, don't inject it everywhere. The discovery object exposes every endpoint; injecting it into controllers invites sprawl and makes tests painful. Create one thin adapter per object type:

class ContactRepository
{
    public function __construct(private \HubSpot\Discovery\Discovery $hubspot) {}

    public function find(string $id): ?SimplePublicObject
    {
        try {
            return $this->hubspot->crm()->contacts()->basicApi()
                ->getById($id, ['email', 'firstname', 'lastname', 'lifecyclestage']);
        } catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
            if ($e->getCode() === 404) return null;
            throw $e;
        }
    }
}

Pagination loop. Paging is cursor-based; getPage() returns a paging->next->after you feed back in. Guard against infinite loops and always cap:

$after = null;
do {
    $page = $this->hubspot->crm()->contacts()->basicApi()
        ->getPage(limit: 100, after: $after, properties: ['email']);
    foreach ($page->getResults() as $c) { /* ... */ }
    $after = $page->getPaging()?->getNext()?->getAfter();
} while ($after !== null);

Use Search for incremental syncs, not full pagination. searchApi()->doSearch() supports filter groups, sorting, and hs_lastmodifieddate filters, which is the cheapest way to build a nightly "what changed since X" job:

$req = (new PublicObjectSearchRequest())
    ->setFilterGroups([[ (new FilterGroup())->setFilters([
        (new Filter())->setPropertyName('hs_lastmodifieddate')
            ->setOperator('GTE')->setValue((string) $sinceMs),
    ])]])
    ->setProperties(['email', 'firstname'])
    ->setSorts(['hs_lastmodifieddate'])
    ->setLimit(100);

$res = $this->hubspot->crm()->contacts()->searchApi()->doSearch($req);

Remember to advance $sinceMs past the last seen updatedAt (epoch milliseconds) or you'll re-process the boundary record forever.

Batch for throughput. One-at-a-time writes will burn your rate limit instantly. Use batchApi() — read, create, update, and (in recent versions) upsert by unique property:

$this->hubspot->crm()->contacts()->batchApi()->create(
    (new BatchInputSimplePublicObjectInputForCreate())->setInputs($inputs)
);

Chunk to 100 per request for most objects; a batch call counts as a single request.

Associations are a separate API surface. To read associations with a record, pass them into getById's associations array (object type + association type id). To write them, use the v4 endpoint:

$this->hubspot->crm()->associations()->v4()->basicApi()->create(
    'contacts', $contactId, 'deals', $dealId,
    [(new AssociationSpec())->setAssociationCategory('HUBSPOT_DEFINED')->setAssociationTypeId(4)]
);

Association type IDs are not intuitive — look them up in the HubSpot associations schema rather than guessing.

Retries belong in Guzzle, not your loop. The SDK does not retry 429s. Add a middleware to the client you hand to Factory:

$stack = HandlerStack::create();
$stack->push(Middleware::retry(
    fn ($retries, $req, $res) => $res && $res->getStatusCode() === 429 && $retries < 5,
    fn ($retries) => (int) ($res?->getHeaderLine('Retry-After') ?: 2 ** $retries) * 1000
));
$client = Factory::createWithAccessToken($token, [
    'client' => ['handler' => $stack, 'timeout' => 10],
]);

Test at the repository boundary. The generated classes are concrete and enormous; mocking them is miserable. Either fake your ContactRepository in feature tests or, for integration tests, inject a Guzzle client built with MockHandler so you can assert the request bodies going out.

Webhooks need hand-rolled verification. The package ships no signature helper. Validate X-HubSpot-Signature-v3 (HMAC-SHA256 over method + uri + body + timestamp, compare with X-HubSpot-Request-Timestamp, reject if older than 5 minutes) inside a middleware before the job dispatch, using the private app's client secret.

Gotchas and Tips

  • Auth changed in v5. Factory::create() no longer reads HUBSPOT_API_KEY the way older tutorials show; use Factory::createWithAccessToken() with a private-app token, or implement OAuth via Client/Auth/OAuth. If you're on 4.x and upgrading, this is the migration.
  • Exceptions are per-endpoint classes. You'll catch \HubSpot\Client\Crm\Contacts\ApiException, ...\Deals\ApiException, and so on — there's no single meaningful base type. getCode() is the HTTP status and getResponseBody() is a PSR-7 stream you must (string) cast. Wrap this once.
  • Properties array omission is the #1 "empty data" bug. Calling getById($id) without a properties list returns only a minimal default set (id, createdAt, updatedAt). Always pass the properties you intend to use, spelled exactly as they appear in HubSpot (case-sensitive, snake_case for custom properties).
  • 404s on associations vs. records don't share semantics. A contact can exist while an association lookup 404s; don't blanket-treat 404 as "record missing."
  • Rate limits are per-app, not per-endpoint. Standard is ~100 requests / 10 seconds and a daily cap; Search has a separate, lower budget. Batch + Search is a requirement at any real volume, not an optimization.
  • Search caps at 10,000 results. For larger exports, partition by hs_lastmodifieddate windows or by createdate ranges. limit on search defaults to 10 — set it to 100 explicitly.
  • Date values are type-dependent. hs_lastmodifieddate is epoch milliseconds; date properties want YYYY-MM-DD. Sending the wrong format returns a validation error that names the property, so log getResponseBody() on 400s.
  • No default timeout. Guzzle's default of zero seconds means a stalled HubSpot call stalls your PHP worker. Always set timeout and connect_timeout in the config array.
  • archived is a real boolean flag, not a filter. Passing true to getById/getPage returns only deleted records; it's easy to leave set during a debugging session.
  • Don't let the generated code poison your tooling. src/Client/ is hundreds of thousands of lines. Exclude it from PHPStan/Psalm paths and from Blade/IDE indexing, or your CI gets slow and your editor gets sad.
  • OAuth tokens expire in 30 minutes. If you're multi-portal, persist refresh_token per portal, and centralize the refresh in a single service — the SDK won't do it for you.
  • Use a sandbox account (free with any HubSpot plan) and a separate private app token for tests. Running integration tests against production properties is how you end up with 4,000 junk contacts named "test".
  • Extension point: every generated API class accepts the underlying GuzzleHttp\ClientInterface in its constructor, and Factory accepts a 'client' config key. That's your hook for logging, tracing, retries, and base-URI overrides — everything else is generated and shouldn't be patched.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core