CRM & sales
AI Agents and HubSpot
Every call the agent handles becomes a HubSpot contact with the transcript attached and a task for whoever owns the follow-up.
Where this stands today
There is no bitpull app in the HubSpot marketplace, and the product does not claim one. Native direct integrations are described as in preparation. What exists today is the outbound webhook: when a conversation ends, bitpull posts the conversation data - transcript, summary, outcome, sentiment, channel and timestamps - to one HTTPS URL you control.
That single hop is enough for HubSpot, because HubSpot has a genuinely good CRM API. Sixty lines in a serverless function will do search-or-create on the contact, attach the transcript as an engagement, and open a task. No middleware bill, no per-task limit.
The webhook is capability-gated: the Actions tab appears in the bitpull dashboard once the flag is enabled for your account. If you cannot see it, that is what to ask for.
What the agent can do
Contact upsert
Search by email or phone, create if missing, update if found. Phone numbers arrive in the SIP fields and need normalising to E.164 before they will match anything already in HubSpot.
Transcript as a note
The full turn-by-turn transcript belongs on the timeline, not in a custom property. HubSpot notes accept HTML, so the conversation stays readable.
Task on unresolved outcomes
The conversation summary carries an outcome and a follow-up flag. Creating a task only when follow-up is genuinely required is what keeps sales from muting the integration in week two.
Deal association
If the caller matches an open deal, associate the note with it. Sales people look at deals, not at contact timelines.
How it is wired
- Conversation endsphone, web or chat
- bitpull webhookPOST to your URL
- Your functionnormalise + map
- HubSpot CRM APIcontact, note, task
Code
const HS = 'https://api.hubapi.com'
const auth = { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`, 'Content-Type': 'application/json' }
export default async function handler(req, res) {
// 1. Answer immediately. A webhook sender should never wait for HubSpot.
res.status(200).end()
const body = req.body ?? {}
const summary = body.summary ?? body.session?.summary ?? {}
const messages = body.messages ?? body.transcript ?? []
const phone = e164(body.sipFrom ?? body.from ?? body.caller)
const email = body.email ?? summary.email ?? null
if (!phone && !email) return
// 2. Find or create the contact.
const search = await fetch(`${HS}/crm/v3/objects/contacts/search`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
filterGroups: [{ filters: [email
? { propertyName: 'email', operator: 'EQ', value: email }
: { propertyName: 'phone', operator: 'EQ', value: phone }] }],
limit: 1
})
}).then((r) => r.json())
const contactId = search.results?.[0]?.id ?? await fetch(`${HS}/crm/v3/objects/contacts`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ properties: { ...(email && { email }), ...(phone && { phone }) } })
}).then((r) => r.json()).then((c) => c.id)
// 3. Transcript on the timeline.
const html = messages
.map((m) => `<p><b>${m.role === 'USER' ? 'Caller' : 'Agent'}:</b> ${escapeHtml(m.content)}</p>`)
.join('')
await fetch(`${HS}/crm/v3/objects/notes`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
properties: {
hs_timestamp: Date.now(),
hs_note_body: `<h3>${escapeHtml(summary.title ?? 'AI agent conversation')}</h3>${html}`
},
associations: [{
to: { id: contactId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 202 }]
}]
})
})
// 4. A task only when the summary actually asks for one.
if (summary.followUp?.required) {
await fetch(`${HS}/crm/v3/objects/tasks`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
properties: {
hs_timestamp: Date.now() + 3600_000,
hs_task_subject: summary.title ?? 'Follow up on AI agent call',
hs_task_body: summary.followUp.reason ?? '',
hs_task_status: 'NOT_STARTED',
hs_task_priority: summary.sentiment === 'NEGATIVE' ? 'HIGH' : 'MEDIUM'
},
associations: [{
to: { id: contactId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 204 }]
}]
})
})
}
}
/** Austrian and German callers arrive as 0043… or 0664… - HubSpot matches neither. */
function e164(raw) {
if (!raw) return null
const d = String(raw).replace(/[^\d+]/g, '')
if (d.startsWith('+')) return d
if (d.startsWith('00')) return '+' + d.slice(2)
if (d.startsWith('0')) return '+43' + d.slice(1) // set your own default country
return '+' + d
}bitpull agent → Actions → Outbound webhook
URL: https://hooks.zapier.com/hooks/catch/… (Zapier "Catch Hook")
Zap:
1. Catch Hook - run one Test delivery so Zapier learns the fields
2. Formatter - normalise the phone number to E.164
3. HubSpot: Find Contact (fallback: Create Contact)
4. HubSpot: Create Engagement - note, body = transcript
5. Filter - only continue when followUp.required is true
6. HubSpot: Create TaskLimits worth knowing before you start
- No marketplace app, no OAuth install, no field mapping UI. This is a webhook and a function you own.
- The outbound webhook is behind an account capability flag. Until it is enabled, the Actions tab is not in the dashboard at all.
- One URL per agent. Fanning out to several destinations is your function's job, not a platform feature.
- Phone numbers arrive in the SIP fields in provider-specific formats - bare E.164, a full sip: URI, or a display name wrapping one. Normalise before you search or every call creates a duplicate contact.
- Call transcripts are personal data. A HubSpot note is not a lawful basis; check what your privacy notice actually says before you push transcripts into a CRM.
Questions
Is there a bitpull app in the HubSpot marketplace?
No. Native direct integrations are described as in preparation. Today the connection is the outbound webhook plus your own function or a Zapier/Make step.
Can HubSpot data be read during the call?
Not through the webhook - it fires after the conversation ends. Reading CRM data mid-call is a custom tool, and the endpoint behind that declaration is wired up with the bitpull team.
What happens if my endpoint is down?
Assume the delivery is lost. Nothing in the product documents a retry schedule or a replay API, so log the raw body the moment it arrives and process it from your own queue.
Agents are created and operated on bitpull.ai - this site documents how to connect one. If the outbound webhook is not visible in your dashboard yet, that is the account capability flag, and support can enable it.