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

Api Client Laravel Package

hubspot/api-client

Official HubSpot PHP API client for CRM, CMS, and marketing APIs. Offers generated endpoint wrappers, OAuth and private app auth, and PSR-friendly HTTP handling for Laravel/PHP integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

The SDK is framework-agnostic — there is no Laravel service provider or facade. Treat it as a Guzzle-backed transport that you wrap yourself.

composer require hubspot/api-client
  1. Create a private app in HubSpot (Settings → Integrations → Private Apps) and copy the access token. Grant only the scopes you need — missing scopes surface as 403 at runtime, not at boot.
  2. Put the token in config/services.php under hubspot.token, sourced from .env. Never inline it.
  3. Bind a singleton in AppServiceProvider:
use HubSpot\Factory;

$this->app->singleton(\HubSpot\Client\Crm\Contacts\Api\ContactsApi::class, fn () =>
    Factory::createWithAccessToken(config('services.hubspot.token'))
);

Where to look first: the GitHub README gives a copy-paste snippet per API group. Then read src/Discovery/Discovery.php — it is the map. $client->crm()->contacts(), ->companies(), ->deals(), ->tickets(), ->objects() (custom objects), ->associations()->v4(), ->lists(), plus marketing(), files(), conversations(), automation(). Under each group, the leaf is coreApi(), searchApi(), batchApi(), or gdprApi(). Method signatures live in src/Client/Crm/<Group>/Api/*Api.php; request payload shapes live in the matching Model/ directory.

First use case — create then read a contact:

use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectInput;

$contacts = $client->crm()->contacts()->coreApi();

try {
    $contact = $contacts->create(new SimplePublicObjectInput([
        'email'     => '[email protected]',
        'firstname' => 'Ada',
        'lastname'  => 'Lovelace',
    ]));

    $id = $contact->getId();                 // string
    $props = $contact->getProperties();      // array<string,string|null>
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
    report($e); // see Gotchas: getCode() is the HTTP status
}

Implementation Patterns

Wrap it, don't sprinkle it. The SDK types (SimplePublicObject, CollectionResponseWithTotalSimplePublicObject, ApiException) are verbose and version-sensitive. Define a narrow interface (HubspotContacts::upsert(), findByEmail(), search()) and one SDK-backed implementation. Your jobs and controllers depend on the interface, not on HubSpot\Client\.... This is also the only realistic way to fake it in tests.

Upsert instead of "search then create". getById() accepts an ID property, so email-as-key is one request, not two:

try {
    $contact = $contacts->getById($email, ['email', 'lifecyclestage'], 'email');
} catch (ApiException $e) {
    if ($e->getCode() === 404) {
        $contact = $contacts->create(/* ... */);
    } else {
        throw $e;
    }
}

Paging. List endpoints return getPaging()->getNext()->getAfter(). Loop until getPaging() is null or after is empty. Cursors are opaque and expire — never construct or persist them.

$after = null;
do {
    $page = $contacts->getPage(100, $after, ['email', 'firstname']);
    foreach ($page->getResults() as $c) { /* ... */ }
    $after = $page->getPaging()?->getNext()?->getAfter();
} while ($after);

Bulk syncs should use batch endpoints, not loops. $client->crm()->contacts()->batchApi()->read(...) takes up to 100 IDs per call; batchApi()->create/update/upsert likewise. A 5,000-record nightly sync drops from 5,000 requests to ~50. That single change is usually the difference between hitting rate limits and not.

Search API for anything filtered. Build PublicObjectSearchRequest with setFilterGroups() (OR of ANDs — the nesting is the opposite of what most people assume), setProperties(), setSorts(), setLimit(), and call searchApi()->doSearch(). Use it instead of pulling every record and filtering in PHP.

Queue the writes, throttle the worker. Wrap calls in jobs and add middleware:

public function middleware(): array
{
    return [(new RateLimited('hubspot'))->releaseAfter(10)];
}

Define hubspot in RateLimiter::for() at roughly your account's 10-second budget (100 req/10s is the common private-app ceiling; check your tier). For a single high-volume process, add the SDK's retry middleware so 429s back off on the Retry-After header rather than failing the job.

Cache the slow-changing metadata. Owners, pipelines/stages, and property definitions change rarely and are needed on every UI render. Cache::remember('hubspot.owners', 3600, ...) removes three API calls per page load.

Always request properties explicitly. Omitting it returns only default properties; custom fields silently come back missing.

Associations are v4. $client->crm()->associations()->v4()->basicApi()->create(...) with a PublicAssociation-style payload. The v3 association endpoints still exist in older SDK versions and are easy to follow by mistake when copying snippets from blog posts.

Webhooks are yours to handle. The SDK is a client; it does not receive or validate webhooks. Implement the X-HubSpot-Signature-v3 check yourself (HMAC over method + URI + body + timestamp, using the client secret) and return 200 fast, doing the work in a job.

Gotchas and Tips

  • Pin the version. This is generated code from HubSpot's OpenAPI specs. Method parameter order, required-ness, and model constructors change between minor releases — notably around the 7.x/8.x/9.x/10.x jumps where models moved to promoted-property constructors with required arguments. Upgrading a patch is usually safe; upgrading a major means re-reading every call site and re-running your test suite. Commit composer.lock and read the diff on upgrade.
  • new SimplePublicObjectInput() can throw ArgumentCountError. Recent versions require the properties array in the constructor. Use new SimplePublicObjectInput(['email' => ...]) or named arguments, and check the constructor in Model/ when a payload "suddenly" breaks after composer update.
  • ApiException::getCode() returns the HTTP status, not a HubSpot error code. Use getResponseBody() (string or stream depending on Guzzle config — check is_string() before json_decode) and getResponseHeaders(). Log x-hubspot-correlation-id from the headers; HubSpot support asks for it first and you cannot recover it later.
  • 403 vs 429 are different problems. 403 with a MISSING_SCOPES category means the private app needs the scope added and the token reissued (old tokens keep working but without the new scope). 429 means back off. Don't retry 403s.
  • Search index lag is real. An object created a second ago may not appear in doSearch() results. For read-after-write, always use getById(); reserve search for asynchronous/list views. Also: search caps at 200 per page and 10,000 total results per query — narrow the filters rather than paging deeper.
  • Properties are strings. Numbers, booleans, and dates must be sent as strings ('42', '2024-01-01', not true or a DateTime). Send '' to clear a property; null typically means "leave unchanged".
  • Set Guzzle timeouts. The default handler has no aggressive timeout, and a single stalled request can hang a queue worker for minutes. Pass Guzzle config (timeout, connect_timeout, custom handler/middleware stack) through the Factory when you build the client in the service provider.
  • Testing: the SDK's concrete classes are painful to mock. Two routes: (a) depend on your own interface and fake it, or (b) pass a Guzzle MockHandler with queued responses through the factory's handler config, which exercises your real serialization/deserialization — worth doing for the upsert and paging wrappers specifically.
  • OAuth apps get no help. There is no token-refresh logic in the SDK. You must exchange and refresh tokens yourself, persist expiry, and rebuild the client with the new access token (which means your singleton must resolve lazily per tenant, not once at boot, in a multi-tenant app).
  • Debugging in local only: attach a Guzzle logging middleware to log request/response bodies — then remember those bodies contain PII and access tokens. Gate it behind app()->isLocal() and scrub the Authorization header, or you will leak contact data into shared log storage.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels