HubSpot Forms API: Capturing Leads From Any Website Without Losing Data
A lot of companies live with a quiet split: the website was built by one team on one stack, the CRM is HubSpot, and the form on the contact page is a native HTML form that posts to the site's own backend. Someone then writes a cron job that moves those submissions into HubSpot once a night, and the sales team wonders why hot leads arrive cold. The form works. The data arrives. But the lead lost its timing, its page context, and its tracking cookie, and nobody can tell which campaign produced it.
The Forms API exists to close that split. You keep your own form, your own design and your own front end, and you send each submission to HubSpot the moment it happens, with the context HubSpot needs to attribute it and the consent record the law requires. In this guide, I will walk through the whole path: which forms you can create via API, how to map fields to properties before you touch anything, the two submission endpoints and when each is right, the consent block, the cookie that makes attribution work, and the error codes you will meet.
Which forms the API can create, and which it cannot
The Marketing Forms v3 API creates and manages forms of the type HubSpot calls hubspot, the classic form object with field groups, display options and consent settings. Those are the forms you create with POST /marketing/v3/forms/, read with a GET on the form id, and extend with a PATCH on fieldGroups. They work with the submission endpoints described below and with every workflow trigger that reads a form id.
What the API does not create is a form built in HubSpot's newer form editor. If your team already uses that editor, you can keep those forms for HubSpot-hosted pages, but the form that receives submissions from an external site should be created or managed through the v3 API, so the object you submit to is the same object you define. Knowing this line up front saves the most common surprise in this project: a team builds a beautiful form in the new editor, then discovers the integration cannot create or alter it programmatically.
Map fields to properties before creating anything
Every form field writes to a contact property, so the first job is not the form, it is the property audit. Pull all contact properties with GET /crm/v3/properties/contacts?archived=false, which is not paginated, and for each field on your site check three things: does a property already exist, by internal name or by label; what are its type and field type; and do its option values match exactly what the site will send.
Portals accumulate look-alike properties over the years, several variants of the same job title or company size field. Reuse the one the current site and the active workflows already use, and create a new property only when nothing fits or when the option ranges are incompatible, because changing a numeric band on an existing property breaks historical reports. Before you touch any property, list the active workflows that depend on it and on the current form id, because changing options, removing a field or moving the site to a new form id breaks those workflows in silence.
When you need a new enumeration property, create it with the label equal to the value for each option unless the portal has a different convention, and remember that a PATCH on options replaces the whole list. Always read the current options first, merge, send the full merged list, and check the result. A 409 on create means the property already exists, which you can treat as success.
Create the form, and turn reCAPTCHA off
Creation is a single POST with the form type, name, field groups, configuration, display options and consent options. Make it idempotent: list existing forms first, with cursor pagination, and skip creation if one with the same name is already there. Two settings decide whether API submissions will work at all. recaptchaEnabled must be false, because a form with reCAPTCHA on rejects every API submission with FORM_HAS_RECAPTCHA_ENABLED. And each field object needs the object type, the property name, the form field type and the required and hidden flags.
One detail is worth more than it looks: a form option's value must exist in the property, but its label can differ. That is the clean way to show new wording on the site while still recording the value your reports and workflows already know. You change what the visitor reads without changing what the CRM stores.
Two submission endpoints, one rule about the key
Submissions go to api.hsforms.com, and there are two doors. The secure endpoint, /submissions/v3/integration/secure/submit/{portalId}/{formGuid}, takes a bearer credential; an account service key or a private app token with the forms scope both work. The public endpoint, /submissions/v3/integration/submit/{portalId}/{formGuid}, takes no credential at all. The rule that decides between them is where the call originates.
If the site submits from a backend, use the secure endpoint and keep the key in a server environment variable, never in browser code, never in a build variable exposed to the client, never in the repository. If the site submits straight from the browser, there is no safe place for a key, so use the public endpoint. The secure endpoint is also the one that lets you pass the visitor's IP address, which only makes sense from a server.
POST https://api.hsforms.com/submissions/v3/integration/secure/submit/{portalId}/{formGuid} Authorization: Bearer {key} { "submittedAt": 1718035200000, "fields": [ { "objectTypeId": "0-1", "name": "email", "value": "ana@example.com" }, { "objectTypeId": "0-1", "name": "firstname", "value": "Ana" } ], "context": { "hutk": "cookie_value", "pageUri": "https://site.com/contact", "pageName": "Contact" }, "legalConsentOptions": { "consent": { "consentToProcess": true, "text": "I agree to the processing of my data." } } }
The cookie that makes attribution work
The difference between a lead HubSpot can attribute and one it cannot is the hutk value inside context. It is the HubSpot tracking cookie set in the visitor's browser by the tracking code. When you pass it with the submission, HubSpot ties the new contact to the browsing history it already recorded, so the original source, the pages visited and the campaign all attach to the lead. Without it, the contact is created clean, with no history, and your attribution reports have a hole where that lead should be.
In a backend submission, read the cookie in the browser and forward it to your server with the form data, then place it in the context. Pass the page URI and page name too, so the submission shows which page produced it. The timestamp goes in submittedAt, in milliseconds, and it cannot be older than one month.
Consent is not optional, and it is never pre-checked
Under GDPR and LGPD, processing someone's data needs their explicit consent, and HubSpot enforces that at the API. The form carries a consent configuration, the site shows a checkbox with the consent text, and the submission sends consentToProcess: true with the displayed text only when the visitor checked the box. The checkbox is never pre-checked: HubSpot does not allow it and the law does not accept it, so the site must block the submit while the box is empty.
A submission without consent returns a 400 with MISSING_PROCESSING_CONSENT, which is the negative test you should keep in your test suite. And if the consent text ever changes, change it in three places at once: the site, the form configuration, and the text sent with each submission. A mismatch between what the visitor read and what was recorded is exactly what an audit looks for.
The error codes you will meet
A short table of responses prevents most support tickets. A 200 means the submission was accepted. A 400 means do not retry; log the errorType, because it names the exact problem: a field that is not in the form definition returns FIELD_NOT_IN_FORM_DEFINITION, which is what happens when a site sends a honeypot field or any field you did not add to the form. A 401 or 403 is a key or scope problem. A 404 means the portal id or form guid is wrong. A 429 or a 5xx is the only case to retry, with exponential backoff, keeping the payload so nothing is lost, the same idempotency and dead-letter discipline that keeps webhooks reliable. Log the x-hubspot-correlation-id header on every failure, because it is what HubSpot support will ask for.
Two formatting rules remove a whole class of 400s. Multiple-choice values go in one field item, separated by semicolons. And optional fields that are empty should be omitted, not sent as empty strings.
A tip from someone who has been burned: test three things before you hand the integration to the developer: the secure endpoint with the real key, the public endpoint with no key, and a submission with no consent that must come back as MISSING_PROCESSING_CONSENT. Use a test email you own, because the test creates or updates a real contact. Ten minutes of this and the developer gets working examples instead of a specification to interpret.
Why this matters for your operation
A lead that arrives in HubSpot the second it is submitted, with its source and its consent attached, is a lead sales can act on and marketing can measure. The same lead arriving hours later, stripped of context, is a name in a list. The Forms API is what turns a website you do not host on HubSpot into a first-class source of attributed, compliant leads, without rebuilding the site.
The cost of skipping the discipline in this guide is not that the integration fails loudly. It is that it works badly in silence: duplicates from fields that did not match, reports with blanks where attribution should be, and a consent trail that does not hold up. Each step here, the property audit, the right endpoint, the cookie, the consent block, is cheap, and together they are the difference between a form that fills the CRM and one that pollutes it.
In practice: the leads that arrived every morning at six
A company had its own website form posting to its own database, and a nightly job that pushed the rows into HubSpot. Every lead arrived at six in the morning with the same creation time, no source, no page, no cookie. Sales called people who had asked for contact the previous afternoon, and marketing could not say which campaign any of them came from.
The fix was to keep the form and change where it posted. The backend started submitting each entry to the secure endpoint the moment it arrived, with the tracking cookie forwarded from the browser, the page context, and the consent text the visitor had accepted. Leads started appearing in real time, attributed to their real source, and the first follow-up call moved from the next morning to within the hour. The website did not change. Only the last mile did. When the follow-up itself needs to run inside HubSpot, the same care applies to custom code actions and their secrets.
Checklist for lead capture via the Forms API
- Did you audit existing contact properties and reuse the ones the site and workflows already use?
- Was the form created through the v3 API with reCAPTCHA turned off?
- Does a backend use the secure endpoint with the key in a server variable, and a browser-only form use the public endpoint?
- Does every submission carry the tracking cookie, page URI and page name in the context?
- Is consent sent only when the visitor checked a never pre-checked box, with the exact displayed text?
- Do you retry only on 429 and 5xx, log the error type on 400, and keep the correlation id?
Frequently asked questions
Can I create any HubSpot form through the API?
The Marketing Forms v3 API creates and manages forms of the classic hubspot type. Forms built in HubSpot's newer form editor are not created through the API, so the form that receives external submissions should be defined through v3.
Which submission endpoint should I use?
The secure endpoint when the site submits from a backend that can hold the key safely, and the public endpoint when the submission comes straight from the browser, where no key can be stored safely.
What credential does the secure endpoint accept?
A bearer credential: an account service key or a private app token with the forms scope. Keep it in a server environment variable, never in client code or the repository.
Why are my leads missing their original source?
Because the submission did not include the hutk tracking cookie in the context. Forward the cookie from the browser and pass it with the submission so HubSpot links the contact to its browsing history.
Why does the API reject my submission with MISSING_PROCESSING_CONSENT?
Because the consent block was missing or false. Send consentToProcess: true with the displayed consent text only after the visitor checked a box that was never pre-checked.
Which errors should I retry?
Only 429 and 5xx, with exponential backoff and the payload kept for resend. A 400 names a definite problem in the error type and should be logged, not retried.
Let's talk?
Want your website leads to land in HubSpot in real time, attributed and compliant, without rebuilding the site? At Insight Sales we design and deliver form and system integrations end to end. Talk to our team and find out how we can help.
Ready to take your operation to the next level?
Talk to a specialist and see how we can help.