Platforms
Connect a HubSpot portal so inbox contacts become HubSpot contacts, messages land on their timelines and closed-won deals reach your ad platforms.
Link a HubSpot portal to a profile, and the profile's inbox flows into your CRM by itself. Anyone who writes to you is created as a HubSpot contact, each message lands on that contact's timeline, and won deals can go back to your ad networks as purchases. You make no calls after connecting: each message syncs a few seconds after it arrives.
#What you can do
| Job | How |
|---|---|
| Create or match HubSpot contacts for inbox contacts | Automatic (syncContacts, on by default) |
| Log every inbox message on the contact's timeline | Automatic (logMessages, on by default) |
| Write a contact at a chosen step of a flow | hubspot_upsert_contact workflow node |
| Send closed-won deals to ad platforms as purchases | reportDeals, off by default |
| Property | Value |
|---|---|
| Platform id | hubspot |
| How it connects | OAuth, through HubSpot's own consent screen, which installs the app in the portal |
| Scopes | Always oauth, crm.objects.contacts.read and crm.objects.contacts.write; crm.objects.deals.read when you want deals |
| Portals per profile | A single one |
| Posting, analytics, inbox on the account itself | No |
#Connect a portal
Whoever connects needs a HubSpot login with permission to install apps in that portal.
curl "https://api.pozy.ai/v1/connect/hubspot?profileId=6710c3a9e4b0d2f1a8c95e21&redirect_url=https://app.example.com/connected" \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/connect/hubspot?profileId=6710c3a9e4b0d2f1a8c95e21&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// Redirect the user to data.authUrlimport os, requests
resp = requests.get(
"https://api.pozy.ai/v1/connect/hubspot",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={"profileId": "6710c3a9e4b0d2f1a8c95e21", "redirect_url": "https://app.example.com/connected"},
)
auth_url = resp.json()["authUrl"]The user picks a portal and approves the app. They return to your redirect_url with connected=hubspot, profileId, accountId and username (the portal domain). Contact sync and message logging start right away. Reconnecting the same portal refreshes the account and keeps its settings.
#Choose what syncs
curl -X PATCH https://api.pozy.ai/v1/accounts/6710c4e7e4b0d2f1a8c95e60/hubspot-settings \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "logMessages": false, "reportDeals": true }'const res = await fetch("https://api.pozy.ai/v1/accounts/6710c4e7e4b0d2f1a8c95e60/hubspot-settings", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ logMessages: false, reportDeals: true }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.account);resp = requests.patch(
"https://api.pozy.ai/v1/accounts/6710c4e7e4b0d2f1a8c95e60/hubspot-settings",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={"logMessages": False, "reportDeals": True},
)| Setting | Default | What it does |
|---|---|---|
syncContacts |
true |
Creates a HubSpot contact when an inbox contact matches none |
logMessages |
true |
Writes each inbox message on the contact's timeline |
reportDeals |
false |
Sends deals that become closed won as Purchase events |
Read the current values, plus the portal id and granted scopes, with GET /v1/accounts/{accountId}/hubspot-settings.
#How contacts are matched
For each message, Pozy looks for the HubSpot contact in this order:
- A contact with the same email address.
- A contact with the same phone number, taken from the person's WhatsApp, SMS, RCS or iMessage channel.
- With
syncContactson, a new contact with the person's name, email and phone. With it off, the message is skipped.
Once found, the link is kept. Pozy attaches to a contact that already exists without changing its name or any property. Should that contact later be deleted or merged, Pozy looks for a match again.
#How messages are logged
| Channel | Logged as |
|---|---|
| Communication (WhatsApp) | |
| SMS | Communication (SMS) |
| Any other inbox channel | Note |
Each entry names the channel and direction, then the text. Attachments are listed as links. A message is logged once, even when its sync is retried. Only conversations linked to an inbox contact sync, and only messages sent or received after you connected. Nothing is backfilled.
#Save a contact from a workflow
To push a lead to HubSpot at a chosen step, say right after a bot collects an email address, put a hubspot_upsert_contact node in the workflow:
{
"id": "save-lead",
"type": "hubspot_upsert_contact",
"config": { "email": "{{answer}}", "properties": { "lifecyclestage": "lead" } }
}The node finds the contact with the same matching as the inbox sync, then writes just the fields you filled in; empty values are ignored. Keys in properties are HubSpot's internal property names. Afterwards the flow continues on success, or on error if the workflow's profile has no HubSpot portal or HubSpot rejects the update.
#Report closed-won deals
With reportDeals on, every deal that becomes closed won is sent as a Purchase to the profile's event destinations, so Meta, Google and your other ad platforms see real revenue. See Conversions.
| Purchase field | Taken from |
|---|---|
value, currency |
The deal's amount and currency, falling back to the portal's default currency |
eventTime |
When the deal closed |
eventId |
hubspot_deal_{portalId}_{dealId}, which stops a network from counting one deal twice |
user |
Email, phone, name and address of the first contact on the deal |
Deals are checked about once an hour. Deals closed before you turned the setting on are not sent, and each deal is sent once. The portal must have granted deal access: if the deals option shows as locked on the consent screen, a HubSpot admin enables it under Settings > Integrations > Connected Apps, then you reconnect.
#Disconnect
DELETE /v1/accounts/{accountId} stops syncing and uninstalls the app from the portal, unless another profile still uses that portal. Uninstalling from HubSpot shows the account as disconnected the next time a message syncs. Contacts and timeline entries already written stay in HubSpot.
#Limits
- One portal per profile.
- Contacts and their timelines only. Companies, deals and tickets are not synced (deals are read only for purchase reporting).
- Some channels log only messages sent from the inbox, not every broadcast, sequence or workflow send.
- A busy portal that HubSpot rate limits can lag by a few minutes; Pozy waits and retries.
#Errors
| Error | Why | What to do |
|---|---|---|
hubspot_profile_has_portal on the redirect |
A different portal is already linked to this profile | Disconnect that one first, or pick another profile |
hubspot_access_denied on the redirect |
The user did not approve the app | Start again and approve it |
hubspot_invalid_state on the redirect |
The connect link was too old or had been used already | Get a new authUrl |
hubspot_connection_failed on the redirect |
HubSpot did not complete the token exchange, or Pozy could not read the portal | Try again; if it keeps failing, write to support@pozy.ai |
400 on settings |
No boolean field in the body, deal access missing for reportDeals, or not a HubSpot account |
Send at least one setting to a HubSpot account id |
404 on settings |
The account is gone or your key cannot reach it | List accounts for a current id |
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/connect/hubspot |
Get HubSpot OAuth connect URL |
GET /v1/accounts/{accountId}/hubspot-settings |
Get HubSpot account settings |
PATCH /v1/accounts/{accountId}/hubspot-settings |
Update HubSpot account settings |
GET /v1/contacts |
List contacts |
#Webhook events
| Event | When it fires |
|---|---|
contact.updated |
Contact updated. |
contact.deleted |
Contact deleted. |
account.disconnected |
Account disconnected. |
contact.tag_added |
Contact tag added. |
contact.tag_removed |
Contact tag removed. |
contact.field_changed |
Contact field changed. |