How to Send Typeform Leads to HubSpot

Create or update a HubSpot contact for every Typeform lead, map answers to contact properties, and alert the owner, using HubSpot's MCP server in Agent Mode or the HubSpot CRM API through the HTTP Requests connector.

What This Integration Does

Typeform is good at collecting leads; HubSpot is where sales works them. This Spojit workflow connects the two. Each Typeform submission arrives at a signed Spojit webhook; the workflow pulls out the email, name, company and qualifying answers, and creates the contact in HubSpot, or updates it if the email already exists, with the answers stored in contact properties. Optionally it tells the contact owner in Slack. You can do the HubSpot step two ways: with HubSpot's own MCP server, where an Agent Mode prompt decides how to create or update the record, or with the HubSpot CRM API in Direct Mode, which is fully predictable.

Updates are keyed on email, so a person who fills the form twice is one contact with their latest answers, not two. Each submission is one run, independent of the others.

Prerequisites

  • The Typeform webhook and Custom signing connection from the "How to Send Typeform Responses to Slack" tutorial in this help center (header Typeform-Signature, SHA-256, Base64, prefix sha256=, raw body).
  • Either a HubSpot MCP server connection (added with Add custom server, server URL https://mcp.hubspot.com, OAuth 2.0 with an MCP connector created in HubSpot), or a HubSpot private app access token with the crm.objects.contacts.write scope for an HTTP Requests connection (Base URL https://api.hubapi.com, Bearer token).
  • Custom contact properties in HubSpot for the answers you want to keep, such as lead_budget and lead_timeline.

Step 1: Webhook Trigger

Add a Trigger node with type Webhook and your Typeform signing connection, and paste the URL into the form's Connect -> Webhooks settings with the same secret.

Step 2: Extract the Lead Fields

Give the questions references in Typeform (email, first_name, last_name, company, budget, timeline). Then add a Code Runner step with execute-javascript and input {{ input.form_response }}:

const out = {};
for (const a of data.answers) {
  out[a.field.ref] = a.choice?.label ?? a.choices?.labels?.join(', ') ?? a[a.type];
}
return out;

Name the output lead. The values are in {{ lead.result.email }}, {{ lead.result.company }} and so on. Add a Condition that stops the run when {{ lead.result.email }} is empty.

Step 3a: Create or Update with the HubSpot MCP Server (Agent Mode)

Add a Connector node on your HubSpot MCP connection in Agent Mode. Prompt:

Create or update a HubSpot contact for this lead. Look the contact up by email first;
if it exists, update it rather than creating a duplicate.
Email: {{ lead.result.email }}
First name: {{ lead.result.first_name }}
Last name: {{ lead.result.last_name }}
Company: {{ lead.result.company }}
Set lead_budget to "{{ lead.result.budget }}" and lead_timeline to "{{ lead.result.timeline }}".
Return the contact ID and whether it was created or updated.

Add a Response Schema with contactId and action so later steps can use the result. HubSpot decides which tools its MCP server offers; use Available Tools to tick only contact search, create and update.

Step 3b: Or Upsert with the CRM API (Direct Mode)

For a deterministic step, add a Connector node on the HubSpot HTTP connection with http-post:

  • url: /crm/v3/objects/contacts/batch/upsert
  • body:
    {
      "inputs": [
        {
          "idProperty": "email",
          "id": "{{ lead.result.email }}",
          "properties": {
            "email": "{{ lead.result.email }}",
            "firstname": "{{ lead.result.first_name }}",
            "lastname": "{{ lead.result.last_name }}",
            "company": "{{ lead.result.company }}",
            "lead_budget": "{{ lead.result.budget }}",
            "lead_timeline": "{{ lead.result.timeline }}"
          }
        }
      ]
    }

The batch upsert endpoint creates the contact or updates the one with that email in a single call, and returns the contact's id in results.

Step 4: Notify the Owner

Add a Slack step with send-message to your sales channel: "New Typeform lead: {{ lead.result.first_name }} {{ lead.result.last_name }} at {{ lead.result.company }}, budget {{ lead.result.budget }}." Include a link to the contact using the ID from Step 3. For high-value answers, add a Condition on the budget and send those to a priority channel.

Tips

  • Lifecycle stage: set lifecyclestage to lead in the properties so new contacts land in the right lists and workflows in HubSpot.
  • UTM tracking: pass utm_source and utm_campaign as Typeform hidden fields and map them to HubSpot properties to keep attribution.
  • Qualify with AI: an Agent Mode step can score the lead from the free-text answers and write the score to a lead_score property before the upsert.
  • Property names, not labels: HubSpot properties have an internal name (lead_budget) and a label ("Lead budget"). The API needs the internal name.

Common Pitfalls

  • Dropdown properties: HubSpot rejects values that are not one of a dropdown property's options. Make the Typeform choices match the HubSpot options exactly, or use single-line text properties.
  • Missing scope: a private app token without crm.objects.contacts.write returns 403. Add the scope and re-copy the token.
  • Duplicates from creates: using the plain create endpoint instead of upsert makes a second contact for repeat submissions. Use the batch upsert with idProperty email.
  • Agent creating duplicates: in Agent Mode, always tell the agent to search by email first, and check the returned action during testing.

Testing

Submit the form with a test email, check the Spojit run, and find the contact in HubSpot with the answers in its properties. Submit again with the same email and a different budget, and confirm the same contact was updated rather than a new one created.

Learn More

Ready to build this?

Start on the free plan: 500 AI credits a month, no card needed.

Start free

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.