Discuss your task

HubSpot + Typeform: form answers into contact and deal

Through the form_response webhook, Typeform sends all form answers in real time. The custom integration parses the answers array by field.ref, creates or updates a contact in HubSpot via email upsert, creates a linked deal with custom properties, and writes UTM parameters from hidden fields into the card. No manual transfer, no data loss on complex field types.

In Exceltic.dev projects we regularly see the same pattern: a qualification form in Typeform is filled out, the manager gets an email notification, opens the answers in Typeform, and manually copies the fields into HubSpot - company, budget, traffic source, task type. This takes 5-7 minutes per lead. With 30 leads a week, a full working day is lost every month. The native Typeform integration with HubSpot covers the basic scenario but breaks exactly where the form is more complex than “name + email.”

Webhook - an HTTP POST request that Typeform automatically sends to a specified URL when a form is submitted. The request body contains a complete JSON with answers, metadata, and hidden fields.

This article explains what breaks in the native integration, how the custom webhook-based scheme works, and what results it produces in a typical project.

What the native integration does (and where it breaks)

The native Typeform integration with HubSpot works through the official connector from the HubSpot marketplace. It can create contacts by email, update standard properties, and - with limitations - pass data to deals and companies.

Limitations that clients run into:

  • Only standard fields for complex types. Data from non-HubSpot form fields (i.e., custom Typeform questions) maps only to single-line text HubSpot properties. If you need to populate a property of type dropdown, number, or checkbox, the native integration passes the value as text - and HubSpot rejects or ignores it.

  • Partial responses do not go to deals or companies. Partial responses - submissions where the form did not reach the final step - are synced only to contacts. They do not reach deals or companies at all.

  • Maximum 1000 historical responses on first connection. If the form was used before the integration was connected, only the 1000 most recent responses will land in HubSpot.

  • One form - one HubSpot account. It is not possible to route answers simultaneously to multiple portalIds, which blocks multi-account scenarios.

  • No deduplication by custom field. The native integration finds contacts only by email. If the email changed or is absent from the form, a duplicate is created.

For a qualification form with 8-10 questions - budget, project type, team size, current stack, source - the native integration transfers at best half the data into the right fields. The rest the manager fixes manually.

A similar problem appears in other native HubSpot integrations: HubSpot + Slack: what the native integration can’t do loses deal context when routing notifications, and HubSpot + Notion: native integration cannot handle real-time sync.

What the custom integration handles

The custom scheme uses the Typeform Webhooks API to receive answers in real time and the HubSpot CRM API to write data.

Webhook form_response -> parsing answers

Each time a respondent completes a form, Typeform sends a POST request to the specified endpoint. The request body contains a form_response object with an answers array.

Structure of each element in answers:

{
  "type": "text",
  "text": "HubSpot + Typeform",
  "field": {
    "id": "JwWggjAKtOkA",
    "type": "short_text",
    "ref": "project_description"
  }
}

Answer types supported by the Typeform Webhooks API (documentation):

  • text - text fields (short_text, long_text)
  • choice - single select, contains choice.label and choice.ref
  • choices - multiple select, contains an array of choices.labels
  • number - numeric fields, ratings, opinion scale
  • email - email field, value in answer.email
  • phone_number - phone, value in answer.phone_number
  • boolean - yes/no questions

Key point: answers in the array are not in the same order as the questions in the form. For reliable mapping, use field.ref - a human-readable field identifier set in the Typeform form settings.

def parse_answers(form_response):
    result = {}
    for answer in form_response.get("answers", []):
        ref = answer["field"]["ref"]
        answer_type = answer["type"]
        if answer_type == "text":
            result[ref] = answer["text"]
        elif answer_type == "choice":
            result[ref] = answer["choice"]["label"]
        elif answer_type == "choices":
            result[ref] = ", ".join(answer["choices"]["labels"])
        elif answer_type == "number":
            result[ref] = answer["number"]
        elif answer_type == "email":
            result[ref] = answer["email"]
        elif answer_type == "phone_number":
            result[ref] = answer["phone_number"]
    return result

Hidden fields for UTM parameters

Hidden fields are parameters passed into the form through the URL without being shown to the respondent. The standard scenario: a visitor arrives from an ad, the URL contains ?utm_source=google&utm_campaign=q2_leads, these values are passed into Typeform through hidden fields and returned in the webhook.

In the payload they are located outside the answers array, in a separate object:

"hidden": {
  "utm_source": "google",
  "utm_campaign": "q2_leads",
  "utm_medium": "cpc"
}

The custom integration writes these values into custom contact and deal properties in HubSpot. The native integration does not support mapping hidden fields to arbitrary properties.

HubSpot contact upsert by email

The HubSpot CRM API provides a batch upsert endpoint that has supported email as idProperty since September 2024 (HubSpot documentation):

POST https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert

Request body:

{
  "inputs": [
    {
      "id": "[email protected]",
      "idProperty": "email",
      "properties": {
        "firstname": "Ivan",
        "company": "Acme Corp",
        "budget_range": "50k-100k",
        "hs_lead_source": "google",
        "utm_campaign": "q2_leads"
      }
    }
  ]
}

Upsert logic: if a contact with that email already exists, properties are updated. If not, a contact is created. This eliminates duplicates when the same email submits the form again.

Creating a deal with custom fields from the form

After creating or updating the contact, a deal is created via POST /crm/v3/objects/deals. The deal is linked to the contact through the associations API. All relevant form answers - budget, project type, deadline, priority - are written to custom deal properties.

This is the key difference from the native integration: custom deal properties accept values of the correct type - number, dropdown, date - because the integration itself converts the string from the answer into the right format before sending it to the HubSpot API.

A related pattern is used for HubSpot + Slack: new deal notifications - immediately after deal creation, a structured notification can be sent to the relevant channel.

Step-by-step integration flow

  1. Set up hidden fields in Typeform. Hidden fields for utm_source, utm_campaign, utm_medium, and utm_content are added in the form settings. On the landing page, a script reads URL parameters and passes them into the form embed code.

  2. Set field.ref for each question. In the Typeform editor, each question gets a unique human-readable ref: budget_range, team_size, current_crm, project_type. This makes the mapping resilient to question renaming.

  3. Deploy the webhook server. A small service (Python/Node.js) is deployed on a dedicated endpoint. Typeform sends POST requests to it. The service verifies the request signature via the Typeform-Signature header (HMAC-SHA256) and rejects unsigned requests.

  4. Register the webhook in Typeform. Through the Typeform Webhooks API or in the interface: Workspace -> Forms -> Integrations -> Webhooks. Provide the endpoint URL and the signing secret.

  5. Parse payload and map. The service reads form_response.answers, iterates over the array, and extracts values by field.ref. Separately reads form_response.hidden for UTM parameters.

  6. Upsert contact in HubSpot. Request to POST /crm/v3/objects/contacts/batch/upsert with idProperty: "email". If the contact exists - properties are updated. If not - created.

  7. Create deal and association. A deal is created in the right pipeline and stage via POST /crm/v3/objects/deals. The deal is linked to the contact via PUT /crm/v3/objects/deals/{dealId}/associations/contacts/{contactId}/deal_to_contact.

  8. Write UTM and custom fields to both cards. UTM parameters are written to both the contact and the deal - so the traffic source is visible on both objects in HubSpot.

The scheme described here is analogous to the Kommo + Tilda: form submissions into the pipeline without Zapier integration - the same principles of webhook reception and email upsert, adapted to the HubSpot data model.

Real-world case

A B2B company in IT services, 35 employees. Qualification form in Typeform: 9 questions including budget (number), project type (choice from 6 options), client team size (number), current stack (multiple choice), and deadline (date). Plus hidden fields for 4 UTM parameters.

Before the integration: each lead was processed manually in 6-8 minutes. 40 leads per month - about 5 hours of operational work just for data transfer. Some data was lost in copying, HubSpot was left with empty fields, and the manager could not see the UTM source directly in the card.

After launching the custom integration:

  • All 9 form fields automatically land in contact and deal properties
  • UTM parameters are written to both cards
  • The deal is created in the right pipeline with the first funnel stage automatically
  • Time from form submission to deal appearing in HubSpot - under 3 seconds
  • Operational time per lead went from 6-8 minutes to 0

Development and deployment time for a typical integration of this type - 3-5 business days.

A similar approach is used when building HubSpot + Notion: client database - contact and deal data can be mirrored to a Notion database immediately on creation.

Who this is relevant for

A custom Typeform + HubSpot integration is justified if:

  • The form has 5+ questions, at least 2-3 of which need to land in custom HubSpot properties (not just single-line text)
  • UTM parameters are passed into the form through hidden fields and you want to see the traffic source in the deal card
  • Lead volume is 20+ per month - at lower volumes, manual transfer may be cheaper than development
  • You need to automatically create a deal (not just a contact) and link it to the right pipeline and stage
  • Deduplication is required: repeat submissions from the same email must update the existing contact, not create a duplicate

If the form collects only a name and email for basic subscription - the native integration will handle it. For qualification forms with business logic, the native integration creates more operational problems than it solves.

Frequently asked questions

Is it true that the native Typeform-HubSpot integration does not work with custom fields?

Yes, partially. The native integration is limited in mapping complex types: data from custom Typeform questions lands in HubSpot only as single-line text. This means that if HubSpot has a property of type number, dropdown, or checkbox, the native integration cannot correctly populate it from the form. The custom webhook integration solves this: the service itself converts the value to the right data type before sending it to the HubSpot API.

How does contact deduplication work for repeat submissions?

The HubSpot Contacts upsert API (available since September 2024) accepts the parameter idProperty: "email". If a contact with that email already exists in the CRM, the API updates its properties. If not, it creates a new contact. This is a standard idempotent operation: no matter how many times the same email submits the form, there will be no duplicates. For more complex deduplication - for example, by a phone field - a similar mechanism is used with a different idProperty or an additional check before upsert.

Can answers from multiple different Typeform forms be sent to HubSpot?

Yes. Each form registers a separate webhook, but they can all point to the same handler service. The service identifies the form by form_id in the payload and applies the corresponding mapping. This is a standard pattern when working with multiple qualification forms - for example, separate forms for different products or regions.

What happens if the HubSpot API is unavailable when a webhook arrives?

Typeform does not automatically retry delivery on failure on your side - if the server returns a non-200 code, the event is considered lost. The correct architecture: the handler service immediately returns 200 and queues the task (Redis, RabbitMQ, SQS). A worker processes the queue with retry logic using exponential backoff. This approach guarantees that no form response is lost even during temporary HubSpot API issues.

How long does development of this integration take?

A typical project - integrating one form with contact and deal creation, mapping 8-12 fields, UTM hidden fields support, deployment with a retry queue - takes 3-5 business days. If more complex business logic is needed - for example, routing to different pipelines based on a budget answer, or Slack notifications on deal creation - add 1-2 days.


If your qualification form loses data when transferred to HubSpot or managers spend time manually copying answers - describe the problem to the Exceltic.dev team. We will review your form structure, evaluate the mapping, and propose an integration scheme for your stack.

More articles

All →