Sync HubSpot to Odoo in Real Time on HubSpot Free · Calibre Consulting

Send marked HubSpot contacts straight to Odoo.

Push selected HubSpot Free contacts into Odoo as soon as they change, using a HubSpot private app webhook, a short script and an Odoo automation rule.

This guide sets up a one-way, real-time feed from HubSpot to Odoo. When someone ticks a box on a HubSpot contact, the contact is sent to Odoo as soon as HubSpot reports the change, and is created there or updated if Odoo already has it. It works on HubSpot's free tier: HubSpot sends a webhook, a short script fetches the contact and passes it to an Odoo automation rule, and the rule finds or creates the contact. The Odoo steps were tested on Odoo 18 and Odoo 19 and checked in the Odoo 17 and 20 source code.

It is written for the person who owns the integration: an administrator who is comfortable pasting a few lines of code, or a developer who wants the shortest route that uses only standard Odoo.

Before you start

Why a middle step is needed

HubSpot lists Webhooks and the Contacts, Companies and Deals APIs as available on its Free tier, and a private app can subscribe to changes on a contact property. The webhook does not carry the contact, though. Each request is a list of up to 100 events, and each event holds the record ID, the property that changed and its new value:

[
  {
    "objectId": 1246965,
    "propertyName": "send_to_odoo",
    "propertyValue": "true",
    "occurredAt": 1760900000000,
    "attemptNumber": 0
  }
]

Real events carry a few more IDs, such as the subscription and the HubSpot account. There is no email or name in them, so something has to read the full contact from HubSpot first. Odoo cannot do that part: the code an automation rule runs has no HTTP client available. Odoo's rule expressions also treat the webhook body as a single object, so a list sent straight from HubSpot fails with error 500.

HubSpot does not guarantee that each event arrives once or in order. It retries a failed delivery up to 10 times over 24 hours, and it counts as failed any error response, a failed connection, or no reply within five seconds. The Odoo side therefore has to be safe to run twice, which is why the rule below finds the contact by email before it creates one.

Step 1: Mark the records in HubSpot

  1. In HubSpot, click the settings icon in the top bar, then Properties in the left sidebar, and choose contact properties.
  2. Create a property with the field type Single checkbox and a label such as Send to Odoo. Note its internal name, for example send_to_odoo, because HubSpot does not let you change it later.
  3. Go to Development, then Legacy apps in the left sidebar, and create a private app. Create it on its own, not through a project: private apps created with projects do not support webhooks.
  4. On the app's Scopes tab, add crm.objects.contacts.read. Copy the access token, and the client secret from the Auth tab.

Only contacts someone ticks reach Odoo, so a marketing import does not flood the database.

Step 2: Create the Odoo automation rule

  1. With developer mode on, open the Settings app and go to Technical > Automation > Automation Rules, then click New.
  2. Name the rule, set Model to Contact and Trigger to On webhook, and save. The rule's address now appears, labelled Secret URL in Odoo 19 and 20 and URL in Odoo 17 and 18. It looks like https://your-odoo-address/web/hook/ followed by a long random code.
  3. Replace the Target Record expression with this one, which finds the contact by email or creates it:
(model.search([('email', '=ilike', payload['email'])], limit=1)
 or model.create({'name': payload['name'], 'email': payload['email']}))
  1. Turn on Log Calls while you test.
  2. On the Actions To Do tab, add an action, set Type to Execute Code, and paste this into the Code tab. It fills in the remaining fields and keeps the current value when HubSpot sends a blank:
record.write({
    'name': payload.get('name') or record.name,
    'phone': payload.get('phone') or record.phone,
})

The Target Record expression must use model, not env. The help text under the field lists env, but in testing on Odoo 18 and 19 an expression using it failed with error 500. The default expression is meant for calls from another Odoo database.

Step 3: Run the middle step

The middle step receives HubSpot's list, checks that it came from HubSpot, reads each ticked contact and posts one object per contact to the Odoo URL, in this shape:

{"email": "ada@example.com", "name": "Ada Tester", "phone": "+1 403 555 0303"}

Here is a minimal version in Python with Flask. It checks the X-HubSpot-Signature header, which for webhook events is a SHA-256 hash of the client secret followed by the request body:

import hashlib, os
import requests
from flask import Flask, request, abort

app = Flask(__name__)
TOKEN = os.environ["HUBSPOT_TOKEN"]          # private app access token
SECRET = os.environ["HUBSPOT_CLIENT_SECRET"] # private app client secret
ODOO_URL = os.environ["ODOO_WEBHOOK_URL"]    # the automation rule's URL
CONTACT = "https://api.hubapi.com/crm/objects/2026-09/contacts/"

@app.post("/hubspot")
def hubspot():
    body = request.get_data()
    expected = hashlib.sha256(SECRET.encode() + body).hexdigest()
    if request.headers.get("X-HubSpot-Signature") != expected:
        abort(401)
    for event in request.get_json():
        if str(event.get("propertyValue")).lower() != "true":
            continue  # box was unticked
        c = requests.get(
            CONTACT + str(event["objectId"]),
            params={"properties": "firstname,lastname,email,phone"},
            headers={"Authorization": "Bearer " + TOKEN},
            timeout=10,
        ).json()["properties"]
        if not c.get("email"):
            continue  # nothing to match on in Odoo
        name = " ".join(filter(None, [c.get("firstname"), c.get("lastname")]))
        requests.post(ODOO_URL, json={
            "email": c["email"],
            "name": name or c["email"],
            "phone": c.get("phone") or "",
        }, timeout=10).raise_for_status()
    return "", 204

The contact path uses HubSpot's date-versioned API; use the latest version shown in HubSpot's Contacts API reference. If Odoo returns an error, the script returns one to HubSpot, which then retries. Automation tools such as n8n or Make can do the same work with a webhook trigger and two HTTP request steps.

Back in HubSpot, open the private app's Webhooks tab, click Edit webhooks, enter the middle step's address, and subscribe to Contacts with Property changed on the Send to Odoo property.

Check that it worked

  1. In HubSpot, tick Send to Odoo on a test contact that has an email address.
  2. In Odoo, open the Contacts app and search for that email. The contact should appear shortly. If it does not, check the logs in the last step.
  3. Change the contact's phone in HubSpot, untick the box, then tick it again. The same Odoo contact should show the new phone, and there should still be only one.
  4. In Odoo, open the rule and click Logs to see each call and its payload. HubSpot's private app also logs successful and failed deliveries.

Contacts created this way show the Public user as their creator, because the webhook runs without a login.

Limits to plan around

Steps verified on Odoo 17 to 20. Menus and labels can differ on other versions.

Common questions

Can HubSpot Free send data to Odoo in real time?

Yes. HubSpot lists Webhooks and the Contacts API as available on Free, and a private app created by a super admin can subscribe to contact property changes. A small middle step then reads the contact and posts it to an Odoo automation rule with the On webhook trigger.

Why can HubSpot not post straight to the Odoo webhook?

HubSpot sends a list of events that hold only record IDs and the changed property, while Odoo reads a webhook body as one object. Sent directly, the list fails with error 500 and there is no email or name to match on.

Which Odoo versions have the On webhook trigger?

Odoo 17, 18, 19 and 20 have it in the Automation Rules module, in both Community and Enterprise. Odoo 16 does not.

Will HubSpot create duplicate contacts in Odoo?

HubSpot can deliver the same event more than once and retries failed deliveries up to 10 times over 24 hours. A Target Record expression that searches by email before creating keeps it to one contact per email address.

Does the HubSpot workflow Send a webhook action work on Free?

No. HubSpot lists that workflow action under Data Hub Professional and Enterprise. On Free, use a private app's webhook subscriptions instead.

Talk to the team.

If you are setting this up, or Odoo is not behaving the way this guide describes, book a 30-minute call. We will look at your configuration and tell you what it needs. Calgary studio, in-house team, no offshore handoffs.

Book the call →