Hunter in the Woods

Quick start tutorial on installing and using Hubspot PHP SDK with Laravel

hubspot-logo - LeadSquaredSo your boss / client / lover? Comes to you and says, “Have you used a CRM before? I think we need one, but I'm not willing to pay any money for it right now and I've heard HubSpot is good. Can you integrate it with our website?". And you take a quick look and see they have an extensive API and you go “Sure, looks ok at face value - I'll do it…".

Immediately clouds roll in, lighting and thunder punctuate the sinister organ music that starts up in the distance, possibly below you - complimenting the gloomy wasteland that you have unwittingly stumbled into like the opening credits of Dark Castle. “Oh shiiii….” you say through the misty air, but it's too late, you said you would do the work so you must now press on.

Lucky for you, I just did this so read on pre-weary developer, and hopefully this can help you navigate around many of the pitfalls that await you.

Disclaimer

This tutorial is really to help you understand the HubSpot API as it's a bit of a mess - so I will only show you the code that is relevant, skipping over creating forms, validation, nice class abstraction, most templates or prettiness, etc - which might make it difficult if your unfamiliar Laravel. However Laravel is well documented so it should be easy to fill in the blanks and I'll allude to what is needed in case you need some direction.

Also I'm using PHP + Laravel in these examples, but Hubspots API will be the same in any other language (Python, .Net, Nodejs, etc) so the same problems will be relivent to you, even if the language is not.

Prerequisites

  1. Understanding of what a Customer Relationship Management (CRM) service is used for and what HubSpot is.
  2. You will need a HubSpot account, you can sign up for free here: https://www.hubspot.com/products/get-started
  3. Intermediate level of understanding of Laravel and PHP: https://laravel.com/ and a project all ready to go.

Laravel package

So the top hit from a quick search returns a simple wrapper package which installs:

  1. https://github.com/HubSpot/hubspot-php
  2. https://github.com/rossjcooper/laravel-hubspot

All looks safe at the time of writing (please verify third-party libraries for yourself) so let's install it:

1. $ cd ~/<path>/<to>/<your>/<project>/<root>
2. $ composer require rossjcooper/laravel-hubspot
3. Get a HubSpot API Key from the Intergrations page of your HubSpot account.
4. For <= Laravel 5.4 only, manually add to 'config/app.php':
    1. Rossjcooper\LaravelHubSpot\HubSpotServiceProvider::class to your providers array.
    2. 'HubSpot' => Rossjcooper\LaravelHubSpot\Facades\HubSpot::class to your aliases array.
5. Create a config/hubspot.php file: $ php artisan vendor:publish --provider="Rossjcooper\LaravelHubSpot\HubSpotServiceProvider" --tag="config"
6. Add your HubSpot API key into the your .env file: HUBSPOT_API_KEY=yourApiKey

Great, we should now have access to the Hubspot SDK via a facade - which we can now call like so:

use Rossjcooper\\LaravelHubSpot\\Facades\\HubSpot;

class SomeController extends Controller
{
public function index(Request $request)
{
try {
$contacts = Hubspot::contacts()->all();
} catch (\\Exception | NotFoundException | BadRequest $e) {
// this will show you the error messages in thier entirity, becasue Guzzle trucates them "helpfully"
if(method_exists($e, 'getResponse')) {
$response = $e->getResponse();
$responseBodyAsString = $response->getBody()->getContents();
dd($e, $responseBodyAsString);
} else {
dd($e);
}
}
dd($contacts);
}
}

Note:

  1. Make sure to include the Facades folder on first line in the example above - for some reason there are two files named Hubspot.php in the package and it's easy to include the wrong one which doesn't work, like this: use Rossjcooper\\LaravelHubSpot\\HubSpot; Roh roh.
  2. Always wrap calls to HubSpot in a try/catch - according to the docs it fails with a 500 error every now and then and best you catch it nicely.

You should now have access to the HubSpot library so we're ready to get some data - thanks Ross.

HubSpot documentation

Ok, hopefully that all worked? Now let's get HubSpot up and running and make some calls! Naturally the docs (https://developers.hubspot.com/docs/overview) are a bit of an confusing mess and there are discrepancies all over the place - Wohoo.

It's a bit beyond me why companies don't use standardized OpenAPI documentation tools for this like Redoc, Swagger, RAML, etc. Lots out there, just do a quick search - but this isn't about documentation, so bravely we must push on or we'll be here all night.

HubSpot architecture to be aware of

With the HubSpot API, you have to manually connect entities within the system with ‘CRM associations’ or attached as an ‘engagement’. For example when you create your first company and contact, you then need to ‘associate’ the two entities just like you would with a pivot table, which is what I guess it's doing in the background.

Or if you need to grab tickets associated with a company, you don't call the tickets end-point as you would expect, but the ‘CRM associations’ endpoint which gives you a list of objects associated with the passed in reference id, for example: ‘company_id’. I'm sure there are plenty more fun to be had understanding these kind of design choices - but this should at least push you in the right direction.

Workflow tips

When you get going, I found that my workflow was to look up an endpoint in the docs, then find it in the HubSpot SDK (https://github.com/HubSpot/hubspot-php) and work back from there as it tells you so much more about the endpoint than the docs do - you know, like what params it accepts, nothing too important.

As soon as you hit an issue, check the forum (https://community.hubspot.com/) for bugs and solutions because you can loose so much time even on simple things due to bad error messages, documentation or a ton of other stake filled pitfalls you can fall down. And by using this trinity of tools you can hopefully make headway.

Create your first HubSpot Contact and Company via the API

Now lets dive in and create ourselves our first company and contact, and then associate the two entities in HubSpot because it doesn't do it itself. How hard can this be right?

So let us smash our faces into this and add it when a new customer registers or maybe just logs in successfully if you already have Users you want to introduce slowly - however you want to do it, the process should be similar:

// RegisterController.php or LoginController.php or similar
public function store(Request $request)
{
// Store new customer details and/or log them in, whatever else you want to do
...

// HubSpot -----

// New company
// Docs: https://developers.hubspot.com/docs/methods/companies/create_company
// NOTE: This call takes a list of array's directly
$company = Hubspot::companies()->create(\[
\[
"name" => "name", // NOTE: The key is 'name' for this call, but 'property' in the contact call below - oh boy!
"value" => $request->user()->company->name // I'm assuming that you have a relationship between a User and a Company within your own system
\],\[
"name" => "description",
"value" => "<Company notes here>"
\]
\]);

// New contact
// Docs: https://developers.hubspot.com/docs/methods/contacts/create_contact
// NOTE: This call takes an array of array's!
$contact = Hubspot::contacts()->create(\[
\[
'property' => 'email', // NOTE: Using 'property' as the key value, you need to check every call as to which is the right one to use
'value' => $request->user()->email
\],\[
'property' => 'firstname',
'value' => $request->user()->first_name
\],\[
'property' => 'lastname',
'value' => $request->user()->last_name
\],\[
'property' => 'company',
'value' => $request->user()->company_name
\],\[
'property' => 'phone',
'value' => $request->user()->phone
\],\[
'property' => 'address',
'value' => $request->user()->address
\],\[
'property' => 'country',
'value' => $request->user()->country
\],\[
'property' => 'city',
'value' => $request->user()->city
\],\[
'property' => 'state',
'value' => $request->user()->state
\],\[
'property' => 'zip',
'value' => $request->user()->zip
\]
\]);

// Now we assoicate the Contact with the Company
// Docs: https://developers.hubspot.com/docs/methods/crm-associations/crm-associations-overview
Hubspot::crmassociations()->create(\[
"fromObjectId" => $contact->vid, // 'ContactId' in hubspot - this value is dictated by the definitionId below
"toObjectId" => $company->companyId, // 'CompanyId' in hubspot - this value is dictated by the definitionId below
"category" => "HUBSPOT_DEFINED",
"definitionId" => 1 // Defines the objects you are connecting - in this case Contact -> Company
\]);
}

Oookey… So this should start to give you a good idea of what you are in for. But you can now start filling up your HubSpot CRM - just think of all those sweet, sweet leads you will start generating for the sales team. I'm getting clammy hands just thinking about it.

Note: If you want a full list of contact properties you can send, you have to call this endpoint like so:

// List of accepted fields in hubspot - because for some reason it's not in thier docs - yay
$contact_properties = Hubspot::ContactProperties()->all();
foreach($contact_properties->getData() as $contact_property) {
if($contact_property->formField) { // hide internal fields
echo $contact_property->name.'<br/>';
}
}

Good luck finding it documentation - just make the call already.

Tickets

Ok, now we have a user and company, lets set up some tickets so that they can send us issues, etc, and take advantage of HubSpot customer support tools.

First we create three new pages, something like:

  • '/tickets' - list of tickets that is associated with that user
  • '/ticket/{ticketId}' - show a ticket's details
  • '/ticket/new' - form for customers to raise new tickets

Add these new routes and set up your templates, etc.

Since we don't know what the tickets will look like just yet, lets create the new ticket page and functionally first.

New Ticket page

Make yourself a form with at least the following fields:

  • Subject - Text input
  • Content - Textarea
  • Files - File input

Validate these fields, do any local work you might need to do and save them into HubSpot like so:

/tickets/new

// TicketController.php or similer
public function store(Request $request)
{
// validate fileds, do local work
...

// HubSpot: Create ticket
// docs: https://developers.hubspot.com/docs/methods/tickets/create-ticket
try {
// Array of arrays...
$ticket = Hubspot::tickets()->create(\[
\[
"name" => "subject", // NOTE: 'name' property...
"value" => $formData\['subject'\]
\],
\[
"name" => "content",
"value" => $formData\['content'\]
\],
\[
"name" => "hs_pipeline",
"value" => "0"
\],
\[
"name" => "hs_pipeline_stage",
"value" => "1"
\]
\]);

    // Assoicate user with company: Company to Ticket
    // Docs: https://developers.hubspot.com/docs/methods/crm-associations/crm-associations-overview
    Hubspot::crmassociations()->create([
      "fromObjectId" => $request->user()->company->hubspot_company_id,
      "toObjectId" => $ticket->objectId,
      "category" => "HUBSPOT_DEFINED",
      "definitionId" => 25
    ]);

} catch (\\Exception | NotFoundException | BadRequest $e) {
if(method_exists($e, 'getResponse')) {
$response = $e->getResponse();
$responseBodyAsString = $response->getBody()->getContents();
dd($e, $responseBodyAsString);
} else {
dd($e);
}
}

// Handle uploaded file(s) here...
}

Cool - that wasn't so hard. But now we need to deal with the uploaded files.

File uploads

Make sure to run this directly after creating a ticket as you will need to the ticket id to attach the note and files to.

The method is:

  1. Create a ticket (as above)
  2. Upload file(s) and
  3. Create an ‘Engagement’ by attaching a ‘Note’ to the ticket with the files attached to that.

/tickets/new

// Upload file
// Docs: https://developers.hubspot.com/docs/methods/files/post_files
if ($request->has('file')) {
try {
// Create a folder for the ticket
$folder_path = 'ticket_'.$ticket->objectId; // lets keep the files in corrosponding folders to help keep things organised
Hubspot::files()->createFolder($folder_path, null);

    // Upload files to folder - must create a path for each file
    $folder_paths = [];
    foreach($request->file as $k => $p) {
      array_push($folder_paths, $folder_path);
    }
    $uploads = Hubspot::files()->batchUpload($request->file, ['folder_paths' => $folder_paths]);
    
    // Create an engagement (attach the uploaded files as notes to the ticket, compony and contact)
    // Docs: https://developers.hubspot.com/docs/methods/engagements/create_engagement
    $uploadIds = [];
    foreach($uploads->data->objects as $upload) {
      array_push($uploadIds, ['id' => $upload->id]);
    }
    Hubspot::engagements()->create([
      "active" => true,
      "ownerId" => $request->user()->hubspot_vid,
      "type" => "NOTE",
    ],[
      "ticketIds" => [$ticket->objectId],
      "companyIds" => [$request->user()->company->hubspot_company_id],
      "contactIds" => [$request->user()->hubspot_contact_vid],
    ],[
      "body" => "Attached files for ticket: ".$ticket->objectId
    ], $uploadIds);

} catch (\\Exception | NotFoundException | BadRequest $e) {
if(method_exists($e, 'getResponse')) {
$response = $e->getResponse();
$responseBodyAsString = $response->getBody()->getContents();
dd($e, $responseBodyAsString);
} else {
dd($e);
}
}
}

Tickets overall page

If all went to plan, you should now be saving tickets onto HubSpot - now lets get the tickets back to our website and display them for the customer:

/tickets

public function index(Request $request)
{
// get ticket pipeline stages for human readable display
$pipelines = Hubspot::crmpipelines()->all('tickets');
$pipelineStages = \[\];
foreach($pipelines->data->results\[0\]->stages as $stage) {
$pipelineStages\[$stage->stageId\] = $stage->label;
}

// get users tickets
$ticketsAssociations = HubSpot::crmassociations()->get($request->user()->company->hubspot_company_id, 25, \['limit' => 50\]);
$hasmore = $ticketsAssociations->data->hasMore;

// debug: show all ticket properties
// $object_properties = HubSpot::ObjectProperties('ticket')->all();
// foreach($object_properties->data as $op) {
//     echo $op->name.' | '.$op->type.'<br/>';
// }
// dd($object_properties);

$tickets = \[\];
foreach($ticketsAssociations->data->results as $ticketId) {
array_push($tickets, HubSpot::tickets()->getById($ticketId, \[
'properties' => \[
'subject',
'content',
'createdate',
'closed_date',
'last_engagement_date',
'hs_pipeline',
'hs_pipeline_stage'\]
\])
);
}

// Sort tickets by date desc
$tickets = collect($tickets)->sortBy(function ($ticket, $key) {
return $ticket->data->properties->createdate->value;
})->reverse();

return view('support.index', compact('tickets', 'hasmore', 'pipelineStages'));
}

Ok good - now you should have everything you need to create a list of tickets - you can loop out the details like so:

/views/tickets/index.blade.php

@inject('carbon', 'Carbon\\Carbon')
...
@foreach ($tickets as $ticket)
{{ $pipelineStages\[$ticket->properties->hs_pipeline_stage->value\] }}<br/>
{{ $ticket->properties->subject->value }}<br/>
{{ Illuminate\\Support\\Str::limit($ticket->properties->content->value, 40) }}<br/>
{{ $carbon::createFromTimestampMs($ticket->properties->createdate->value)->format("j M, Y g:i:s A") }}<br/>
<a href="{{ route('ticket', $ticket->data->objectId) }}">View ticket</a><br/>
<hr/>
@endforeach
...

A few things to take note of:

  1. You need to look up the ticket status
  2. Dates are stored as unix timestamps in milliseconds so use Carbon to make them nice

And now we can link to the final page:

Ticket details page

/ticket/{ticketId}

public function index(Request $request, $ticketId) {
$ticket = Hubspot::tickets()->getById($ticketId, \['properties' => \[
'subject',
'content',
'createdate',
'closed_date',
'last_engagement_date',
'hs_pipeline',
'hs_pipeline_stage'\]\]
);
$ticket = $ticket->data;

// get engagements for this ticket
$engagements = Hubspot::engagements()->all();
$engagements = $engagements->data->results;

// Sort engagements
$engagements = collect($engagements)->sortBy(function ($engagement, $key) {
return $engagement->engagement->createdAt;
})->reverse();

// Get ticket pipeline stages for lookups
$pipelines = Hubspot::crmpipelines()->all('tickets');
$pipelineStages = \[\];
foreach($pipelines->data->results\[0\]->stages as $stage) {
$pipelineStages\[$stage->stageId\] = $stage->label;
}

return view('support.show', compact('ticket', 'engagements', 'pipelineStages'));
}

And a quick template:

/view/tickets/ticket.blade.php

Title: {{ $ticket->properties->subject->value) }}<br/>
Description: {{ $ticket->properties->content->value }}
Status: {{ $pipelineStages\[$ticket->properties->hs_pipeline_stage->value\] }}</p>
<h3>Activity:</h3>
@foreach($engagements as $engagement)
@if($engagement->engagement->type != 'TASK')
@if($engagement->engagement->type  == 'EMAIL' || $engagement->engagement->type == 'INCOMING_EMAIL')
<strong>Email:</strong> {!! $engagement->metadata->subject ?? '-' !!}
@elseif($engagement->engagement->type == 'NOTE')
<strong>Note</strong>
@endif
<br/><small class="for-tablet-portrait-up">{{ $carbon::createFromTimestampMs($engagement->engagement->createdAt)->format("j M, Y g:i:s A") }}</small>

      @if($engagement->engagement->type == 'EMAIL' || $engagement->engagement->type == 'INCOMING_EMAIL')
          {!! $engagement->metadata->html !!}</p>
      @elseif($engagement->engagement->type == 'NOTE')
          @if(isset($engagement->metadata->body))
          {!! $engagement->metadata->body !!} 
      @else
          <p>File(s) uploaded.</p>
      @endif
    @else
        ----
    @endif 

@endif  
@endforeach

Debugging:

Object properties

Get all properties on an object, eg: a ticket - why they don't seem to list these anywhere is beyond me, oh maybe because there is 67 of them, but what do they all do?! Arrrg. Anyway you can grab it like so:

$object_properties = HubSpot::ObjectProperties('ticket')->all();
foreach($object_properties->data as $op) {
echo $op->name.' | '.$op->type.'<br/>';
}
dd($object_properties);

This is a common pattern you will get to know it well.

File system

Get a list of folders because you can't always see them in the HubSpot website instantly:

dd(Hubspot::files()->folders());

View all uploaded files in case you loose files:

dd(Hubspot::files()->all());
Purge old data

You could go through and manually delete all the records, but you'll find somethings you can't delete like media folders and it sounds like a ball ache anyway. I'm a programmer so eat my API calls HubSpot:

// Purge script
/**

* Cleans out Hubspot. All of it must go - **USE WITH EXTREME CAUTION**
  \*/
  function purge() {
  echo "Running purge! You might have to run it a few times if you have a lot of content as it caps at 100 records per call";

  // Delete files
  $files = Hubspot::Files()->all();
  $count = 0;
  foreach($files->data->objects as $file) {
  Hubspot::Files()->delete($file->id);
  $count++;
  }
  echo "\\nDeleted files: ".$count;

  // Delete folders
  $folders = Hubspot::Files()->folders();
  $count = 0;
  foreach($folders->data->objects as $folder) {
  Hubspot::Files()->deleteFolder($folder->id);
  $count++;
  }
  echo "\\nDeleted folders: ".$count;

  // Delete tickets
  $tickets = Hubspot::Tickets()->all();
  $count = 0;
  foreach($tickets->data->objects as $ticket) {
  Hubspot::Tickets()->delete($ticket->objectId);
  $count++;
  }
  echo "\\nDeleted tickets: ".$count;

  // Delete users
  $contacts = Hubspot::Contacts()->all();
  $count = 0;
  foreach($contacts->data->contacts as $contact) {
  Hubspot::Contacts()->delete($contact->vid);
  $count++;
  }
  echo "\\nDeleted contacts: ".$count;

  // Delete companies
  $companies = Hubspot::Companies()->all();
  $count = 0;
  foreach($companies->data->companies as $company) {
  Hubspot::Companies()->delete($company->companyId);
  $count++;
  }
  echo "\\nDeleted companies: ".$count;

  // Engagements
  $engagements = Hubspot::Engagements()->all();
  $count = 0;
  foreach($engagements->data->results as $engagement) {
  Hubspot::Engagements()->delete($engagement->engagement->id);
  $count++;
  }
  echo "\\nDeleted Engagements: ".$count."\\n";
  }
  

500 errors

One thing I have observed is that the HubSpot API endpoints return Gateway Time-out errors quite often. This happens mostly on calls like the purge where you are doing quite a lot all at once, however just running the script again will fix the issue. If it's somewhere where you can't do that you might need to program in a fallback retry script so it tries a few times before accepting it's a real error it can't move on from. Good times.

End

So by no means complete or extensive tutorial, however this is packed full of useful solutions to many pain points you will face. Hopefully this helps you navigate through the worst of it and I very much hope it will help you as much as it will help me the next time I need to do this!

Best of luck :)

Made with since 2015