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

jeffersongoncalves/laravel-hubspot

Laravel HubSpot is a package that brings HubSpot integration to Laravel applications, offering configuration and a straightforward way to connect your app to HubSpot services. It helps developers add HubSpot functionality with minimal setup and clean Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with Composer, publish the config (php artisan vendor:publish), and set a HubSpot private app access token in .env (not the legacy API key). Scope the token to only the object types you actually touch — scopes are per-object (crm.objects.contacts.read/write, crm.objects.deals.*, crm.schemas.custom.read, etc.) and a missing scope surfaces as a 403 wrapped in HubspotException.
  • First place to look is the config file: base URL (sandbox vs production), timeout, and retry settings. Second is HubspotException — it carries the raw API body, so dd($e->getResponseBody()) (or $e->response) is the fastest way to understand any failure.
  • First use case: create a contact when a user signs up or submits a form.
$contact = Hubspot::contacts()->create([
    'properties' => [
        'email'     => $user->email,
        'firstname' => $user->first_name,
        'lastname'  => $user->last_name,
    ],
]);

$user->update(['hubspot_id' => $contact['id']]);
  • Add a hubspot_id (nullable, indexed) column to every local model you mirror. Everything else — updates, associations, dedupe — gets easier once you have the remote ID stored.
  • The object types share one CRUD surface (contacts, companies, deals, tickets, products, line_items, and custom objects), so learn the shape once and it transfers.

Implementation Patterns

Wrap the client, don't call it from controllers. The package is a thin transport layer. Put it behind a repository/service per object type so you own the mapping between your Eloquent models and HubSpot properties:

final class HubspotContactRepository
{
    public function upsert(User $user): string
    {
        $properties = ['email' => $user->email, 'firstname' => $user->first_name];

        return $user->hubspot_id
            ? tap($user->hubspot_id, fn ($id) => Hubspot::contacts()->update($id, compact('properties')))
            : Hubspot::contacts()->create(compact('properties'))['id'];
    }
}

Search: use the helper for one property, raw payloads for everything else. The single-property filter helper is fine for email-style lookups, but the moment you need multiple criteria, IN/GT/HAS_PROPERTY operators, or OR logic across filterGroups, drop to the raw payload. Raw payloads are your escape hatch — treat the helper as sugar, not the abstraction.

Always paginate with the cursor. Search and list responses return paging.next.after; loop until it's absent rather than assuming one page. Never fetch a full object list into memory to filter in PHP — that burns rate limit and returns stale data.

Idempotent, queued writes. Rely on the automatic 429 retry for throttling, but wrap every write in a queued job with $tries, backoff(), and retryUntil() so transient 5xx and timeouts recover too. Because retries can re-run a write that already succeeded (see gotchas), make the job check hubspot_id before creating.

Two-phase association writes. Create both objects first, then associate:

Hubspot::deals()->associations()->create($dealId, 'contact', $contactId, ['associationCategory' => 'HUBSPOT_DEFINED', 'associationTypeId' => 3]);

Read associations back to hydrate relationship lists in your UI, and cache them briefly — association reads are the most chatty part of a typical sync.

Error mapping at the boundary. Catch HubspotException in the repository and translate known API errors into domain exceptions: 409 CONFLICT on a duplicate email, 404 on a deleted/archived object, 400 VALIDATION_ERROR with errors[].errorType for property mismatches. Don't let raw API bodies reach your controllers or logs unfiltered — they can contain PII.

Inbound flows use form submissions and marketing emails. Form submissions are best consumed from HubSpot webhooks (or polled) and matched to contacts by email; marketing emails are read-oriented (definitions, stats, campaign context) — you cannot send a marketing email through the API, so keep transactional sending in Laravel and use this only for reporting/segmentation.

Test with Http::fake(). Since the client rides on Laravel's HTTP client, you can fake responses per endpoint and assert the payload shape your repository produces. Do this for the 429 path too, to prove retries actually happen.

Gotchas and Tips

  • The 100 requests / 10 seconds cap is per portal, not per worker. Automatic 429 retry will keep individual calls alive but will not save you if ten queue workers hammer the API in parallel. Cap concurrency (--max-time/dedicated queue with numprocs), or use batch endpoints where the package exposes them.
  • Retries are not idempotent. HubSpot has no idempotency key on these endpoints. A request that times out after the server accepted it will be retried and can create a duplicate contact. Always search-or-update by a unique property before create, and treat retries as at-least-once.
  • Search is eventually consistent. A record created a second ago may not appear in search results. Never validate a create by immediately searching for it; trust the returned id.
  • Search has hard limits: max ~10k results reachable via pagination, page size capped at 200, and a stricter per-second rate limit than the CRM CRUD endpoints. For "export everything" workloads, use the list endpoints with property selection, not search.
  • Line items are not standalone — they must be associated with a deal (and usually a product) to be meaningful. Order of operations: product → line item → association to deal.
  • Custom objects need the schema, not just the name. Calls require the object's fully qualified name/type ID, and the token needs crm.schemas.custom.read to discover it. A schema created this morning may not be immediately available.
  • Properties are namespaced under properties. Sending ['email' => ...] at the top level of a create/update silently does nothing useful. Dates are epoch milliseconds (or ISO depending on property type), enums are case-sensitive internal values, and read-only properties (hs_object_id, createdate) will be rejected on write.
  • HubspotException's body is whatever HubSpot returned. That's usually JSON with status, category, message, errors[] — but 502/503 from their gateway can be HTML. Always guard your parsing: json_decode($e->getResponseBody(), true) ?: [].
  • Archived vs deleted. delete() archives the object; the ID is dead for most operations but an identical email will still 409 on create. Handle "archived contact, same email" explicitly in your dedupe logic.
  • Watch the retry telemetry. Automatic retry hides rate-limit pressure. Log the retry count (or a listener/hook on the HTTP client) and alert when it climbs — it's the earliest signal that a new sync or N+1 association read is over budget.
  • Config quirks to pin down early: client timeout (defaults are often too short for batch writes), sandbox/base URL override for testing, and whether the access token is resolved per-request or cached. Extension points worth knowing: HTTP client middleware for logging/correlation IDs, and any macroable/factory hook for registering custom object types.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings