Drift (now Salesloft Conversations) is a conversational marketing platform: chatbots, live chat, video, and Sales meetings. Companies use Drift to capture leads from their website and qualify them through conversation. HubSpot has a native integration with Drift via the HubSpot Marketplace. The problem: the native integration creates a Contact in HubSpot for every new Drift conversation, but it does not create or update a Deal. The conversation history (transcript, meeting booked, playbook triggered) never makes it into the Deal Timeline.
Sales ops can’t see the Drift conversation context inside the deal. When an AE picks up a lead from the Drift bot, the chat history and qualification results are stored in Drift - not in the HubSpot Deal. A custom integration via the Drift Conversation API + HubSpot Deals API closes this gap.
Drift Playbook is a chatbot script: a set of questions and branching logic for lead qualification. The Playbook outcome (email, company, budget, timeline) is valuable CRM data that the native integration never passes into the Deal.
What the Native Integration Does
The native HubSpot + Drift integration can:
- Create or update a Contact in HubSpot when a conversation starts
- Sync email and basic contact fields
The native integration cannot:
- Create a Deal when a conversation starts or ends
- Add the conversation transcript to the Deal Timeline
- Pass Playbook answers as Deal Properties
- Update the Deal Stage when a meeting is booked through Drift
- Log a Drift meeting to a Deal as an activity
The Right Architecture
Website visitor -> Drift chat -> Qualification Playbook
-> Collects: email, company, budget, timeline, intent
Drift webhook: conversation.status_updated (status = closed/won)
-> Your server
Your server
-> Drift API: GET /v2/conversations/{id} (full transcript)
-> HubSpot Contacts API: find/create Contact by email
-> HubSpot Deals API: create Deal with:
- Name: "Drift: {company} - {date}"
- Properties: drift_conversation_id, budget, timeline, playbook_outcome
-> HubSpot Engagements API: add Note with transcript to Deal Timeline
-> HubSpot Associations: link Deal -> Contact
Implementation: Drift Webhook -> HubSpot Deal
import requests, os, hmac, hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
DRIFT_TOKEN = os.environ["DRIFT_API_TOKEN"]
DRIFT_WEBHOOK_SEC = os.environ["DRIFT_WEBHOOK_SECRET"]
HS_TOKEN = os.environ["HUBSPOT_PRIVATE_APP_TOKEN"]
DRIFT_BASE = "https://driftapi.com"
DRIFT_HDR = {"Authorization": f"Bearer {DRIFT_TOKEN}", "Content-Type": "application/json"}
HS_BASE = "https://api.hubapi.com"
HS_HDR = {"Authorization": f"Bearer {HS_TOKEN}", "Content-Type": "application/json"}
HS_DEAL_STAGE_NEW = os.environ.get("HS_DEAL_STAGE_ID", "appointmentscheduled")
def verify_drift_signature(body: bytes, sig_header: str) -> bool:
# Drift HMAC-SHA256 hex
computed = hmac.new(DRIFT_WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, sig_header.replace("sha256=", ""))
def get_drift_conversation(conv_id: int) -> dict:
r = requests.get(f"{DRIFT_BASE}/v2/conversations/{conv_id}/messages", headers=DRIFT_HDR)
if r.status_code == 200:
return r.json().get("data", {})
return {}
def get_drift_playbook_data(conv_id: int) -> dict:
r = requests.get(f"{DRIFT_BASE}/v2/conversations/{conv_id}", headers=DRIFT_HDR)
if r.status_code != 200:
return {}
conv = r.json().get("data", {})
return conv.get("attributes", {})
def build_transcript(messages: list) -> str:
lines = []
for msg in messages:
author = msg.get("author", {})
name = author.get("name", "Bot") if author.get("type") == "agent" else "Visitor"
body = msg.get("body", "")
if body:
lines.append(f"{name}: {body}")
return "
".join(lines)
def find_or_create_hs_contact(email: str, name: str) -> str:
r = requests.get(
f"{HS_BASE}/crm/v3/objects/contacts/search",
headers=HS_HDR,
json={"filterGroups": [{"filters": [{"propertyName": "email", "operator": "EQ", "value": email}]}]},
)
if r.status_code == 200:
results = r.json().get("results", [])
if results:
return results[0]["id"]
parts = name.split(" ", 1)
r_create = requests.post(
f"{HS_BASE}/crm/v3/objects/contacts",
headers=HS_HDR,
json={"properties": {
"email": email,
"firstname": parts[0],
"lastname": parts[1] if len(parts) > 1 else "",
}},
)
return r_create.json().get("id", "")
def create_hs_deal(contact_id: str, conv_id: int, attrs: dict) -> str:
company = attrs.get("company", "")
budget = attrs.get("budget", "")
timeline = attrs.get("timeline", "")
import time
now_ms = int(time.time() * 1000)
deal_name = f"Drift: {company} - {__import__('datetime').date.today().isoformat()}"
r = requests.post(
f"{HS_BASE}/crm/v3/objects/deals",
headers=HS_HDR,
json={
"properties": {
"dealname": deal_name,
"dealstage": HS_DEAL_STAGE_NEW,
"drift_conversation_id": str(conv_id),
"budget_range": budget,
"timeline": timeline,
"lead_source": "Drift Chat",
},
"associations": [{
"to": {"id": contact_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3}]
}]
},
)
r.raise_for_status()
return r.json().get("id", "")
def add_note_to_deal(deal_id: str, transcript: str, conv_id: int):
import time
note_body = f"Drift conversation #{conv_id}:
{transcript[:4000]}"
requests.post(
f"{HS_BASE}/crm/v3/objects/notes",
headers=HS_HDR,
json={
"properties": {
"hs_note_body": note_body,
"hs_timestamp": str(int(time.time() * 1000)),
},
"associations": [{
"to": {"id": deal_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214}]
}]
},
)
@app.route("/webhooks/drift", methods=["POST"])
def drift_webhook():
sig = request.headers.get("X-Drift-Signature", "")
if DRIFT_WEBHOOK_SECRET and not verify_drift_signature(request.data, sig):
return jsonify({"error": "invalid signature"}), 401
event = request.json or {}
# Drift sends an array of events
for ev in (event if isinstance(event, list) else [event]):
ev_type = ev.get("type", "")
if ev_type != "conversation_status_updated":
continue
data = ev.get("data", {})
conv_id = data.get("conversationId")
new_status = data.get("statusId", "")
if new_status not in ("closed", "won"):
continue
attrs = get_drift_playbook_data(conv_id)
email = attrs.get("email", "")
name = attrs.get("name", "Unknown")
if not email:
continue
msg_data = get_drift_conversation(conv_id)
messages = msg_data.get("messages", []) if isinstance(msg_data, dict) else []
transcript = build_transcript(messages)
contact_id = find_or_create_hs_contact(email, name)
deal_id = create_hs_deal(contact_id, conv_id, attrs)
if transcript:
add_note_to_deal(deal_id, transcript, conv_id)
return jsonify({"status": "ok"}), 200
Configuring Drift Webhooks
- Drift Dashboard -> App Settings -> Webhooks -> Add Endpoint
- URL:
https://your-server.com/webhooks/drift - Events:
conversation_status_updated - Copy the Signing Secret (used for
X-Drift-Signature)
Important: Drift (now part of Salesloft) - webhook functionality may differ depending on your pricing plan and region. Check the documentation for your version of Drift/Salesloft Conversations.
Playbook Data as HubSpot Deal Properties
Create custom properties in HubSpot to store Drift Playbook answers:
drift_conversation_id(Text)budget_range(Text or Dropdown)timeline(Text or Dropdown)playbook_outcome(Text): qualified / not-qualified / meeting_booked
These fields are populated automatically when a Deal is created via webhook.
Real-World Case
A SaaS company with 300 Drift conversations per month, 40% qualified. Before the custom integration: the AE opened Drift, found the conversation history, and manually copied the data into a HubSpot Deal (5-7 min per contact). After: the Deal is created automatically with the transcript and Playbook answers. The AE sees full context in HubSpot without switching to Drift.
Who This Is For
B2B SaaS and service companies using Drift to capture inbound leads from their website and qualify them via chatbot. Especially relevant when Drift conversation volume exceeds 50/month and every qualified lead is handled by an AE through HubSpot.
A similar anti-pattern is described for HubSpot + Zoom native integration and HubSpot + DocuSign.
Frequently Asked Questions
Drift was rebranded as Salesloft Conversations - has the API changed?
After Salesloft’s acquisition in 2023, the Drift rebrand was completed in 2024. The driftapi.com API endpoint continues to work. New Salesloft customers may access Conversations through the Salesloft API. Check the current documentation for your account - the endpoint may differ.
How do I link a Drift meeting (Calendly inside Drift) to a HubSpot Deal?
Drift allows embedding calendars (including Drift Meetings) inside the chat. The meeting_booked webhook contains the contact email and meeting details. On this event - create a Task in the HubSpot Deal (/crm/v3/objects/tasks) with the meeting details and update the Deal Stage to appointmentscheduled.
The native integration creates duplicate contacts - how do I prevent that?
Our custom implementation searches for a Contact by email (/crm/v3/objects/contacts/search) before creating one. If the contact already exists, its ID is reused. The native Drift integration creates a contact without checking for duplicates.
Summary
The native HubSpot + Drift integration creates a Contact, but not a Deal, and does not link the transcript to the deal. The custom integration:
- Drift webhook
conversation_status_updated(status = closed/won) - Drift Conversation API: retrieve transcript + Playbook answers
- HubSpot: find or create Contact -> create Deal -> add Note with transcript
associationTypeId: 3for Deal -> Contact,214for Note -> Deal- Custom Deal properties:
drift_conversation_id,budget_range,timeline
If the native Drift integration is losing conversation context in HubSpot - contact Exceltic.dev.