Finally! A Simple Way to Embed HubSpot Forms in Next.js - step-by-step guide

Published on November 9, 2025 by ImageMagix Team

Share:
Thumbnail for Finally! A Simple Way to Embed HubSpot Forms in Next.js - step-by-step guide

Introduction: Why Connect a Custom React Form to HubSpot?

Hello Devs! In this blog, we’ll learn how to easily integrate a HubSpot Form with React.js and Next.js. If you want to collect user data, leads, or contact information directly from your website or web app, HubSpot Forms can be a great solution for you. HubSpot is a popular marketing, sales, and customer relationship management (CRM) platform that allows you to gather leads (name, email, message, etc.) and link them to marketing resources.

Why use Next.js + React? Because you can create any kind of custom user interface (UI) with React, including elegant forms, client-side validation, and sophisticated interactions. Next.js adds a server layer (API routes) that is perfect for securely forwarding form submissions to HubSpot. This approach gives you:

  • Aa personalized look and feel for your form (React).
  • Secure server-side forwarding to HubSpot (Next.js API route).
  • Improved control over tracking, error messages, and validation.

Why Use the Custom API Approach?

There are two common ways to add a HubSpot form:

1. Embed HubSpot’s form script: HubSpot gives you a snippet that renders the form automatically.
Pros: Very quick, no server work.
Cons: Less control over design and behavior.

2. Custom React form + HubSpot Forms API (this article): You build the UI and send data to HubSpot via a server route.
Pros: Full control over design, validation, tracking, and user experience. You can add reCAPTCHA, custom analytics, and handle errors exactly how you want.
Cons: Slightly more work as you must create one server endpoint to forward data.

Which to choose? If you want a pixel-perfect form, special validation logic, or you want to protect sensitive keys and avoid CORS issues — go with the custom API approach. If you want the fastest route and don’t need a custom UI, use the embed option.

Step 1: Get HubSpot Portal ID and Form ID

First, log into your HubSpot account. Navigate to Marketing → Forms. Open the form you created and note down the Portal ID (a number) and the Form ID (a long GUID). You will use these to build the HubSpot endpoint URL.

Step 1: Get HubSpot Portal ID and Form ID

Use the visual editor to build forms with multisteps, advance styling, and more

From here you can select any template according to your requirement

Choose which type of field you want to connect to your form and click on review and submit

Now your form is published and to embed this form, simply copy and paste the code into the HTML code or you can follow the below steps

Step 2: Know the Submission URL and Payload Shape

HubSpot’s submission URL looks like this: `https://api.hsforms.com/submissions/v3/integration/submit/HUBSPOT_PORTAL_ID/HUBSPOT_FORM_ID`. You'll send a POST request to this URL with a specific JSON payload.

Step 3: Create a Next.js API Route to Forward Requests (Server)

Calling HubSpot from the browser can cause CORS problems and may expose sensitive keys. The best practice is to create a Next.js API route that accepts your form data and forwards it to HubSpot.

Here is an example for a Next.js App Router file located at `app/api/submit-hubspot/route.ts`:

import { NextResponse } from "next/server";

const HUBSPOT_ENDPOINT = `https://api.hsforms.com/submissions/v3/integration/submit/${process.env.HUBSPOT_PORTAL_ID}/${process.env.HUBSPOT_FORM_ID}`;

export async function POST(req: Request) {
  const data = await req.json(); // e.g., { name, email, message }
  const payload = {
    fields: [
      { name: "name", value: data.name },
      { name: "email", value: data.email },
      { name: "message", value: data.message }
    ],
    context: { pageUri: data.pageUri || "", pageName: "Contact page" }
  };

  const res = await fetch(HUBSPOT_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload)
  });

  if (!res.ok) {
    const text = await res.text();
    return NextResponse.json({ message: "HubSpot error: " + text }, { status: res.status });
  }
  return NextResponse.json({ message: "ok" });
}
typescript

Remember to put `HUBSPOT_PORTAL_ID` and `HUBSPOT_FORM_ID` in your environment variables (.env file). This server code keeps secrets out of the browser and avoids CORS issues.

Step 4: Build the React Client Form (Browser)

Your React form should submit data to your new Next.js API route (`/api/submit-hubspot`), not directly to HubSpot. The flow is simple:
1. A user types into the form fields.
2. Your client-side code validates the input (e.g., checking for required fields, valid email format).
3. Your client calls `/api/submit-hubspot` with the form data in a JSON payload.
4. The server forwards the data to HubSpot and returns the result.
5. Your client shows a success or error message to the user.

Important: Make sure the `name` keys you send in your payload (e.g., `name`, `email`, `message`) match the internal names of the fields in your HubSpot form.

// components/ContactForm.tsx  (client component)
"use client";
import { useState } from "react";

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });
  const [submitting, setSubmitting] = useState(false);
  const [success, setSuccess] = useState(false);
  const [error, setError] = useState < string | null > (null);

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
    setError(null);
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    try {
      const res = await fetch("/api/submit-hubspot", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(formData),
      });
      if (!res.ok) {
        const err = await res
          .json()
          .catch(() => ({ message: "Unknown error" }));
        throw new Error(err.message || `Status ${res.status}`);
      }
      setSuccess(true);
      setFormData({ name: "", email: "", message: "" });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to send.");
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center p-6 bg-gray-50">
      <div className="max-w-lg w-full bg-white p-8 rounded-xl shadow-lg">
        <h2 className="text-2xl font-bold text-center mb-2">Contact Us</h2>
        <p className="text-center text-gray-600 mb-6">
          Fill out the form and we’ll get back to you soon.
        </p>

        {success ? (
          <div className="text-green-600 text-center font-medium">
            ✅ Message sent successfully!
          </div>
        ) : (
          <form onSubmit={handleSubmit} className="space-y-4">
            {error && (
              <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
                {error}
              </div>
            )}
            <div>
              <label className="block text-gray-700 mb-1 font-medium">
                Name
              </label>
              <input
                type="text"
                name="name"
                value={formData.name}
                onChange={handleChange}
                required
                className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
              />
            </div>

            <div>
              <label className="block text-gray-700 mb-1 font-medium">
                Email
              </label>
              <input
                type="email"
                name="email"
                value={formData.email}
                onChange={handleChange}
                required
                className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
              />
            </div>

            <div>
              <label className="block text-gray-700 mb-1 font-medium">
                Message
              </label>
              <textarea
                name="message"
                rows={4}
                value={formData.message}
                onChange={handleChange}
                required
                className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
              ></textarea>
            </div>

            <button
              type="submit"
              disabled={submitting}
              className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
            >
              {submitting ? "Sending..." : "Send Message"}
            </button>
          </form>
        )}
      </div>
    </div>
  );
}
tsx

Step 5: Handle Errors, Retries, and UX

Create a smooth user experience:

  • Show a friendly error message if the server returns an error (e.g., "Something went wrong, please try again.").
  • Disable the submit button while the form is submitting to prevent multiple submissions.
  • On success, clear the form inputs and show a thank-you message.
  • Log server-side errors so you can debug any field mapping or payload issues.

Step 6: Security & Extras

  • Add reCAPTCHA: To prevent spam, add a reCAPTCHA to your form and verify the token on the server-side before forwarding the request to HubSpot.
  • Include Context: Add `context.pageUri` to your payload so HubSpot can track where the lead originated.
  • HubSpot Tracking: Optionally, include HubSpot's tracking script on your page to enrich lead data with session information and improve marketing attribution.

Step 7: Test Everything

After setting everything up, it's time to test:
1. Submit your form and check the browser's Network tab for the request to your API route.
2. Check your Next.js server logs for any errors.
3. Finally, confirm that the new lead appears in your HubSpot account's contacts or form submissions list.

Step 7: Test Everything

Conclusion

Integrating a custom React and Next.js form with the HubSpot API gives you the best of both worlds: complete design control and secure, reliable data submission. While it requires a bit more setup than a simple embed, the custom API approach provides a far superior and more professional user experience. Happy coding!

Related Tools

Try out these tools mentioned in the article.

Related Articles

Comments

No comments yet. Be the first to comment!

Leave a Reply

Your email address will not be published.