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.
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. |
Accounts & Access
Environment
gcloud CLI authenticated to your GCP projectFrom the pantry (provided in this repo)
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.
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
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.
For each workspace, go to PandaDoc Admin → Webhooks → Add Endpoint:
https://YOUR_FUNCTION_URL/{workspace_name}document_state_changed, document_completed_pdf_readySend 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.
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"auto- (e.g. tag auto-NDA → folder name NDA). Add auto-* tags to your PandaDoc templates to control filinglinked_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/pandadoc_sync/{Company}/{Document Type}/{Status}/PandaDoc/{Workspace}/{Company}/{Document Type}/{Status}/[Status] Document Name.pdf — the status prefix (e.g. [Sent], [Completed]) makes document state visible in the folder without opening the fileORG_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 pathOnce 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.
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.
Complete source code is in the GitHub repository:
github.com/Suixcity/pandadoc-sync
Part of a professional portfolio — view the project brief