Zero
Zero
Back

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

Sam Magura

A typewriter

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:

Selecting the scopes for the HubSpot application

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:

Adding a HubSpot secret in Zero

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.

The directory structure for the monorepo

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:

1
2
3
4
5
json
{
  "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

1
shell
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
svelte
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
svelte
<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.

Zero dashboard

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
svelte
<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:

1
2
3
4
5
typescript
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript
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:

1
shell
npm install @zerosecrets/zero @hubspot/api-client

Then create a file called getHubSpotClient.ts and paste in the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typescript
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:

1
2
3
4
5
6
7
8
typescript
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

1
2
3
4
5
shell
# 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!

The Contacts page in HubSpot showing the contacts we added through the API

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

An unwound fiber optic cable

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.

A tall office building

Build an Email Summary Bot using Claude AI, Gmail, and Slack (Part 1)

Integrate with the Gmail Push Notifications API to enable your app to intelligently respond to new emails.

Secure your secrets

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.