Connect CF7 to HubSpot – Contact Form 7 CRM Integration

Description

Sends each Contact Form 7 submission to HubSpot. Your forms stay as they are.

One form can create several linked records. A quote request, for example, can create a contact, their company and a deal in the pipeline and stage you choose. A support form can open a ticket. You can also send a form to an existing HubSpot form, so HubSpot workflows and page tracking treat it like a HubSpot form.

Setup

  1. Create a HubSpot service key with the scopes from the FAQ and paste it into CF7 to HubSpot Settings.
  2. Open CF7 to HubSpot Forms and pick a form.
  3. Add the HubSpot objects, map the fields, turn Sync on.

Features

  • Contact, Company, Deal and Ticket from one form, each with its own mapping, associated with each other.
  • HubSpot form target, with the HubSpot tracking cookie and the page URL.
  • Update the contact with the same email, or always create a new one.
  • Deal pipeline and stage.
  • Uploaded files attached to the records.
  • Custom HubSpot properties.
  • Consent check: a submission from an unsubscribed email is not sent.
  • Logs with what HubSpot answered and which properties it rejected. Personal data is masked.
  • Error emails. Repeats of the same error within an hour come as one summary.
  • Optional background sending: the visitor does not wait for HubSpot, failed jobs are retried up to three times. Uses Action Scheduler when available, WordPress cron otherwise.
  • UTM parameters and click ids (gclid, fbclid) to the properties you map them to.
  • Optional HubSpot custom events: form submitted, synced, failed.
  • Developer hooks, see the FAQ.

External services

This plugin sends Contact Form 7 submissions to HubSpot, a third-party service, when a form is submitted and synced. It sends the values of the mapped form fields, uploaded files that are mapped, and, when enabled, attribution values; for HubSpot form targets it also sends the visitor’s IP address, the HubSpot tracking cookie and the page the form was sent from. In the admin it reads your HubSpot properties, forms, pipelines and subscription types to fill the mapping screens.

The plugin sends requests to:

  • https://api.hubapi.com
  • https://api.hsforms.com

HubSpot Terms of Service: https://legal.hubspot.com/terms-of-service
HubSpot Privacy Policy: https://legal.hubspot.com/privacy-policy

Privacy

Attribution cookies are off by default. When you turn on “Attribution cookies” in the plugin settings, the site stores the campaign parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content) and ad click ids (gclid, fbclid) of the visitor’s landing page in first-party cookies named cf7hs_* for up to 30 days (some browsers shorten this). When a consent plugin supports the WP Consent API, the cookies are set only after marketing consent. The values are sent to HubSpot with a contact form the visitor submits. Marketing cookies may need visitor consent where the law requires it.

Screenshots

Installation

  1. Install and activate Contact Form 7 if your site does not have it yet.
  2. In WordPress go to Plugins Add New Plugin, search for “Connect CF7 to HubSpot”, then click Install Now and Activate.
  3. Go to CF7 to HubSpot Settings, paste your HubSpot service key (see the FAQ for how to create one) and click Connect.
  4. Go to CF7 to HubSpot Forms, open a form, add the HubSpot objects it should create, map the fields and turn Sync on.

WordPress Multisite: do not network-activate. Activate the plugin on each site that needs it; each site connects its own HubSpot account.

FAQ

How do I get a HubSpot access token (service key)?

HubSpot no longer lets you create new private apps (since 28 September 2026 for new accounts, and 26 October 2026 for existing ones). Use a service key instead; a token from an existing private app keeps working.

  • Log in to HubSpot as a super admin and create a service key: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/account-service-keys
  • Give it these scopes: crm.objects.contacts.read and .write, crm.objects.companies.read and .write, crm.objects.deals.read and .write, crm.objects.tickets.read and .write, crm.schemas.contacts.read, crm.schemas.companies.read, crm.schemas.deals.read, forms, files.write and communication_preferences.read. (A private app token with the older tickets and files scopes keeps working.)
  • If you use journey (custom behavioral) events (cf7_form_submitted, cf7_form_synced, cf7_form_sync_failed), also add analytics.behavioral_events.send and behavioral_events.event_definitions.read_write, create matching custom event definitions in HubSpot, and enable Journey events in the plugin settings.
  • Paste the key into CF7 to HubSpot Settings Access token and click Connect. The token is saved only after HubSpot accepts it.

Does it work with the free HubSpot CRM?

Contacts, companies, deals, tickets and HubSpot forms use HubSpot’s standard CRM and Forms APIs. Journey events use HubSpot custom events, which are not part of every HubSpot plan; leave them off if your plan does not have them.

Can I send one form to several HubSpot objects?

Yes. On the form’s mapping screen click “Add an object” and pick a Company, Deal or Ticket (or another Contact). Each object has its own field mapping and can be associated with an object above it.

Will it create duplicate contacts?

A Contact object has a switch: update the contact with the same email, or always create a new one.

Can I put deals into a specific HubSpot pipeline?

Yes. Add a Deal object to the form’s mapping and choose its pipeline and deal stage.

Can I send UTM parameters and hidden fields?

Yes. Any Contact Form 7 field, hidden fields included, can be mapped to a HubSpot property. UTM parameters and ad click ids are added only to the properties you map them to; turn on “Attribution cookies” to keep them from the landing page until the form is sent.

Should I send to HubSpot objects or to a HubSpot form?

Send to a HubSpot form when you want HubSpot’s form features: form-based workflows, form submission reports and the visitor’s page history on the contact. Send to objects when you need companies, deals or tickets, or several linked records from one submission.

Where do I see why a submission did not reach HubSpot?

Open CF7 to HubSpot Logs. Click an entry to see what HubSpot answered and which properties it rejected. Personal data is masked; use “Copy details” to share the entry with support.

HubSpot shows each submission twice

HubSpot’s own tracking code can also collect Contact Form 7 submissions. In HubSpot, turn off “Collect data from website forms” under the non-HubSpot forms settings.

Do visitors wait for HubSpot when they submit a form?

Only if background processing is off. Turn it on in Settings and the form answers at once while the HubSpot requests run in the background.

How do I get help or suggest a feature?

Post in the support forum of this plugin, or write to hello@procoders.tech.

How do I change what the plugin sends to HubSpot from my own code? (developer hooks)

Since 1.4.1 the plugin has developer hooks. They are for people who write PHP; there is no setting for them in the admin.

  • cf7hs_object_payload (filter, $data, $module, $form_id, $context): change the HubSpot properties of one object just before it is written. $module is contacts, company, deals or tickets, or the HubSpot form GUID when the form is sent to a HubSpot form. Register with 4 accepted arguments to receive $context (index, action, post_id, request, async, attempt).
  • cf7hs_object_synced (action, $record_id, $module, $form_id, $outcome, $context): an object was created or updated. Register with 5 accepted arguments to receive $context. $record_id is an integer, and 0 for a HubSpot form target.
  • cf7hs_object_sync_failed (action, $module, $form_id, $outcome, $context): an object failed or was skipped. Register with 4 accepted arguments to receive $context.
  • cf7hs_submission_completed (action, $status, $form_id, $outcomes, $context): the submission reached its final outcome. $status is synced when the first object was written, otherwise failed. Register with 4 accepted arguments to receive $context. It does not fire when consent blocks the submission.
  • cf7hs_is_form( $form_id, $id_or_hash ): true when the form is the one with that post id or that shortcode hash (the 7 or more characters in [contact-form-7 id="2f24e69"]).

Example: set the lifecycle stage to customer for contacts from one form only.

add_filter( 'cf7hs_object_payload', 'my_cf7hs_customer_stage', 10, 4 );
function my_cf7hs_customer_stage( $data, $module, $form_id, $context ) {
    if ( 'contacts' === $module && cf7hs_is_form( $form_id, '2f24e69' ) ) {
        $data['lifecyclestage'] = 'customer';
    }
    return $data;
}

The value is written in the same request that creates or updates the contact, so it costs no extra API call. For a form sent to a HubSpot form, $module is the HubSpot form GUID, so test that instead of 'contacts': if ( 'your-hubspot-form-guid' === $module && cf7hs_is_form( $form_id, '2f24e69' ) ).

Lifecycle stage notes: use the internal value (customer, not “Customer”). Custom stages have numeric ids; read the options of the lifecyclestage property in HubSpot to find them. HubSpot only moves a lifecycle stage forward through the API and forms, so a contact who is already past customer stays where they are.

Things to know:

  • Keep callbacks idempotent. With asynchronous submissions the same object can reach your callback again on a retry (see $context['attempt']); cf7hs_object_sync_failed can repeat, cf7hs_object_synced effectively fires once per object. With async, uploaded files are attached after the objects are written, so cf7hs_object_synced can run before the files are attached.
  • cf7hs_submission_completed says failed when the first object was not written. Later objects may still have been written, and no retry follows then. $outcomes lists every object and is empty when the final attempt threw an error. A form whose first object has no mapped fields fires no hooks.
  • Return values of the filter: true/false are sent as true/false, other scalars as text, and '' clears the property in HubSpot. Arrays and other non-scalar values are dropped with a warning in the log. Date and datetime values are sent as epoch milliseconds in a string (for example 1760832000000), which is what HubSpot expects, so a callback that sets a date property should send the same format. A multi-checkbox is one value joined with ;. File fields and the HubSpot tracking context are not passed to the filter and cannot be changed by it.
  • If a callback throws an error it is caught and logged as a warning, and the submission goes on. For the filter, the unfiltered properties are then sent, so changes made by earlier callbacks are discarded too.
  • Warning about consent: the plugin checks consent before the filter runs. If your callback changes email or a consent property, the new value is sent without that check.
  • Arguments of these hooks are only ever added at the end; new keys may appear in $context. cf7hs_is_form with a digit-only string of 7 or more characters matches the post id first, so it can differ from how Contact Form 7 resolves the same value.

Other filters

cf7hs_log_ttl_days sets how many days log entries are kept (30 by default). cf7hs_error_email_throttle_seconds sets the window in which repeated error emails are counted instead of sent (one hour by default).

Where is the source of the compiled admin CSS?

The admin stylesheets in src/Admin/Assets/build/ are compiled from the SCSS in src/Admin/Assets/scss/, which ships with the plugin. They are built with Dart Sass 1.105.1 and @wordpress/base-styles 13.3.0:
sass –no-source-map –style=compressed –load-path=node_modules src/Admin/Assets/scss:src/Admin/Assets/build

Reviews

June 25, 2026
I just wanted to integrate my simple CF7 form with my simple HubSpot. Is that too much to ask? But it is way too complicated to wrestle with the HS API. So this plugin is a lifesaver. Other WP HS plugins force a BOATLOAD of other, unrelated code on you. And worse, they come with frustrating limits, like not allowing the user of "custom fields". This plugin is simple, but fully powerful – and it gets the job done. Also, to their credit, I found a small, cosmetic bug in the code, notified the devs, and they fixed it immediately.Thank you Alex Khryplyvyi @ProCoders
November 20, 2024
I love the simplicity of this plugin. Following the instructions for the integration was straightforward and with a few tweaks on the HubSpot Contacts table columns, I am able to see the captured form fields.
Read all 3 reviews

Contributors & Developers

“Connect CF7 to HubSpot – Contact Form 7 CRM Integration” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

1.4.2

  • Readme rewritten: shorter description, setup steps, installation from the Plugins screen, and new FAQ answers (free HubSpot CRM, duplicate contacts, UTM and hidden fields, HubSpot form or objects). The plugin code is unchanged.
  • Changelog entries before 1.4.0 moved to changelog.txt.

1.4.1

  • Developer hooks: cf7hs_object_payload filters the HubSpot properties of each object before it is written (contacts, companies, deals, tickets and HubSpot forms), cf7hs_object_synced and cf7hs_object_sync_failed fire after each object, and cf7hs_submission_completed fires once per submission with its final outcome. They behave the same with and without asynchronous submissions.
  • cf7hs_is_form( $form_id, $id_or_hash ) matches a form by post id or by the hash in its shortcode.
  • A callback that throws an error is logged as a warning instead of breaking the submission or causing an asynchronous retry.
  • Settings and the FAQ list the exact scopes a HubSpot service key needs: read and write for contacts, companies, deals and tickets, schema reads for contacts, companies and deals, forms, files.write and communication_preferences.read. A private app token with the older scopes keeps working.
  • A deal’s pipeline and stage are never sent with any other HubSpot object, even if a form’s settings were saved outside the mapping screen.
  • Fixed: a field mapped to “Attachments” was also sent to HubSpot as a property named attachment_field. On a portal without such a property HubSpot rejected the whole record, so the contact (or deal, company, ticket) was not created. The upload is now only attached to the record.

1.4.0

  • Send one form to several HubSpot objects: Contact, Company, Deal and Ticket, each with its own mapping and an association with an object above it. A HubSpot form target stays a single object.
  • Contacts: one switch chooses between updating the contact whose email already exists and always creating a new one.
  • Uploaded files are attached only to the objects whose mapping includes them, in sync and async mode.
  • Consent is checked on the CRM path too: a CRM form can choose the subscription types it enforces, and a submission is blocked when the email is unsubscribed or HubSpot cannot be asked.
  • Opt-in first-touch attribution cookies that wait for marketing consent through the WP Consent API.
  • The access token is saved only after HubSpot accepts it.
  • Logs move from debug.log to a database table with personal data masked, kept for 30 days. The Logs screen has search, result tabs, a form filter, and entries that open under their row.
  • Error emails carry a masked summary and a link to the log entry; repeats of the same kind within an hour are counted and summarised instead of mailed one by one.
  • Redesigned admin screens in the WordPress look: the forms list (status tabs, search, bulk pause and resume), the mapping screen and Logs, with layouts for phones.
  • Uninstall (when “Delete data on uninstall” is on) also removes the log table and the email throttle data.
  • Requires WordPress 5.9 or later (PHP 8.0 was already required).

Older releases: see changelog.txt in the plugin folder.