LoopFour

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:

  1. Manually log into PandaDoc or another contract system
  2. Find the right template and create a new document
  3. Copy deal data from the CRM into the contract
  4. Add recipients and send for signature
  5. Update the CRM with the document link
  6. 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:

  1. Triggers when a HubSpot deal moves to "Contract Sent" stage
  2. Retrieves deal and contact information
  3. Creates a contract from a PandaDoc template with deal data
  4. Sends the contract for signature
  5. Updates the HubSpot deal with the document link
  6. Notifies the sales team in Slack
  7. (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

ProviderConnection TypePurpose
HubSpotOAuth via NangoReceive webhooks, get/update deals
PandaDocOAuth via NangoCreate and send documents
SlackOAuth via NangoTeam notifications

HubSpot Setup

  1. Configure Webhook Subscriptions in your HubSpot app:

    • Subscribe to deal.propertyChange events
    • Include dealstage property in the subscription
  2. 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)
  3. Custom Properties (recommended):

    • contract_url (Single-line text) - To store PandaDoc link
    • contract_status (Dropdown) - Draft, Sent, Viewed, Signed

PandaDoc Setup

  1. 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
  2. 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 dealstage changes to contractsent
  • 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 name
  • amount - Contract value
  • closedate - Expected close date
  • associations - 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 name
  • email - For contract delivery
  • jobtitle - 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 ID
  • recipients - Who needs to sign
  • tokens - Dynamic values to populate in the contract
  • metadata - 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}}"
  }
}

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.json

2. 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

SystemWhat to Verify
PandaDocNew document created and sent
HubSpotDeal updated with contract_url
SlackNotification in #sales-contracts
Recipient EmailContract email received

5. End-to-End Test

  1. Create a test deal in HubSpot with a contact and company
  2. Move the deal to "Contract Sent" stage
  3. Watch the workflow execute
  4. Verify contract created in PandaDoc
  5. 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"
      }
    }
  ]
}

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

IssueCauseSolution
Document not createdInvalid template IDVerify template ID in PandaDoc
Missing recipientNo contact on dealAdd validation step, require contact
Duplicate contractsWebhook retriesAdd idempotency check using deal ID
Wrong data in contractIncorrect token namesMatch template tokens exactly
Send failedDocument still processingIncrease delay or poll for status