Deal Sync
Automatically create contracts when CRM deals reach the right stage
Deal Sync Tutorial
Learn how to automatically create and send PandaDoc contracts when HubSpot deals reach the contract stage, eliminating manual document creation and accelerating your sales cycle.
The Problem
When a sales deal is ready for contract, sales teams typically:
- Manually log into PandaDoc or another contract system
- Find the right template and create a new document
- Copy deal data from the CRM into the contract
- Add recipients and send for signature
- Update the CRM with the document link
- Track signing status manually
This manual process causes:
- Delayed contracts - Hours between "ready to sign" and contract sent
- Data inconsistencies - Typos, wrong amounts, outdated information
- Lost deals - Momentum dies while waiting for paperwork
- No visibility - Sales managers can't see contract status at a glance
The Solution
Build a workflow that automatically:
- Triggers when a HubSpot deal moves to "Contract Sent" stage
- Retrieves deal and contact information
- Creates a contract from a PandaDoc template with deal data
- Sends the contract for signature
- Updates the HubSpot deal with the document link
- Notifies the sales team in Slack
- (Bonus) Updates the deal when the contract is signed
What You'll Build
HubSpot Deal → Contract Sent Stage
│
▼
┌───────────────────┐
│ Get Deal Details │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Get Primary │
│ Contact │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Get Company │
│ Details │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Create PandaDoc │
│ Contract │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Send for │
│ Signature │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Update HubSpot │
│ Deal │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Notify Sales │
│ in Slack │
└───────────────────┘Prerequisites
Required Connections
| Provider | Connection Type | Purpose |
|---|---|---|
| HubSpot | OAuth via Nango | Receive webhooks, get/update deals |
| PandaDoc | OAuth via Nango | Create and send documents |
| Slack | OAuth via Nango | Team notifications |
HubSpot Setup
-
Configure Webhook Subscriptions in your HubSpot app:
- Subscribe to
deal.propertyChangeevents - Include
dealstageproperty in the subscription
- Subscribe to
-
Deal Pipeline Stages: Note your stage IDs
- Go to Settings → Objects → Deals → Pipelines
- Find the internal ID for your "Contract Sent" stage (e.g.,
contractsent)
-
Custom Properties (recommended):
contract_url(Single-line text) - To store PandaDoc linkcontract_status(Dropdown) - Draft, Sent, Viewed, Signed
PandaDoc Setup
-
Create Contract Template:
- Go to Templates → Create Template
- Design your contract with placeholder fields
- Add tokens for dynamic data (deal amount, company name, etc.)
- Note the template ID from the URL
-
Configure Tokens in your template:
{{deal_name}}- Deal/opportunity name{{deal_amount}}- Contract value{{company_name}}- Customer company{{contact_name}}- Signer name{{contact_email}}- Signer email
Step-by-Step Implementation
Step 1: Create the Workflow
Start with the trigger configuration:
{
"name": "Deal → Contract",
"description": "Create and send PandaDoc contract when deal reaches contract stage",
"trigger": {
"type": "webhook",
"provider": "hubspot",
"events": ["deal.propertyChange"],
"filter": {
"propertyName": { "$eq": "dealstage" },
"propertyValue": { "$eq": "contractsent" }
}
},
"steps": []
}Why this filter?
- HubSpot sends property change events for any deal update
- We filter to only trigger when
dealstagechanges tocontractsent - This prevents duplicate contract creation
Step 2: Get Deal Details
Retrieve the full deal information:
{
"id": "get-deal",
"type": "action",
"name": "Get Deal Details",
"config": {
"connector": "hubspot",
"action": "getDeal",
"dealId": "{{input.objectId}}",
"properties": [
"dealname",
"amount",
"closedate",
"dealstage",
"pipeline",
"hubspot_owner_id"
],
"associations": ["contacts", "companies"]
}
}Key properties:
dealname- The deal/opportunity nameamount- Contract valueclosedate- Expected close dateassociations- Returns linked contacts and companies
Step 3: Get Primary Contact
Retrieve the contact who will sign the contract:
{
"id": "get-contact",
"type": "action",
"name": "Get Primary Contact",
"config": {
"connector": "hubspot",
"action": "getContact",
"contactId": "{{steps.get-deal.output.associations.contacts.results.[0].id}}",
"properties": [
"firstname",
"lastname",
"email",
"jobtitle",
"phone"
]
}
}Output includes:
firstname,lastname- Contact nameemail- For contract deliveryjobtitle- For contract personalization
Step 4: Get Company Details
Retrieve the associated company:
{
"id": "get-company",
"type": "action",
"name": "Get Company Details",
"config": {
"connector": "hubspot",
"action": "getCompany",
"companyId": "{{steps.get-deal.output.associations.companies.results.[0].id}}",
"properties": [
"name",
"domain",
"address",
"city",
"state",
"zip",
"country"
]
}
}Step 5: Create PandaDoc Document
Create the contract from your template:
{
"id": "create-contract",
"type": "action",
"name": "Create Contract from Template",
"config": {
"connector": "pandadoc",
"action": "createDocument",
"templateId": "YOUR_TEMPLATE_ID",
"name": "Contract - {{steps.get-deal.output.properties.dealname}}",
"recipients": [
{
"email": "{{steps.get-contact.output.properties.email}}",
"first_name": "{{steps.get-contact.output.properties.firstname}}",
"last_name": "{{steps.get-contact.output.properties.lastname}}",
"role": "Signer"
}
],
"tokens": [
{
"name": "deal_name",
"value": "{{steps.get-deal.output.properties.dealname}}"
},
{
"name": "deal_amount",
"value": "{{steps.get-deal.output.properties.amount}}"
},
{
"name": "company_name",
"value": "{{steps.get-company.output.properties.name}}"
},
{
"name": "contact_name",
"value": "{{steps.get-contact.output.properties.firstname}} {{steps.get-contact.output.properties.lastname}}"
},
{
"name": "contact_title",
"value": "{{steps.get-contact.output.properties.jobtitle}}"
},
{
"name": "effective_date",
"value": "{{now | date: '%B %d, %Y'}}"
}
],
"metadata": {
"hubspot_deal_id": "{{input.objectId}}",
"hubspot_company_id": "{{steps.get-company.output.id}}"
}
}
}Key configuration:
templateId- Your PandaDoc template IDrecipients- Who needs to signtokens- Dynamic values to populate in the contractmetadata- Track the source deal
Step 6: Wait for Document Processing
PandaDoc needs time to process the document:
{
"id": "wait-for-document",
"type": "delay",
"name": "Wait for Document Processing",
"config": {
"duration": 5,
"unit": "seconds"
}
}Step 7: Send Document for Signature
Send the contract to the recipient:
{
"id": "send-contract",
"type": "action",
"name": "Send Contract for Signature",
"config": {
"connector": "pandadoc",
"action": "sendDocument",
"documentId": "{{steps.create-contract.output.id}}",
"message": "Hi {{steps.get-contact.output.properties.firstname}},\n\nPlease find attached the contract for {{steps.get-deal.output.properties.dealname}}.\n\nPlease review and sign at your earliest convenience.\n\nThank you!",
"subject": "Contract Ready: {{steps.get-deal.output.properties.dealname}}"
}
}Step 8: Create Signing Link
Generate a direct signing link:
{
"id": "create-signing-link",
"type": "action",
"name": "Create Signing Link",
"config": {
"connector": "pandadoc",
"action": "createDocumentLink",
"documentId": "{{steps.create-contract.output.id}}",
"recipientEmail": "{{steps.get-contact.output.properties.email}}",
"lifetime": 604800
}
}Parameters:
lifetime- Link validity in seconds (604800 = 7 days)
Step 9: Update HubSpot Deal
Update the deal with contract information:
{
"id": "update-deal",
"type": "action",
"name": "Update Deal with Contract Link",
"config": {
"connector": "hubspot",
"action": "updateDeal",
"dealId": "{{input.objectId}}",
"properties": {
"contract_url": "{{steps.create-signing-link.output.link}}",
"contract_status": "Sent",
"notes_last_updated": "Contract sent to {{steps.get-contact.output.properties.email}} on {{now | date: '%Y-%m-%d %H:%M'}}"
}
}
}Step 10: Notify Sales Team
Send a Slack notification:
{
"id": "notify-sales",
"type": "slack",
"name": "Notify Sales Team",
"config": {
"action": "sendMessage",
"channel": "#sales-contracts",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Contract Sent for Signature"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Deal:*\n{{steps.get-deal.output.properties.dealname}}"
},
{
"type": "mrkdwn",
"text": "*Value:*\n${{steps.get-deal.output.properties.amount}}"
},
{
"type": "mrkdwn",
"text": "*Company:*\n{{steps.get-company.output.properties.name}}"
},
{
"type": "mrkdwn",
"text": "*Signer:*\n{{steps.get-contact.output.properties.firstname}} {{steps.get-contact.output.properties.lastname}}"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Sent to: {{steps.get-contact.output.properties.email}}"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View in PandaDoc"
},
"url": "https://app.pandadoc.com/documents/{{steps.create-contract.output.id}}"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Deal"
},
"url": "https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{{input.objectId}}"
}
]
}
]
}
}Complete Workflow
Here's the complete workflow JSON:
{
"name": "Deal → Contract",
"description": "Create and send PandaDoc contract when deal reaches contract stage",
"trigger": {
"type": "webhook",
"provider": "hubspot",
"events": ["deal.propertyChange"],
"filter": {
"propertyName": { "$eq": "dealstage" },
"propertyValue": { "$eq": "contractsent" }
}
},
"steps": [
{
"id": "get-deal",
"type": "action",
"name": "Get Deal Details",
"config": {
"connector": "hubspot",
"action": "getDeal",
"dealId": "{{input.objectId}}",
"properties": ["dealname", "amount", "closedate", "dealstage", "pipeline"],
"associations": ["contacts", "companies"]
}
},
{
"id": "get-contact",
"type": "action",
"name": "Get Primary Contact",
"config": {
"connector": "hubspot",
"action": "getContact",
"contactId": "{{steps.get-deal.output.associations.contacts.results.[0].id}}",
"properties": ["firstname", "lastname", "email", "jobtitle", "phone"]
}
},
{
"id": "get-company",
"type": "action",
"name": "Get Company Details",
"config": {
"connector": "hubspot",
"action": "getCompany",
"companyId": "{{steps.get-deal.output.associations.companies.results.[0].id}}",
"properties": ["name", "domain", "address", "city", "state", "zip"]
}
},
{
"id": "create-contract",
"type": "action",
"name": "Create Contract from Template",
"config": {
"connector": "pandadoc",
"action": "createDocument",
"templateId": "YOUR_TEMPLATE_ID",
"name": "Contract - {{steps.get-deal.output.properties.dealname}}",
"recipients": [
{
"email": "{{steps.get-contact.output.properties.email}}",
"first_name": "{{steps.get-contact.output.properties.firstname}}",
"last_name": "{{steps.get-contact.output.properties.lastname}}",
"role": "Signer"
}
],
"tokens": [
{ "name": "deal_name", "value": "{{steps.get-deal.output.properties.dealname}}" },
{ "name": "deal_amount", "value": "{{steps.get-deal.output.properties.amount}}" },
{ "name": "company_name", "value": "{{steps.get-company.output.properties.name}}" },
{ "name": "contact_name", "value": "{{steps.get-contact.output.properties.firstname}} {{steps.get-contact.output.properties.lastname}}" }
],
"metadata": {
"hubspot_deal_id": "{{input.objectId}}"
}
}
},
{
"id": "wait-for-document",
"type": "delay",
"name": "Wait for Document Processing",
"config": {
"duration": 5,
"unit": "seconds"
}
},
{
"id": "send-contract",
"type": "action",
"name": "Send Contract for Signature",
"config": {
"connector": "pandadoc",
"action": "sendDocument",
"documentId": "{{steps.create-contract.output.id}}",
"message": "Please review and sign the attached contract.",
"subject": "Contract Ready: {{steps.get-deal.output.properties.dealname}}"
}
},
{
"id": "create-signing-link",
"type": "action",
"name": "Create Signing Link",
"config": {
"connector": "pandadoc",
"action": "createDocumentLink",
"documentId": "{{steps.create-contract.output.id}}",
"recipientEmail": "{{steps.get-contact.output.properties.email}}",
"lifetime": 604800
}
},
{
"id": "update-deal",
"type": "action",
"name": "Update Deal with Contract Link",
"config": {
"connector": "hubspot",
"action": "updateDeal",
"dealId": "{{input.objectId}}",
"properties": {
"contract_url": "{{steps.create-signing-link.output.link}}",
"contract_status": "Sent"
}
}
},
{
"id": "notify-sales",
"type": "slack",
"name": "Notify Sales Team",
"config": {
"action": "sendMessage",
"channel": "#sales-contracts",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "Contract Sent for Signature" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Deal:*\n{{steps.get-deal.output.properties.dealname}}" },
{ "type": "mrkdwn", "text": "*Value:*\n${{steps.get-deal.output.properties.amount}}" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View in PandaDoc" },
"url": "https://app.pandadoc.com/documents/{{steps.create-contract.output.id}}"
}
]
}
]
}
}
]
}Testing
1. Create Test Workflow
curl -X POST http://localhost:3000/api/v1/workflows \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @workflow.json2. Activate Workflow
curl -X POST http://localhost:3000/api/v1/workflows/{workflowId}/activate \
-H "x-api-key: YOUR_API_KEY"3. Test with Simulated Webhook
curl -X POST http://localhost:3000/webhooks/hubspot/{companyId} \
-H "Content-Type: application/json" \
-d '{
"objectId": 12345678,
"propertyName": "dealstage",
"propertyValue": "contractsent",
"subscriptionType": "deal.propertyChange"
}'4. Verify Results
| System | What to Verify |
|---|---|
| PandaDoc | New document created and sent |
| HubSpot | Deal updated with contract_url |
| Slack | Notification in #sales-contracts |
| Recipient Email | Contract email received |
5. End-to-End Test
- Create a test deal in HubSpot with a contact and company
- Move the deal to "Contract Sent" stage
- Watch the workflow execute
- Verify contract created in PandaDoc
- Check recipient received signing request
Bonus: Handle Contract Signed
Create a second workflow that triggers when the contract is signed:
{
"name": "Contract Signed → Close Deal",
"trigger": {
"type": "webhook",
"provider": "pandadoc",
"events": ["document_state_changed"],
"filter": {
"data.status": { "$eq": "document.completed" }
}
},
"steps": [
{
"id": "get-document",
"type": "action",
"config": {
"connector": "pandadoc",
"action": "getDocument",
"documentId": "{{input.data.id}}"
}
},
{
"id": "update-deal-closed",
"type": "action",
"config": {
"connector": "hubspot",
"action": "updateDeal",
"dealId": "{{steps.get-document.output.metadata.hubspot_deal_id}}",
"properties": {
"dealstage": "closedwon",
"contract_status": "Signed"
}
}
},
{
"id": "download-signed-contract",
"type": "action",
"config": {
"connector": "pandadoc",
"action": "downloadDocument",
"documentId": "{{input.data.id}}"
}
},
{
"id": "notify-deal-won",
"type": "slack",
"config": {
"action": "sendMessage",
"channel": "#sales-wins",
"text": ":tada: Contract signed! Deal {{steps.get-document.output.name}} is now Closed Won!"
}
}
]
}Error Handling
Handle Missing Contact
{
"id": "check-contact-exists",
"type": "condition",
"config": {
"if": "{{steps.get-deal.output.associations.contacts.results.length > 0}}",
"then": "get-contact",
"else": "notify-missing-contact"
}
}Handle PandaDoc Errors
{
"id": "create-contract",
"type": "action",
"config": { ... },
"onError": {
"action": "goto",
"step": "notify-contract-error"
}
}Notify on Failure
{
"id": "notify-contract-error",
"type": "slack",
"config": {
"action": "sendMessage",
"channel": "#sales-alerts",
"text": ":warning: Failed to create contract for deal {{steps.get-deal.output.properties.dealname}}. Error: {{error.message}}"
}
}Variations and Extensions
Multiple Signers
Add a counter-signer from your company:
{
"recipients": [
{
"email": "{{steps.get-contact.output.properties.email}}",
"first_name": "{{steps.get-contact.output.properties.firstname}}",
"last_name": "{{steps.get-contact.output.properties.lastname}}",
"role": "Customer",
"signing_order": 1
},
{
"email": "sales@yourcompany.com",
"first_name": "Sales",
"last_name": "Team",
"role": "Company",
"signing_order": 2
}
]
}Different Templates by Deal Size
Use conditional logic to select templates:
{
"id": "select-template",
"type": "transform",
"config": {
"operation": "map",
"data": {
"templateId": "{{#if (gt steps.get-deal.output.properties.amount 100000)}}ENTERPRISE_TEMPLATE_ID{{else if (gt steps.get-deal.output.properties.amount 25000)}}STANDARD_TEMPLATE_ID{{else}}BASIC_TEMPLATE_ID{{/if}}"
}
}
}Salesforce Variant
Use Salesforce instead of HubSpot:
{
"trigger": {
"type": "webhook",
"provider": "salesforce",
"events": ["opportunity.updated"],
"filter": {
"StageName": { "$eq": "Contract" }
}
},
"steps": [
{
"id": "get-opportunity",
"type": "action",
"config": {
"connector": "salesforce",
"action": "getOpportunity",
"opportunityId": "{{input.payload.Id}}"
}
},
{
"id": "get-contact",
"type": "action",
"config": {
"connector": "salesforce",
"action": "query",
"query": "SELECT Id, Name, Email FROM Contact WHERE AccountId = '{{input.payload.AccountId}}' LIMIT 1"
}
}
]
}Add Legal Review Step
For high-value deals, add a legal review step:
{
"id": "check-legal-review",
"type": "condition",
"config": {
"if": "{{steps.get-deal.output.properties.amount > 500000}}",
"then": "request-legal-review",
"else": "send-contract"
}
},
{
"id": "request-legal-review",
"type": "slack",
"config": {
"action": "sendMessage",
"channel": "#legal-review",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Legal Review Required*\n\nDeal: {{steps.get-deal.output.properties.dealname}}\nAmount: ${{steps.get-deal.output.properties.amount}}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "Approve" },
"style": "primary",
"action_id": "approve_contract_{{input.objectId}}"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Reject" },
"style": "danger",
"action_id": "reject_contract_{{input.objectId}}"
}
]
}
]
}
}Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Document not created | Invalid template ID | Verify template ID in PandaDoc |
| Missing recipient | No contact on deal | Add validation step, require contact |
| Duplicate contracts | Webhook retries | Add idempotency check using deal ID |
| Wrong data in contract | Incorrect token names | Match template tokens exactly |
| Send failed | Document still processing | Increase delay or poll for status |