HubSpot имеет нативную интеграцию с DocuSign через Marketplace. После подключения отправка документов на подпись доступна прямо из HubSpot. Проблема: нативная интеграция ассоциирует конверт с Контактом (Contact), а не со Сделкой (Deal). Sales ops не видит статус подписи в pipeline. Нет автоматического перехода сделки на следующий этап при получении подписи. Нет логики “конверт отклонён - вернуть сделку на предыдущий этап”.
Это не баг - это архитектурное решение HubSpot Marketplace интеграции. DocuSign Connect (webhook-система DocuSign) умеет слать события любому получателю, но нативный коннектор HubSpot слушает только на уровне Contact. Кастомная интеграция через DocuSign Connect + HubSpot Engagements API решает этот разрыв.
DocuSign Connect - механизм push-уведомлений: при смене статуса конверта DocuSign отправляет POST-запрос на настроенный URL. Webhook включается в настройках DocuSign Admin -> Integrations -> Connect.
Что не работает в нативной интеграции
Нативная HubSpot + DocuSign интеграция умеет:
- Отправлять конверт из HubSpot CRM
- Записывать подписанный документ в Timeline контакта
Нативная интеграция НЕ умеет:
- Показывать статус конверта в карточке Сделки
- Автоматически переводить Deal в следующий stage при подписании
- Создавать задачу если конверт отклонён или истёк срок
- Логировать в Deal Timeline с правильным типом активности
Итог: sales team смотрит на воронку и не знает, подписан ли контракт по открытым сделкам. Спрашивают вручную или переключаются в DocuSign.
Правильная архитектура
HubSpot Deal: stage -> Contract Sent
-> Отправить конверт через DocuSign API
textCustomField kommo_deal_id = HubSpot Deal ID
DocuSign
-> Конверт подписан (envelope.completed)
-> POST /your-server/webhooks/docusign
{status: completed, customFields: [{name: hubspot_deal_id, value: 123}]}
Ваш сервер
-> Верифицировать X-DocuSign-Signature-1
-> HubSpot Deals API: обновить deal stage на "Closed Won"
-> HubSpot Engagements API: создать note/attachment в Deal Timeline
Реализация: отправка конверта из HubSpot сделки
import requests, os
import base64, hashlib, hmac
DS_ACCOUNT_ID = os.environ["DOCUSIGN_ACCOUNT_ID"]
DS_ACCESS_TOKEN = os.environ["DOCUSIGN_ACCESS_TOKEN"] # OAuth JWT или обычный token
DS_TEMPLATE_ID = os.environ["DOCUSIGN_TEMPLATE_ID"] # ID шаблона контракта
DS_HMAC_KEY = os.environ["DOCUSIGN_HMAC_KEY"] # Connect HMAC secret
HS_TOKEN = os.environ["HUBSPOT_PRIVATE_APP_TOKEN"]
HS_BASE = "https://api.hubapi.com"
HS_HDR = {"Authorization": f"Bearer {HS_TOKEN}", "Content-Type": "application/json"}
DS_BASE = f"https://na4.docusign.net/restapi/v2.1/accounts/{DS_ACCOUNT_ID}"
DS_HDR = {"Authorization": f"Bearer {DS_ACCESS_TOKEN}", "Content-Type": "application/json"}
def send_contract(deal_id: str, signer_email: str, signer_name: str) -> str:
payload = {
"templateId": DS_TEMPLATE_ID,
"templateRoles": [{
"email": signer_email,
"name": signer_name,
"roleName": "Client",
"tabs": {}
}],
"customFields": {
"textCustomFields": [{
"name": "hubspot_deal_id",
"value": str(deal_id),
"required": "false",
"show": "false"
}]
},
"status": "sent",
}
r = requests.post(f"{DS_BASE}/envelopes", headers=DS_HDR, json=payload)
r.raise_for_status()
envelope_id = r.json()["envelopeId"]
# Сохранить envelope_id в HubSpot Deal custom property
requests.patch(
f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
headers=HS_HDR,
json={"properties": {"docusign_envelope_id": envelope_id}},
)
return envelope_id
Реализация: DocuSign Connect -> HubSpot
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_docusign_hmac(raw_body: bytes, signature_header: str) -> bool:
# DocuSign Connect HMAC: base64(hmac-sha256(body, key))
computed = base64.b64encode(
hmac.new(DS_HMAC_KEY.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(computed, signature_header)
@app.route("/webhooks/docusign", methods=["POST"])
def docusign_webhook():
sig = request.headers.get("X-DocuSign-Signature-1", "")
if not verify_docusign_hmac(request.data, sig):
return jsonify({"error": "invalid signature"}), 401
data = request.json or {}
status = data.get("status", "")
# Извлечь hubspot_deal_id из customFields
custom_fields = data.get("customFields", {}).get("textCustomFields", [])
deal_id = None
for f in custom_fields:
if f.get("name") == "hubspot_deal_id":
deal_id = f.get("value")
break
if not deal_id:
return jsonify({"status": "no_deal_id"}), 200
if status == "completed":
handle_signed(deal_id, data)
elif status in ("declined", "voided"):
handle_declined(deal_id, status, data)
return jsonify({"status": "ok"}), 200
def handle_signed(deal_id: str, data: dict):
# Перевести сделку в "Closed Won"
requests.patch(
f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
headers=HS_HDR,
json={"properties": {
"dealstage": "closedwon",
"contract_signed_at": data.get("completedDateTime", ""),
}},
)
# Добавить Note в Deal Timeline
requests.post(
f"{HS_BASE}/crm/v3/objects/notes",
headers=HS_HDR,
json={
"properties": {
"hs_note_body": "DocuSign: контракт подписан всеми сторонами.",
"hs_timestamp": str(int(__import__("time").time() * 1000)),
},
"associations": [{
"to": {"id": deal_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214}]
}]
},
)
def handle_declined(deal_id: str, status: str, data: dict):
label = "отклонён клиентом" if status == "declined" else "аннулирован"
# Вернуть сделку на предыдущий этап и создать задачу
requests.patch(
f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
headers=HS_HDR,
json={"properties": {"dealstage": "presentationscheduled"}},
)
requests.post(
f"{HS_BASE}/crm/v3/objects/tasks",
headers=HS_HDR,
json={
"properties": {
"hs_task_body": f"DocuSign конверт {label}. Выяснить причину и переотправить.",
"hs_task_status": "NOT_STARTED",
"hs_task_type": "TODO",
"hs_timestamp": str(int(__import__("time").time() * 1000) + 86400000),
},
"associations": [{
"to": {"id": deal_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 216}]
}]
},
)
Что нужно настроить в DocuSign
- Перейти в DocuSign Admin -> Integrations -> Connect
- Создать Connect configuration с URL вашего endpoint
- Включить HMAC Security: Manage -> Add Key -> скопировать в
DS_HMAC_KEY - Выбрать события: Envelope Completed, Envelope Declined, Envelope Voided
- Include Document Fields: YES (чтобы получить customFields в payload)
Реальный кейс
SaaS-компания, 8 AE в HubSpot. Нативная DocuSign интеграция использовалась полгода. Sales ops тратил 2-3 часа еженедельно на ручную проверку статусов конвертов и обновление deal stages. Клиенты подписывали контракт, но сделка в HubSpot оставалась в “Contract Sent” до ручного обновления.
После внедрения кастомной интеграции: сделка переходит в Closed Won в течение минуты после подписания. Tasks создаются автоматически при отклонении. Sales ops перестал вручную проверять DocuSign.
Для кого актуально
Компании, использующие DocuSign для контрактов и HubSpot как CRM. Особенно если цикл контрактования занимает >3 дней и воронка содержит стадии “Contract Sent” / “Signed” / “Active”.
Похожий антипаттерн описан для HubSpot + Zoom нативной интеграции.
Часто задаваемые вопросы
Как работает авторизация DocuSign API (JWT vs OAuth)?
Для server-to-server (без участия пользователя) используйте JWT Grant с сервисным аккаунтом: подписываете JWT своим RSA private key, меняете на access token с TTL 1 час. Для разовых задач подходит Personal Access Token из DocuSign Admin. Все описанные в статье вызовы API работают с любым методом авторизации.
Могут ли textCustomFields в DocuSign быть видны подписантам?
Параметр "show": "false" скрывает поле от подписантов. Поле используется только для машинной обработки (webhook correlation). Убедитесь что required тоже false - иначе подписант увидит незаполненное обязательное поле.
Что делать если DocuSign отправил webhook но HubSpot Deal ID не найден?
Такое возможно если конверт создан не через интеграцию (например, напрямую в DocuSign). Логируйте все входящие webhooks с envelope_id. Настройте мониторинг: если webhook не нашёл deal_id - alert в Slack. Это поможет выявить конверты вне интеграции.
Итог
Нативная HubSpot + DocuSign интеграция ассоциирует конверты с Contact, не с Deal. Кастомная интеграция через DocuSign Connect + HubSpot API:
textCustomFields.hubspot_deal_idв конверте при отправке- DocuSign Connect webhook -> верификация
X-DocuSign-Signature-1 envelope.completed-> обновить Deal Stage + создать Noteenvelope.declined/voided-> вернуть Stage + создать Task- Кастомное поле
docusign_envelope_idв Deal для обратной ссылки
Если нативная интеграция тормозит ваш закрывающий процесс - обратитесь в Exceltic.dev. Реализуем кастомную связку под ваш стек HubSpot.