---
title: "Recipe: Contracts to CRM, Delivered While Still Hot"
description: Automate syncing executed documents from PandaDoc to HubSpot and Google Drive effortlessly with a Cloud Function integration. Here’s how to set it up.
---

<https://tomdelaney.us/blog>

# [Recipe: Contracts to CRM, Delivered While Still Hot](https://tomdelaney.us/blog/recipe-contracts-delivered-while-still-hot)

 Written by [Thomas Delaney](https://tomdelaney.us/blog/author/thomas-delaney) | Mar 5, 2026 4:29:31 PM

 

# 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](https://github.com/Suixcity/pandadoc-sync)**

*Part of a professional portfolio — [view the project brief](https://tomdelaney.us/executed-documents-automatically-portfolio)*

[View full post](https://tomdelaney.us/blog/recipe-contracts-delivered-while-still-hot)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Thomas Delaney"
  },
  "dateModified" : "2026-03-05T16:37:01.434Z",
  "datePublished" : "2026-03-05T16:29:31Z",
  "headline" : "Recipe: Contracts to CRM, Delivered While Still Hot",
  "image" : {
    "@type" : "ImageObject",
    "height" : 768,
    "url" : "https://24279033.fs1.hubspotusercontent-na1.net/hubfs/24279033/Gemini_Generated_Image_lhgafjlhgafjlhga.png",
    "width" : 1376
  },
  "mainEntityOfPage" : "https://tomdelaney.us/blog/recipe-contracts-delivered-while-still-hot",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60,
      "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
      "width" : 60
    },
    "name" : "blog"
  }
}
```