Recipe: Contracts to CRM, Delivered While Still Hot

 

Recipe: Contracts to CRM, Delivered While Still Hot

Every executed document — contracts, NDAs, proposals — automatically synced to HubSpot and Google Drive the moment it's sent, organized by workspace, company, and document type.


About This Recipe

PandaDoc is where documents get signed. HubSpot is where deals get worked. Google Drive is where files get stored. None of them talk to each other by default, which means someone has to manually move executed documents from one to the other — or it doesn't happen.

This recipe wires up a Google Cloud Function to receive PandaDoc webhooks and handle the full sync automatically. When a document is sent or a PDF becomes available, the function downloads it from PandaDoc, uploads it to HubSpot with the right deal associations, and files it in Google Drive in a folder structure organized by workspace, company, document type, and status. All of it in seconds, without anyone touching it.

It supports multiple PandaDoc workspaces out of the box — sales, legal, HR, operations, executive — each with their own API credentials and filing destinations, all handled by a single deployed function.


Difficulty Intermediate
Prep Time ~30 mins (credentials across 3 APIs)
Cook Time ~20 mins (deploy + webhook configuration)
Serves Any team using PandaDoc and HubSpot together
Allergens Requires PandaDoc Admin access to configure webhooks. Requires a Google Shared Drive and service account. Each PandaDoc workspace needs its own API key.

Ingredients

Accounts & Access

  • 1× PandaDoc account with Admin access — needed to create API keys and webhook subscriptions
  • 1× HubSpot Private App — files write access, notes write access, CRM read access
  • 1× Google Shared Drive with a service account as Editor
  • 1× GCP project with Cloud Functions and Secret Manager enabled

Environment

  • Python 3.11+ (local testing only)
  • gcloud CLI authenticated to your GCP project

From the pantry (provided in this repo)

  • Single Cloud Function handling all workspaces
  • HMAC-SHA256 webhook signature validation per workspace
  • Intelligent company name resolution with fallback chain
  • Automatic HubSpot folder creation, note creation, and deal/contact/company association
  • Automatic Google Drive folder hierarchy creation

Method

Mise en Place — Credentials for Three APIs

1. PandaDoc (one set per workspace)

PandaDoc Admin → Configuration → API → Create API key

Also configure a webhook subscription per workspace:

Admin → Webhooks → Add Endpoint

Set the URL to https://YOUR_FUNCTION_URL/{workspace_name} (e.g. /sales, /legal). Subscribe to document_state_changed and document_completed_pdf_ready. Copy the signing secret — you'll need it next.

2. HubSpot Private App

Settings → Integrations → Private Apps → Create

Required scopes: files (write), crm.objects.notes (write), crm.objects.contacts (read), crm.objects.companies (read), crm.objects.deals (read), crm.associations (write).

3. Google Drive service account

GCP Console → IAM → Service Accounts → Create → JSON key

Share your Shared Drive with the service account email (Editor access). Note the Shared Drive ID from the URL.


Part One: Store Secrets in GCP Secret Manager

Store each credential as a separate secret. For multi-workspace setups, suffix with the workspace name:

# Per workspace (repeat for each: OPERATIONS, HR, LEGAL, SALES, EXECUTIVE) echo -n "your-webhook-secret" | gcloud secrets create PANDADOC_WEBHOOK_SECRET_SALES \   --data-file=- --project=YOUR_PROJECT_ID  echo -n "your-api-key" | gcloud secrets create PANDADOC_API_KEY_SALES \   --data-file=- --project=YOUR_PROJECT_ID  # Shared across all workspaces echo -n "your-hubspot-token" | gcloud secrets create HUBSPOT_PRIVATE_APP_TOKEN \   --data-file=- --project=YOUR_PROJECT_ID  # Google service account key — store the full JSON as a string gcloud secrets create GOOGLE_SERVICE_ACCOUNT_KEY \   --data-file=service-account-key.json --project=YOUR_PROJECT_ID  echo -n "your-drive-id" | gcloud secrets create GOOGLE_DRIVE_SHARED_DRIVE_ID \   --data-file=- --project=YOUR_PROJECT_ID 

Part Two: Deploy the Cloud Function

gcloud functions deploy pandadoc-webhook \   --gen2 \   --runtime python311 \   --trigger-http \   --allow-unauthenticated \   --set-env-vars GCP_PROJECT=YOUR_PROJECT_ID,ENV=gcp \   --region us-central1 

Copy the function URL from the output — you'll need it to configure PandaDoc webhooks.

Chef's note: The function returns HTTP 200 even for invalid signatures. This is intentional — PandaDoc retries non-200 responses, which would cause duplicate documents in HubSpot and Drive. Returning 200 on validation failure stops the retry loop while logging the error.


Part Three: Configure PandaDoc Webhooks

For each workspace, go to PandaDoc Admin → Webhooks → Add Endpoint:

  • URL: https://YOUR_FUNCTION_URL/{workspace_name}
  • Events: document_state_changed, document_completed_pdf_ready
  • Authentication: the signing secret you stored in Secret Manager

Send a test document through each workspace. Sales documents linked to a HubSpot deal should appear in both Drive and HubSpot. Documents from other workspaces (legal, HR, etc.) that aren't deal-linked will appear in Drive only — that's expected.


Plating Notes

  • Company name resolution — the function tries in order: Client.Company token → Company.Name token → recipient with "Client" role (full email for Legal/HR, domain only for others) → external signer email/domain → "Unknown Company"
  • Document type — extracted from the first PandaDoc tag prefixed with auto- (e.g. tag auto-NDA → folder name NDA). Add auto-* tags to your PandaDoc templates to control filing
  • HubSpot sync is deal-gated — the function only syncs to HubSpot if the document has a linked HubSpot deal in PandaDoc's linked_objects. Sales documents created from a deal record will have this; legal, HR, and executive documents typically won't. No configuration needed — the routing is automatic
  • Google Drive sync runs for everything — all workspaces, all documents, regardless of HubSpot deal linkage
  • HubSpot folder path: /pandadoc_sync/{Company}/{Document Type}/{Status}/
  • Google Drive folder path: PandaDoc/{Workspace}/{Company}/{Document Type}/{Status}/
  • Filename format: [Status] Document Name.pdf — the status prefix (e.g. [Sent], [Completed]) makes document state visible in the folder without opening the file
  • HubSpot associations: the note is associated to all contacts on the deal, the primary company, and the deal itself — so it appears everywhere it's relevant in the CRM
  • Adding workspaces: add the new org key to ORG_WEBHOOK_SECRETS and ORG_API_KEYS in main.py, store its secrets in Secret Manager, and configure the PandaDoc webhook to point to the new URL path

Serving Suggestion

Once the sync is running, the most useful HubSpot workflow to layer on top is one that triggers when a new note is created with an attachment on a deal — for example, notifying the account owner that a contract has been executed, or automatically moving the deal to a "Contract Sent" stage. The note and file are standard HubSpot objects, so any existing workflow logic applies.


Shelf Life

PandaDoc's webhook payload structure is the most likely point of change — specifically the tokens, linked_objects, and recipients fields used for metadata extraction. If documents stop filing correctly, check the raw webhook payload against the field references in main.py. The HubSpot Files API and Google Drive API are both stable and versioned — changes there are less likely.


Full Recipe

Complete source code is in the GitHub repository:

github.com/Suixcity/pandadoc-sync


Part of a professional portfolio — view the project brief

People are talking about this blog post