Create a Lead Capture Form using the HubSpot API
Let's build a full stack web application that integrates with the API for HubSpot, a CRM platform that also offers sales, marketing, and CMS tools.
Sam Magura
While HubSpot is a CRM platform at its core, it feels unfair to describe it as "just" a CRM. In addition to the features you would expect from a CRM, HubSpot also offers platforms for your organization's marketing, sales, customer service, content hosting, and operations.
Many resources in HubSpot's suite of tools can be managed programmatically using the HubSpot API . The API supports a wide variety of use cases, such as adding or updating data in the CRM based on user activity on your company's website.
The use case we'll focus on in this post is implementing a web-based lead capture form. The idea is that a prospective customer has found the website for your upcoming product. The product hasn't hit the market yet, so the prospect can't make a purchase yet — but you'd like to capture their information and add them as a lead in your CRM. Once the lead is in your CRM, you could start an automated email sequence to keep them engaged, or have a member of your presales team reach out to them.
The rest of this post will show you how to proof-of-concept this idea using the HubSpot API and the Zero secrets manager.
What We're Building
To implement our lead capture form, we'll need both a frontend and backend. The frontend will display the form to the prospect and then submit the data to the backend. The backend will then create a contact in HubSpot based on the submitted data.
Since every frontend example on this blog has been written in React thus far, it's time to change things up by coding the frontend in Svelte ! The backend will run on Node.js and use Express to expose a REST API.
To communicate with HubSpot, we'll first use the Zero TypeScript SDK to securely retrieve the HubSpot access token from the cloud. Then, we'll pass the access token to the HubSpot JavaScript API client, which is available on npm as @hubspot/api-client
.
🔗 The full code for this example is available in the zerosecrets/examples GitHub repository.
Creating a HubSpot Private App
If you don't have a HubSpot account already, visit their homepage and click the "Get started free" button. To access the HubSpot API, you'll need to create a HubSpot app. There are two types of apps: public and private. Since we are building an app for our own use (not an integration that anyone can install into their HubSpot account), we want a private app.
To create the private app and obtain an access token, follow the instructions on the private apps documentation page . For the API scopes, select the crm.objects.contacts.write
scope:
After selecting the scopes, you'll be presented with your app's access token. For now, copy-paste the access token into a safe location on your local computer.
Adding a HubSpot Secret to Zero
Now let's store the HubSpot access token in the Zero secrets manager so our Node.js application can fetch it at runtime. Log into your Zero account and create a new project (I called mine "marketing-website"). You'll be shown the Zero token for the newly-created project — copy it to that same safe location on your computer.
Next, click the "New secret" button to add a secret to the project. Fill out the form like this, using your HubSpot access token:
With our HubSpot account set up and the API token stored in Zero, it's time to get coding.
Setting up the Repository
Our application will need both a frontend and backend. Svelte is purely a frontend framework, so we can't implement a REST API directly in our Svelte project like we could if using a full stack framework like Next.js . Because of this, we need to set up a simple monorepo that contains two packages which I'll call frontend
and backend
. npm workspaces will be used to handle dependency management across the monorepo.
We'll use the following directory structure for the monorepo, which is pretty standard. For now, you only need to create the workspace package.json
and an empty packages
directory.
There is a root package.json
, as well as a package.json
for each individual package in the packages
folder. The root package.json
should define the workspaces
key so that npm knows where to find our packages:
{
"name": "marketing-website",
"private": true,
"workspaces": ["packages/*"]
}
There's nothing special about the package.json
files for backend
and frontend
— these files will simply list the dependencies
and devDependencies
of the code like normal.
Creating a Lead Capture Form in Svelte
The standard way to create a new Svelte project is using SvelteKit , which will define a few initial dependencies for you and set up Vite . To run the SvelteKit bootstrapper, simply cd
to the packages
directory and execute
npm create svelte@latest frontend
Then, answer the prompts — I chose the "Skeleton project" app template and enabled type checking using TypeScript syntax.
To run the project, install dependencies with npm install
and then run npm run dev
. This will make your app accessible at http://localhost:5173/ .
All of the code for this simple project will go in src/routes/+page.svelte
. To start, write the HTML code for a form with a single email address field:
<main>
<h1>Our product is launching soon!</h1>
<p>Enter your email below to start a conversation:</p>
<form on:submit={onSubmit}>
<input bind:value={email} placeholder="Email address" type="email" required />
<button type="submit">Submit</button>
</form>
{#if resultMessage}
<p>{resultMessage}</p>
{/if}
</main>
The code is mostly plain HTML, with some Svelte-specific syntax like the on:submit
and bind:value
attributes. The bind:value
syntax demonstrates a big difference between Svelte and React: Svelte supports two-way binding between JavaScript variables and the DOM, while React does not.
Feel free to add some styles to pretty-up the form. Here's what I used:
<style>
:global(body) {
font-family: sans-serif;
}
main {
max-width: 500px;
margin: 0 auto;
}
form {
display: flex;
column-gap: 0.5rem;
margin-bottom: 2rem;
}
</style>
These styles live in the same file as the Svelte component, and are component-scoped by default. This is another neat Svelte feature.
Secure your secrets conveniently
Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.
Handling Form Submissions
Next, we'll add some client-side logic to the page by putting a <script>
tag above the HTML code, in the same file.
<script lang="ts">
let email = ''
let resultMessage: string | undefined
async function onSubmit(e: SubmitEvent) {
e.preventDefault()
resultMessage = undefined
try {
const response = await fetch('http://localhost:3000/api/lead', {
method: 'POST',
body: JSON.stringify({email}),
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`The API returned an error status code: ${response.status}.`)
}
resultMessage = 'Success!'
email = ''
} catch (e) {
resultMessage = `An error occurred: ${(e as Error).message}`
console.error(e)
}
}
</script>
This code defines two stateful variables (email
and resultMessage
), as well as a submit handler for the form. The submit handler uses fetch
to execute the POST /api/lead
method of our backend API (which we'll create soon). To test that the code is working, enter an email address and click "Submit". Then check the network tab of the browser DevTools — you should see a failed fetch request to /api/lead
that contains the email address in the request body. With that, the frontend portion of this project is complete!
Setting up the REST API
We'll be using TypeScript and the Express web server framework for the backend API, just like we did in the previous post on integrating with the OpenAI API. Please refer to that post for instructions on setting up a minimal Express project with TypeScript support. Name the new package backend
and place it in the packages
directory, like we did for the frontend
package.
There's one piece of setup that is necessary for this project that wasn't needed for the OpenAI project: configuring the server to send CORS headers. In development, the API is served at localhost:3000
, while the frontend is served at localhost:5173
. These are different origins as far as CORS is concerned, hence the need to send CORS headers in the API responses.
To enable CORS, install the cors npm package and add the middleware to the Express app:
import cors from 'cors'
// ...
app.use(cors())
The middleware's default configuration is sufficient for our purposes. In production, I recommend disabling CORS to improve the security and performance of your app. It's better to use a reverse proxy so that both the API and frontend are served from the same origin (making it so the API calls from the frontend are same origin requests). You can even use a reverse proxy in development with Caddy .
Defining an API Method
Our API only needs a single method, which accepts the email address from the lead capture form and then creates a HubSpot contact using the HubSpot API. The API handler should look something like this:
app.post('/api/lead', async (req, res) => {
const email = req.body?.email
if (typeof email !== 'string' || email.length === 0) {
res.status(400).send()
return
}
// TODO Call the HubSpot API
console.log(`Created lead: ${email}`)
res.status(200).send()
})
Integrating with the HubSpot API
To call the HubSpot API, we'll need to get the access token from Zero and use it to instantiate the HubSpot API client. Towards that end, let's install the requisite dependencies:
npm install @zerosecrets/zero @hubspot/api-client
Then create a file called getHubSpotClient.ts
and paste in the following code:
import hubspot from '@hubspot/api-client'
import {zero} from '@zerosecrets/zero'
let hubSpot: hubspot.Client | undefined
export async function getHubSpotClient(): Promise<hubspot.Client> {
// Reuse the same HubSpot client if one has already been created, so that we
// don't call Zero on every request
if (hubSpot) {
return hubSpot
}
if (!process.env.ZERO_TOKEN) {
throw new Error('Did you forget to set the ZERO_TOKEN environment variable?')
}
const secrets = await zero({
token: process.env.ZERO_TOKEN,
pick: ['hubspot'],
}).fetch()
if (!secrets.hubspot) {
throw new Error('Did not receive an API key for HubSpot.')
}
return new hubspot.Client({accessToken: secrets.hubspot.token})
}
If you've read some of the other blog posts, you'll definitely recognize this code — this is my standard pattern for exchanging the Zero token for the 3rd party API's access token, and then using it to create the API client.
Now that we have a HubSpot client, it's time to do a bit of research on the HubSpot Contacts API . After reading the first section of that documentation page, you'll see that to create a contact, all we need to do is pass a properties
object with an email
field to the appropriate API endpoint. All other fields shown in the example request body are optional.
Let's use that knowledge to update our Express API handler to create the contact. To do that, replace the TODO
comment with this code:
const hubSpot = await getHubSpotClient()
hubSpot.crm.contacts.basicApi.create({
properties: {
email,
},
associations: [],
})
The code is very straightforward, apart from the line associations: []
. The associations
field is not actually required for the API request to succeed, but the HubSpot type definitions require it to be present.
Testing it Out
You can test that everything is working end-to-end by starting up both the frontend and backend via the commands
# In packages/backend
ZERO_TOKEN='YOUR_ZERO_TOKEN' npm start
# In packages/frontend
npm run dev
Open the Svelte app in your browser, enter an email address, and submit the form. If everything worked as expected, you'll get a 200 response from the backend API.
⚠️ HubSpot imposes a strict rate limit on free accounts. If you attempt to create two contacts with a 10-second time period, you'll get an error from their API.
Now, if you log in to the HubSpot dashboard and navigate to the Contacts page, you'll see the contact(s) that were created from the application!
Closing Thoughts
HubSpot is a powerful platform for streamlining your marketing and sales workflows. Even better, the HubSpot API makes it quick and easy to automate common tasks from your web application backends. To make the API even more convenient to use, I recommend storing the HubSpot access token in the Zero secrets manager, together with all of the other secrets & API keys your application needs.
This post showed you how to tie together a web frontend and backend with the HubSpot API, but it really only scratched the surface of what is possible. While your real use case for the HubSpot API is likely significantly more complex, this guide should serve as a solid starting point. Good luck with your project!
Other articles
Add Error Monitoring to an Express.js App with Rollbar
Stay on top of errors in your Node.js apps to find and fix bugs faster.
Creating a Payment Categorization API with OpenAI's GPT
The world's most powerful AI models are built using extremely advanced machine learning and run on expensive specialized hardware. But it's actually incredibly easy to integrate these models' functionality into your application, as I'll show in this article.
Secure your secrets
Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.