Use HubSpot with Prismic
Learn how to build websites with HubSpot and Prismic.
This guide shows how to integrate HubSpot forms with Prismic to let content writers select and display forms directly in their content. You’ll learn to create a custom integration catalog that connects your HubSpot forms to Prismic’s integration fields, giving developers full control over form rendering and submission.
Prerequisites
To integrate HubSpot into your website, you’ll first need to set up HubSpot API access and configure environment variables for your project.
Create a HubSpot private app
HubSpot requires a private app for API access.
Navigate to Settings > Integrations > Private Apps in your HubSpot account and click Create a private app.
Use the following values when creating your app:
Field Value App Name ”Prismic Forms Integration” (or your preferred name) Description ”Display forms through Prismic pages.” Scopes Select forms
under OtherClick Create app to finish setup.
Learn more about HubSpot private apps
Set up environment variables
After creating your app, save your credentials as environment variables in a
.env
file:.env# The access token from the HubSpot private app. HUBSPOT_ACCESS_TOKEN=your_access_token_here # The Portal ID from the HubSpot account settings. HUBSPOT_PORTAL_ID=your_portal_id_here
Display HubSpot forms
Follow these steps when content writers need to display a form from a HubSpot project on your website.
Add an integration endpoint to your website
Making forms available to integration fields requires a custom integration catalog.
To begin, add an API endpoint that returns your forms.
src/app/api/forms/route.tsimport type { IntegrationAPIItem, IntegrationAPIResults, } from "@prismicio/client"; export const dynamic = "force-dynamic"; const MAX_PER_PAGE = 50; export type HubSpotForm = { id: string; }; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const page = parseInt(searchParams.get("page") ?? "1") || 1; const res = await fetch("https://api.hubapi.com/marketing/v3/forms/", { headers: { Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`, }, }); const { results: forms } = (await res.json()) as { results: { id: string; name: string; updatedAt: string }[]; }; // Paginate results const startIndex = (page - 1) * MAX_PER_PAGE; const endIndex = startIndex + MAX_PER_PAGE; const paginatedForms = forms.slice(startIndex, endIndex); const results: IntegrationAPIItem<HubSpotForm>[] = paginatedForms.map( (form) => ({ id: form.id!, title: form.name || "Untitled Form", description: "HubSpot form", last_update: new Date(form.updatedAt!).getTime(), blob: { id: form.id!, }, }), ); const response: IntegrationAPIResults<HubSpotForm> = { results_size: forms.length, results, }; return Response.json(response); }
Deploy your website
Your API endpoint needs to be deployed and accessible by Prismic.
Follow the deployment instructions for Next.js, Nuxt, or SvelteKit before continuing. Remember to add your environment variables to your deployment.
You’ll use your website’s deployed URL in the next step.
Create an integration catalog
Follow the linked guide to connect the API endpoint to a custom integration catalog.
Learn how to create a custom integration catalog
Use the following field values when creating the catalog:
Field Description Catalog Name ”HubSpot Forms” Description ”Forms from HubSpot.” Endpoint The full public URL to the API endpoint (e.g. https://example.com/api/forms
)Access Token An optional secret string used to authenticate API calls. Add a form field to a content model
After creating the catalog, connect it to an integration field in a slice, page type, or custom type depending on where you need the form data.
Learn how to add an integration field
Display the form
After fetching a form from HubSpot, you need to render it on your website. The following example provides minimal, unstyled components that you can copy and customize.
First, create an API endpoint to send form submissions to HubSpot.
src/app/api/forms/[id]/route.tsimport { NextRequest, NextResponse } from "next/server"; type Params = { id: string }; export async function POST( request: NextRequest, { params }: { params: Promise<Params> }, ) { const { id } = await params; const formData = await request.formData(); const fields = Array.from(formData, ([name, value]) => ({ name, value: value.toString(), })); const response = await fetch( `https://api.hsforms.com/submissions/v3/integration/secure/submit/${process.env.HUBSPOT_PORTAL_ID}/${id}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`, }, body: JSON.stringify({ fields, context: { hutk: request.cookies.get("hubspotutk")?.value || "", pageUri: request.headers.get("referer") || "", }, }), }, ); return response.ok ? NextResponse.json({ success: true }) : NextResponse.json( { error: "Failed to submit form" }, { status: response.status }, ); }
Next, create the form components that handle all HubSpot field types. You can copy this entire file into your project and customize as needed:
src/components/HubSpotForm.tsx"use client"; import { FormEvent } from "react"; type HubSpotField = { name: string; label: string; fieldType: string; required: boolean; placeholder?: string; defaultValue?: string; options?: { label: string; value: string; }[]; validation?: { data?: { min?: number; max?: number; }; }; }; type HubSpotFieldGroup = { groupType: string; fields: HubSpotField[]; }; type HubSpotForm = { id: string; fieldGroups: HubSpotFieldGroup[]; displayOptions?: { submitButtonText?: string; }; }; export function HubSpotForm({ form }: { form: HubSpotForm }) { async function handleSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const formEl = event.target as HTMLFormElement; const response = await fetch(`/api/forms/${form.id}`, { method: "POST", body: new FormData(formEl), }); if (response.ok) { alert("Form submitted successfully!"); formEl.reset(); } else { alert("Failed to submit form. Please try again."); } } return ( <form onSubmit={handleSubmit}> {form.fieldGroups?.map((group) => ( <div key={group.groupType}> {group.fields.map((field) => ( <Field key={field.name} field={field} /> ))} </div> ))} <button type="submit"> {form.displayOptions?.submitButtonText || "Submit"} </button> </form> ); } const fieldComponents: Record< string, React.ComponentType<{ field: HubSpotField }> > = { text: TextField, email: TextField, tel: TextField, number: TextField, date: TextField, datepicker: TextField, textarea: TextAreaField, select: SelectField, radio: RadioField, checkbox: CheckboxField, booleancheckbox: MultiCheckboxField, }; function Field({ field }: { field: HubSpotField }) { const Component = fieldComponents[field.fieldType] ?? TextField; return <Component field={field} />; } const inputTypes: Record<string, string> = { email: "email", tel: "tel", number: "number", date: "date", datepicker: "date", }; function TextField({ field }: { field: HubSpotField }) { return ( <div> <label htmlFor={field.name}> {field.label} {field.required && " *"} </label> <input type={inputTypes[field.fieldType] || "text"} id={field.name} name={field.name} defaultValue={field.defaultValue ?? ""} placeholder={field.placeholder} required={field.required} min={field.validation?.data?.min} max={field.validation?.data?.max} /> </div> ); } function SelectField({ field }: { field: HubSpotField }) { return ( <div> <label htmlFor={field.name}> {field.label} {field.required && " *"} </label> <select id={field.name} name={field.name} defaultValue={field.defaultValue ?? ""} required={field.required} > <option value="">{field.placeholder ?? "Select an option"}</option> {field.options?.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> ))} </select> </div> ); } function TextAreaField({ field }: { field: HubSpotField }) { return ( <div> <label htmlFor={field.name}> {field.label} {field.required && " *"} </label> <textarea id={field.name} name={field.name} defaultValue={field.defaultValue ?? ""} placeholder={field.placeholder} required={field.required} rows={4} /> </div> ); } function CheckboxField({ field }: { field: HubSpotField }) { return ( <div> <input type="checkbox" id={field.name} name={field.name} value="true" defaultChecked={field.defaultValue === "true"} required={field.required} /> <label htmlFor={field.name}> {field.label} {field.required && " *"} </label> </div> ); } function RadioField({ field }: { field: HubSpotField }) { return ( <fieldset> <legend> {field.label} {field.required && " *"} </legend> {field.options?.map((option) => ( <div key={option.value}> <input type="radio" id={`${field.name}-${option.value}`} name={field.name} value={option.value} defaultChecked={field.defaultValue === option.value} required={field.required} /> <label htmlFor={`${field.name}-${option.value}`}> {option.label} </label> </div> ))} </fieldset> ); } function MultiCheckboxField({ field }: { field: HubSpotField }) { const defaultValues = field.defaultValue ? field.defaultValue.split(";") : []; return ( <fieldset> <legend> {field.label} {field.required && " *"} </legend> {field.options?.map((option) => ( <div key={option.value}> <input type="checkbox" id={`${field.name}-${option.value}`} name={field.name} value={option.value} defaultChecked={defaultValues.includes(option.value)} /> <label htmlFor={`${field.name}-${option.value}`}> {option.label} </label> </div> ))} </fieldset> ); }
Finally, display the form selected through the integration field. This example shows how to display the form in a slice.
src/slices/Form/index.tsximport type { Content } from "@prismicio/client"; import { isFilled } from "@prismicio/client"; import type { SliceComponentProps } from "@prismicio/react"; import type { HubSpotForm as HubSpotFormData } from "@/app/api/forms/route"; import { HubSpotForm } from "@/components/HubSpotForm"; type FormProps = SliceComponentProps<Content.FormSlice>; export default async function Form({ slice }: FormProps) { if (!isFilled.integration(slice.primary.form)) { return null; } // Fetch data directly in the component since this is a React Server Component. const res = await fetch( new URL( slice.primary.form.id as HubSpotFormData["id"], "https://api.hubapi.com/marketing/v3/forms/", ), { headers: { Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`, }, }, ); const form = await res.json(); return ( <section> <HubSpotForm form={form} /> </section> ); }
Add a form to a page
Test your new field by selecting a form in a page. If everything was set up correctly, you should see the form displayed on your page.
Form metadata
The form metadata returned by the custom integration catalog includes the following fields:
id
string
The form’s unique identifier from HubSpot.