---
title: "Use HubSpot with Prismic"
description: "Learn how to build websites with HubSpot and Prismic."
category: "integrations"
audience: developers
lastUpdated: "2025-11-06T01:07:50.000Z"
---

This guide shows how to integrate [HubSpot forms](https://www.hubspot.com/products/marketing/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](https://prismic.io/docs/fields/integration.md) 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.

1. **Create a HubSpot private app**

   HubSpot requires a [private app](https://developers.hubspot.com/docs/guides/apps/private-apps/overview) 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 Other                           |

   Click **Create app** to finish setup.

   [Learn more about HubSpot private apps](https://developers.hubspot.com/docs/guides/apps/private-apps/overview)

2. **Set up environment variables**

   After creating your app, save your credentials as environment variables in a `.env` file:

   ```bash filename=.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.

> These steps use the HubSpot credentials set up in the [prerequisites](#prerequisites) section.

1. **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.

   **Next.js example:**

   ```ts filename=src/app/api/forms/route.ts collapsed
   import 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);
   }
   ```

   **Nuxt example:**

   ```ts filename=~/server/api/forms.ts collapsed
   import type {
     IntegrationAPIItem,
     IntegrationAPIResults,
   } from "@prismicio/client";

   const MAX_PER_PAGE = 50;

   export type HubSpotForm = {
     id: string;
   };

   export default defineEventHandler(async (event) => {
     const query = getQuery(event);
     const page = parseInt((query.page as string) ?? "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;
   });
   ```

   **SvelteKit example:**

   ```ts filename=src/routes/api/forms/+server.ts collapsed
   import type {
     IntegrationAPIItem,
     IntegrationAPIResults,
   } from "@prismicio/client";
   import { HUBSPOT_ACCESS_TOKEN } from "$env/static/private";
   import type { RequestHandler } from "./$types";

   const MAX_PER_PAGE = 50;

   export type HubSpotForm = {
     id: string;
   };

   export const GET: RequestHandler = async ({ url }) => {
     const page = parseInt(url.searchParams.get("page") ?? "1") || 1;

     const res = await fetch("https://api.hubapi.com/marketing/v3/forms/", {
       headers: {
         Authorization: `Bearer ${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);
   };
   ```

   > This endpoint fetches up to 100 forms from HubSpot. Accounts with more forms need a custom implementation to fetch them all.

2. **Deploy your website**

   Your API endpoint needs to be deployed and accessible by Prismic.

   Follow the deployment instructions for [Next.js](https://prismic.io/docs/nextjs.md#deploy), [Nuxt](https://prismic.io/docs/nuxt.md#deploy), or [SvelteKit](https://prismic.io/docs/sveltekit.md#deploy) before continuing. Remember to add your environment variables to your deployment.

   You'll use your website's deployed URL in the next step.

3. **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](https://prismic.io/docs/fields/integration.md#open-your-repository-settings)

   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](https://prismic.io/docs/fields/integration.md#create-a-custom-api-catalog) string used to authenticate API calls. |

4. **Add a form field to a content model**

   After creating the catalog, connect it to an [integration](https://prismic.io/docs/fields/integration.md) field in a [slice](https://prismic.io/docs/slice.md), [page type](https://prismic.io/docs/content-modeling.md#page-types), or [custom type](https://prismic.io/docs/content-modeling.md#custom-types) depending on where you need the form data.

   [Learn how to add an integration field](https://prismic.io/docs/fields/integration.md#add-an-integration-field-to-a-content-model)

5. **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.

   > **Important**
   >
   > Server-side submission is a security best practice that enables validation and rate limiting.

   First, create an API endpoint to send form submissions to HubSpot.

   **Next.js example:**

   ```ts filename=src/app/api/forms/[id]/route.ts collapsed
   import { 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 },
         );
   }
   ```

   **Nuxt example:**

   ```ts filename=~/server/api/forms/[id].ts collapsed
   export default defineEventHandler(async (event) => {
     const id = getRouterParam(event, "id");

     // Fetch form details server-side to keep the HubSpot access token secure
     if (event.method === "GET") {
       const res = await fetch(`https://api.hubapi.com/marketing/v3/forms/${id}`, {
         headers: {
           Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
         },
       });
       const form = await res.json();
       return form;
     }

     if (event.method === "POST") {
       const formData = await readFormData(event);
       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: getCookie(event, "hubspotutk") || "",
               pageUri: getHeader(event, "referer") || "",
             },
           }),
         },
       );

       if (response.ok) {
         return { success: true };
       }

       setResponseStatus(event, response.status);
       return { error: "Failed to submit form" };
     }
   });
   ```

   **SvelteKit example:**

   ```ts filename=src/routes/api/forms/[id]/+server.ts collapsed
   import { json } from "@sveltejs/kit";
   import { HUBSPOT_PORTAL_ID, HUBSPOT_ACCESS_TOKEN } from "$env/static/private";
   import type { RequestHandler } from "./$types";

   export const POST: RequestHandler = async ({ params, request, cookies }) => {
     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/${HUBSPOT_PORTAL_ID}/${params.id}`,
       {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
           Authorization: `Bearer ${HUBSPOT_ACCESS_TOKEN}`,
         },
         body: JSON.stringify({
           fields,
           context: {
             hutk: cookies.get("hubspotutk") || "",
             pageUri: request.headers.get("referer") || "",
           },
         }),
       },
     );

     if (response.ok) {
       return json({ success: true });
     }

     return 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:

   **Next.js example:**

   ```tsx filename=src/components/HubSpotForm.tsx collapsed
   "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>
     );
   }
   ```

   **Nuxt example:**

   ```vue filename=~/components/HubSpotForm.vue collapsed
   <script setup lang="ts">
   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;
     };
   };

   const props = defineProps<{
     form: HubSpotForm;
   }>();

   async function handleSubmit(event: Event) {
     event.preventDefault();

     const response = await fetch(`/api/forms/${props.form.id}`, {
       method: "POST",
       body: new FormData(event.target as HTMLFormElement),
     });

     if (response.ok) {
       alert("Form submitted successfully!");
       (event.target as HTMLFormElement).reset();
     } else {
       alert("Failed to submit form. Please try again.");
     }
   }

   function getInputType(fieldType: string): string {
     const types: Record<string, string> = {
       email: "email",
       tel: "tel",
       number: "number",
       date: "date",
       datepicker: "date",
     };
     return types[fieldType] || "text";
   }

   function getDefaultValues(defaultValue: string | undefined): string[] {
     return defaultValue ? defaultValue.split(";") : [];
   }
   </script>

   <template>
     <form @submit="handleSubmit">
       <div v-for="group in form.fieldGroups" :key="group.groupType">
         <template v-for="field in group.fields" :key="field.name">
           <div
             v-if="
               field.fieldType === 'text' ||
               field.fieldType === 'email' ||
               field.fieldType === 'tel' ||
               field.fieldType === 'number' ||
               field.fieldType === 'date' ||
               field.fieldType === 'datepicker'
             "
           >
             <label :for="field.name">
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </label>
             <input
               :type="getInputType(field.fieldType)"
               :id="field.name"
               :name="field.name"
               :value="field.defaultValue ?? ''"
               :placeholder="field.placeholder"
               :required="field.required"
               :min="field.validation?.data?.min"
               :max="field.validation?.data?.max"
             />
           </div>

           <div v-else-if="field.fieldType === 'select'">
             <label :for="field.name">
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </label>
             <select
               :id="field.name"
               :name="field.name"
               :value="field.defaultValue ?? ''"
               :required="field.required"
             >
               <option value="">
                 {{ field.placeholder ?? "Select an option" }}
               </option>
               <option
                 v-for="option in field.options"
                 :key="option.value"
                 :value="option.value"
               >
                 {{ option.label }}
               </option>
             </select>
           </div>

           <div v-else-if="field.fieldType === 'textarea'">
             <label :for="field.name">
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </label>
             <textarea
               :id="field.name"
               :name="field.name"
               :value="field.defaultValue ?? ''"
               :placeholder="field.placeholder"
               :required="field.required"
               rows="4"
             />
           </div>

           <div v-else-if="field.fieldType === 'checkbox'">
             <input
               type="checkbox"
               :id="field.name"
               :name="field.name"
               value="true"
               :checked="field.defaultValue === 'true'"
               :required="field.required"
             />
             <label :for="field.name">
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </label>
           </div>

           <fieldset v-else-if="field.fieldType === 'radio'">
             <legend>
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </legend>
             <div v-for="option in field.options" :key="option.value">
               <input
                 type="radio"
                 :id="`${field.name}-${option.value}`"
                 :name="field.name"
                 :value="option.value"
                 :checked="field.defaultValue === option.value"
                 :required="field.required"
               />
               <label :for="`${field.name}-${option.value}`">
                 {{ option.label }}
               </label>
             </div>
           </fieldset>

           <fieldset v-else-if="field.fieldType === 'booleancheckbox'">
             <legend>
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </legend>
             <div v-for="option in field.options" :key="option.value">
               <input
                 type="checkbox"
                 :id="`${field.name}-${option.value}`"
                 :name="field.name"
                 :value="option.value"
                 :checked="
                   getDefaultValues(field.defaultValue).includes(option.value)
                 "
               />
               <label :for="`${field.name}-${option.value}`">
                 {{ option.label }}
               </label>
             </div>
           </fieldset>

           <div v-else>
             <label :for="field.name">
               {{ field.label }}
               <span v-if="field.required"> *</span>
             </label>
             <input
               type="text"
               :id="field.name"
               :name="field.name"
               :value="field.defaultValue ?? ''"
               :placeholder="field.placeholder"
               :required="field.required"
             />
           </div>
         </template>
       </div>
       <button type="submit">
         {{ form.displayOptions?.submitButtonText ?? "Submit" }}
       </button>
     </form>
   </template>
   ```

   **SvelteKit example:**

   ```svelte filename=src/lib/components/HubSpotForm.svelte collapsed
   <script lang="ts">
     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;
       };
     };

     let { form }: { form: HubSpotForm } = $props();

     async function handleSubmit(event: SubmitEvent) {
       event.preventDefault();

       const response = await fetch(`/api/forms/${form.id}`, {
         method: "POST",
         body: new FormData(event.target as HTMLFormElement),
       });

       if (response.ok) {
         alert("Form submitted successfully!");
         (event.target as HTMLFormElement).reset();
       } else {
         alert("Failed to submit form. Please try again.");
       }
     }

     function getInputType(fieldType: string): string {
       const types: Record<string, string> = {
         email: "email",
         tel: "tel",
         number: "number",
         date: "date",
         datepicker: "date",
       };
       return types[fieldType] || "text";
     }

     function getDefaultValues(defaultValue: string | undefined): string[] {
       return defaultValue ? defaultValue.split(";") : [];
     }
   </script>

   <form onsubmit={handleSubmit}>
     {#each form.fieldGroups as group (group.groupType)}
       <div>
         {#each group.fields as field (field.name)}
           {#if field.fieldType === "text" || field.fieldType === "email" || field.fieldType === "tel" || field.fieldType === "number" || field.fieldType === "date" || field.fieldType === "datepicker"}
             <div>
               <label for={field.name}>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </label>
               <input
                 type={getInputType(field.fieldType)}
                 id={field.name}
                 name={field.name}
                 value={field.defaultValue ?? ""}
                 placeholder={field.placeholder}
                 required={field.required}
                 min={field.validation?.data?.min}
                 max={field.validation?.data?.max}
               />
             </div>
           {:else if field.fieldType === "select"}
             <div>
               <label for={field.name}>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </label>
               <select
                 id={field.name}
                 name={field.name}
                 value={field.defaultValue ?? ""}
                 required={field.required}
               >
                 <option value="">
                   {field.placeholder ?? "Select an option"}
                 </option>
                 {#each field.options ?? [] as option (option.value)}
                   <option value={option.value}>
                     {option.label}
                   </option>
                 {/each}
               </select>
             </div>
           {:else if field.fieldType === "textarea"}
             <div>
               <label for={field.name}>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </label>
               <textarea
                 id={field.name}
                 name={field.name}
                 value={field.defaultValue ?? ""}
                 placeholder={field.placeholder}
                 required={field.required}
                 rows="4"
               />
             </div>
           {:else if field.fieldType === "checkbox"}
             <div>
               <input
                 type="checkbox"
                 id={field.name}
                 name={field.name}
                 value="true"
                 checked={field.defaultValue === "true"}
                 required={field.required}
               />
               <label for={field.name}>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </label>
             </div>
           {:else if field.fieldType === "radio"}
             <fieldset>
               <legend>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </legend>
               {#each field.options ?? [] as option (option.value)}
                 <div>
                   <input
                     type="radio"
                     id={`${field.name}-${option.value}`}
                     name={field.name}
                     value={option.value}
                     checked={field.defaultValue === option.value}
                     required={field.required}
                   />
                   <label for={`${field.name}-${option.value}`}>
                     {option.label}
                   </label>
                 </div>
               {/each}
             </fieldset>
           {:else if field.fieldType === "booleancheckbox"}
             <fieldset>
               <legend>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </legend>
               {#each field.options ?? [] as option (option.value)}
                 <div>
                   <input
                     type="checkbox"
                     id={`${field.name}-${option.value}`}
                     name={field.name}
                     value={option.value}
                     checked={getDefaultValues(field.defaultValue).includes(
                       option.value,
                     )}
                   />
                   <label for={`${field.name}-${option.value}`}>
                     {option.label}
                   </label>
                 </div>
               {/each}
             </fieldset>
           {:else}
             <div>
               <label for={field.name}>
                 {field.label}
                 {#if field.required}
                   *{/if}
               </label>
               <input
                 type="text"
                 id={field.name}
                 name={field.name}
                 value={field.defaultValue ?? ""}
                 placeholder={field.placeholder}
                 required={field.required}
               />
             </div>
           {/if}
         {/each}
       </div>
     {/each}
     <button type="submit">
       {form.displayOptions?.submitButtonText ?? "Submit"}
     </button>
   </form>
   ```

   Finally, display the form selected through the integration field. This example shows how to display the form in a slice.

   **Next.js example:**

   ```tsx filename=src/slices/Form/index.tsx
   import 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>
     );
   }
   ```

   **Nuxt example:**

   ```vue filename=~/slices/Form/index.vue
   <script setup lang="ts">
   import type { Content } from "@prismicio/client";
   import { isFilled } from "@prismicio/client";
   import HubSpotForm from "~/components/HubSpotForm.vue";
   import type { HubSpotForm as HubSpotFormData } from "~~/server/api/forms";

   const props = defineProps(getSliceComponentProps<Content.FormSlice>(["slice"]));

   const { data: form } = await useAsyncData(
     `form-${props.slice.primary.form.id}`,
     async () => {
       if (!isFilled.integration(props.slice.primary.form)) {
         return null;
       }

       const res = await fetch(
         new URL(
           props.slice.primary.form.id as HubSpotFormData["id"],
           "https://api.hubapi.com/marketing/v3/forms/",
         ),
         {
           headers: {
             Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
           },
         },
       );
       return res.json();
     },
   );
   </script>

   <template>
     <section v-if="form">
       <HubSpotForm :form="form" />
     </section>
   </template>
   ```

   **SvelteKit example:**

   ```ts filename=src/routes/[[preview=preview]]/[uid]/+page.server.ts collapsed
   // SvelteKit server-side data fetching strategy:
   // - All external API calls happen on the server in +page.server.ts
   // - Use mapSliceZone to enhance slices with additional data
   // - Components receive pre-fetched data as props (no client-side fetching)

   import type { PageServerLoad } from "./$types";
   import { createClient } from "$lib/prismicio";
   import { mapSliceZone, isFilled } from "@prismicio/client";
   import { HUBSPOT_ACCESS_TOKEN } from "$env/static/private";

   export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
     const client = createClient({ fetch, cookies });
     const page = await client.getByUID("page", params.uid);

     const slices = await mapSliceZone(page.data.slices, {
       form: async ({ slice, ...otherProps }) => {
         if (!isFilled.integration(slice.primary.form)) {
           return {
             slice,
             ...otherProps,
             form: null,
           };
         }

         const res = await fetch(
           `https://api.hubapi.com/marketing/v3/forms/${slice.primary.form.id}`,
           {
             headers: {
               Authorization: `Bearer ${HUBSPOT_ACCESS_TOKEN}`,
             },
           },
         );
         const form = await res.json();

         return {
           slice,
           ...otherProps,
           form,
         };
       },
     });

     return { page, slices };
   };
   ```

   Render the page's slices using the `slices` property:

   ```svelte filename=src/routes/[[preview=preview]]/[uid]/+page.svelte
   <script lang="ts">
     import { SliceZone } from "@prismicio/svelte";
     import { components } from "$lib/slices";

     let { data } = $props();
   </script>

   <SliceZone slices={data.slices} {components} />
   ```

   Use the `form` prop in the slice component:

   ```svelte filename=src/lib/slices/Form/index.svelte
   <script lang="ts">
     import type { Content } from "@prismicio/client";
     import type { SliceComponentProps } from "@prismicio/svelte";
     import HubSpotForm from "$lib/components/HubSpotForm.svelte";
     import type { HubSpotForm as HubSpotFormData } from "$lib/../routes/api/forms/+server";

     type Props = SliceComponentProps<Content.FormSlice> & {
       form: any;
     };

     let { slice, form }: Props = $props();
   </script>

   {#if form}
     <section>
       <HubSpotForm {form} />
     </section>
   {/if}
   ```

   > These components are intentionally unstyled. Add your own CSS classes or styling solution to match your design system.

6. **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:

| Property | Type   | Description                                | Default |
| -------- | ------ | ------------------------------------------ | ------- |
| id       | string | The form's unique identifier from HubSpot. | None    |
