Skip to content

The Scalable Way to Implement HubSpot OAuth in Public Apps

This article was authored by a member of the HubSpot developer community, Tim Joyce.

If you're planning to create an app for HubSpot, chances are you want to get right to the fun stuff—like moving data in/out, syncing with other platforms, generating content, manipulating data, and so much more. But, in order to do that, you need to create a scalable foundation for your app that is versatile, while also setting up your HubSpot account authentication. 

What languages, frameworks, and libraries you use are entirely up to you (we will be using Laravel and PHP throughout this post), but here's how you can architect your database for OAuth, as well as some real-world code examples. 

What do you need to think about?

It's easy to say, "I just want users to authenticate their HubSpot account with my app," but that can lead to a one-to-one connection in which a user, identified by their email address, can authenticate only to a single HubSpot account. 

But, if the same user is part of an agency, the agency may need to connect to many client HubSpot accounts. Now, you have to think about how to tie a single user to multiple accounts. 

We can take the example even further. Let's say that the agency has multiple users who need to connect to multiple accounts. What would the database architecture look like, and how do we authenticate accounts to all of these users?

The answer is that you authenticate an account to an entity instead of an individual user. This allows you to start creating functionality in your app that means users can be added and removed from the entity without disrupting the account authentication that has already been put in place. 

The entity can go by many labels: client, team, company, group, crew, etc. We are going to use "team" as the nomenclature here.  

By authenticating your HubSpot accounts to a team entity, you aren't limiting your infrastructure to just agencies. You are covering all of the bases, including business owners, internal marketing teams, and consultants.

It's a very powerful approach!

The internal marketing team flow, for instance, might look something like this:

Marketing Team Flow Example

Here, you have many users who just need to authenticate to a single HubSpot account.

Meanwhile, the agency flow might look something like this:

Agency Flow Example

The agency marketing team follows the same rules as the internal marketing team’s flow—we've just expanded the diagram a bit to show more versatility. A user can belong to many teams, and each team can have many accounts.

The database architecture

Example of the Database Architecture

As you can see in the diagram above, HubSpot account authentication is attached to a team entity instead of a user. Then, a pivot table created between users and teams allows users to belong to multiple teams. 

When you make your subsequent API requests to HubSpot from your app, you simply look up the authentication tokens for the team and pass them along.

Let's see some code!

As previously mentioned, for this example we will be using the Laravel framework on top of PHP, but you can use any framework you prefer. If you plan to use Laravel, there is a github repo that you may fork or download, called the Laravel HubSpot Starter Kit app, to get you up and running quickly. If you plan to use another language, I recommend checking out HubSpot's client libraries to see if there is one that fits with your codebase.

If you haven't done so already, please follow the steps to create your own HubSpot Public Application and get familiar with the HubSpot OAuth Documentation before proceeding to the next steps. In this example, we are only going to require a single scope crm.lists.read so we can fetch contact lists from a HubSpot account and display them in our dashboard.

The first step is to trigger the authentication between your app's team entity and a HubSpot account.

Modify your .env file:

HS_AUTH_URL=https://app.hubspot.com/oauth/authorize HS_APP_ID=XXXXXX HS_CLIENT_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX HS_CLIENT_SECRET=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX

Define a route:

Route::get('/hs-auth', [HubSpotAuthController::class, 'authTrigger']);

Create your function: 

public function authTrigger() { $scopes = ['crm.lists.read']; $query = [ 'client_id' => env('HS_CLIENT_ID'), 'redirect_uri' => env('APP_URL') . '/submitauth', 'scope' => implode(" ", $scopes), ]; $url = env('HS_AUTH_URL') . "?" . http_build_query($query); return redirect()->to($url); }

The next step is to wait for the Authentication response from HubSpot.

Define another route:

Route::get('/submitauth', [HubSpotAuthController::class, 'authResponse']);

Create your function:

public function authResponse(Request $request) { if (!$request->input('code')) { return response("FAILED - HSA", 401); } $client = new Client(['base_uri' => 'https://api.hubapi.com/oauth/v1/']); $token_query = [ 'grant_type' => 'authorization_code', 'client_id' => env('HS_CLIENT_ID'), 'client_secret' => env('HS_CLIENT_SECRET'), 'redirect_uri' => env('APP_URL') . '/submitauth', 'code' => $request->input('code'), ]; try { $token_response = $client->request('POST', 'token', [ 'query' => $token_query, 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'] ]); } catch (\Exception $e) { \Log::info($e->getMessage()); die; } $token_response = json_decode($token_response->getBody(), true); $token_meta_response = $client->request('GET', 'access-tokens/' . $token_response['access_token']); $token_meta_response = json_decode($token_meta_response->getBody(), true); $portal = new \App\Models\Portal(); $portal->team_id = session('alias'); $portal->hub__id = $token_meta_response['hub_id']; $portal->hub__domain = $token_meta_response['hub_domain']; $portal->token_expires = \Carbon\Carbon::now()->addSecond($token_response['expires_in']); $portal->refresh_token = $token_response['refresh_token']; $portal->access_token = $token_response['access_token']; $portal->save(); session()->put('portal', $portal); return redirect()->to('/hs/lists'); }

The functions above should successfully attach a HubSpot account to a team entity within your application. 

When a user chooses a HubSpot account from within your application, you will need to set that account information into the user's session variables and any subsequent calls made to the HubSpot API by the user, will pass the OAuth access token of the intended HubSpot account.

Here's an example of how to fetch the contact lists from the account using the HubSpot API

Create a route:

Route::get('hs/lists', [HubSpotAuthController::class, 'lists'])->name('hs-lists.get');

Create a function to refresh the access token: 

public function refreshAccessToken() { $team = Team::whereId(session('alias'))->firstOrFail(); $portal = Portal::whereTeamId($team->id)->whereId(session('portal')['id'])->firstOrFail(); $query = [ 'grant_type' => 'refresh_token', 'client_id' => env('HS_CLIENT_ID'), 'client_secret' => env('HS_CLIENT_SECRET'), 'refresh_token' => $portal->refresh_token ]; $client = new Client(); try{ $token_response = $client->request('POST', 'https://api.hubapi.com/oauth/v1/token', [ 'query' => $query, 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'] ]); $token_response = json_decode($token_response->getBody(), true); } catch (\Exception $e) { \Log::info($e->getMessage()); die; } $portal->token_expires = \Carbon\Carbon::now()->addSecond($token_response['expires_in']); $portal->access_token = $token_response['access_token']; $portal->save(); return $token_response['access_token']; }

Fetch the contact lists from the HubSpot API:

public function lists() { $token = $this->refreshAccessToken(); $client = new Client(); $response = $client->request('GET', 'https://api.hubapi.com/contacts/v1/lists', [ 'query' => NULL, 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8' ] ]); $response = json_decode($response->getBody(), true); return response()->json($response); }

Of course, there's a lot more work to be done to get your application to production, but this should provide you with a solid foundation and a good starting point.

Happy coding!