June 30, 2026 · Tutorial

How to Automate NDA Signing: From Template to Signature in Under 60 Seconds

Most teams still send NDAs as email attachments, wait days for a scan-and-return, then manually rename and file the PDF. This tutorial walks through a fully automated workflow — from a reusable markdown template to signed document filed in cloud storage — with zero human intervention after the CRM trigger fires.

TL;DR

Automate NDA signing by wiring your CRM to a server that renders a markdown template, dispatches it via the Signbee API, listens for a webhook completion event, and files the signed PDF to S3 or GCS. The entire pipeline runs in under 60 seconds — no human touches the document between CRM trigger and signed file. Node.js and Python code below.

The NDA Signing Problem

Non-disclosure agreements are the most frequently signed business document, yet most teams still handle them manually. A sales rep copies a Word template, edits the party names, emails it as an attachment, waits for the counterparty to print-sign-scan-return (or forward to their own e-signature tool), then renames the PDF and drops it into a shared folder. Each NDA takes 15-45 minutes of human effort across multiple people — and that's before accounting for the 1-3 day turnaround lag.

An automated NDA workflow eliminates every manual step. When a deal reaches the right stage in your CRM, a webhook fires, your server generates the NDA from a template, sends it for electronic signature via API, and files the completed document — all without anyone touching a keyboard. Let's build it.

Workflow Architecture Overview

Before writing code, here's the full pipeline. Each box is an independent service — if any stage fails, the others remain unaffected, and you can retry the failed stage without re-running the entire flow:

NDA automation pipeline — end-to-end
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Deal Stage     │     │  CRM Webhook    │     │  Your Server    │
│  Change (CRM)   │────▶│  (HTTP POST)    │────▶│  (Node/Python)  │
└─────────────────┘     └─────────────────┘     └────────┬────────┘
                                                         │
                                              Template + Variables
                                                         │
                                                         ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  File to        │     │  Signed Webhook  │     │  Signbee API    │
│  S3 / GCS      │◀────│  (document.      │◀────│  POST /send     │
└─────────────────┘     │   completed)     │     └────────┬────────┘
                        └─────────────────┘              │
                                                         ▼
                                                ┌─────────────────┐
                                                │  Signer Email   │
                                                │  (signing link) │
                                                └─────────────────┘

The flow is linear: Deal Stage Change → CRM Webhook → Your Server → Signbee API → Signer Email → Signed Webhook → File to S3/GCS. Each arrow is an HTTP request. The entire chain, from CRM trigger to signed-and-filed, typically completes in under 60 seconds of compute time (signer response time is the only variable).

Step 1: The NDA Markdown Template

Start with a reusable template. We use markdown because it renders to a clean PDF without any design tooling, and variable placeholders are just strings you replace before sending. Store this in your codebase as a .md file or a template literal — either approach works. For more template options, see our free agreement form templates.

nda-template.md — reusable NDA with variable placeholders
# Mutual Non-Disclosure Agreement

**Effective Date:** {{effective_date}}

## Parties

**Disclosing Party:** {{company_a_name}}, a {{company_a_state}}
{{company_a_entity_type}}, with its principal office at
{{company_a_address}} ("Party A")

**Receiving Party:** {{company_b_name}}, a {{company_b_state}}
{{company_b_entity_type}}, with its principal office at
{{company_b_address}} ("Party B")

## 1. Definition of Confidential Information

"Confidential Information" means any non-public information
disclosed by either Party to the other, whether orally, in writing,
or by inspection, including but not limited to: technical data,
trade secrets, business plans, customer lists, financial data,
product roadmaps, source code, algorithms, and proprietary
processes.

## 2. Obligations of Receiving Party

Each Party agrees to:
(a) Hold Confidential Information in strict confidence
(b) Not disclose to any third party without prior written consent
(c) Use Confidential Information solely for {{purpose}}
(d) Protect with at least the same degree of care used for its
    own confidential information, but no less than reasonable care
(e) Limit access to employees and agents with a need to know

## 3. Exclusions

Confidential Information does not include information that:
(a) Is or becomes publicly available through no fault of Recipient
(b) Was rightfully known to Recipient prior to disclosure
(c) Is independently developed without reference to or use of
    the Confidential Information
(d) Is rightfully received from a third party without restriction
(e) Is required to be disclosed by law or court order, provided
    Recipient gives prompt written notice to Discloser

## 4. Term and Survival

This Agreement is effective for {{term_years}} year(s) from the
Effective Date. Confidentiality obligations survive for
{{survival_years}} year(s) after expiration or termination.

## 5. Return of Materials

Upon termination or written request, each Party shall promptly
return or destroy all Confidential Information and certify
destruction in writing.

## 6. Remedies

Each Party acknowledges that breach may cause irreparable harm
for which monetary damages are inadequate. The non-breaching
Party is entitled to seek injunctive relief in addition to
any other remedies available at law or in equity.

## 7. Governing Law

This Agreement shall be governed by the laws of
{{governing_law_jurisdiction}}, without regard to conflict
of law principles.

## 8. Entire Agreement

This Agreement constitutes the entire understanding between
the Parties regarding confidentiality and supersedes all prior
agreements, whether written or oral.

By signing below, each Party acknowledges that it has read,
understood, and agrees to be bound by the terms of this
Agreement.

Every {{variable}} placeholder maps to a field in your CRM or deal record. When a deal triggers the workflow, your server pulls these values and performs a straightforward string replacement before dispatching the rendered document.

Step 2: Node.js Implementation

This Express handler receives a CRM webhook, renders the NDA template, sends it via the Signbee API, and returns the document ID. The template is loaded from disk at startup, so each request only performs string replacement and one outbound API call:

Node.js — CRM webhook → template render → API dispatch
import express from "express";
import fs from "fs/promises";
import crypto from "crypto";

const app = express();
app.use(express.json());

// Load template once at startup
const NDA_TEMPLATE = await fs.readFile("./nda-template.md", "utf-8");
const SIGNBEE_API_KEY = process.env.SIGNBEE_API_KEY;

function renderTemplate(template, variables) {
  return Object.entries(variables).reduce(
    (doc, [key, value]) => doc.replaceAll(`{{${key}}}`, value),
    template
  );
}

// CRM webhook endpoint — fires when deal reaches "NDA Required" stage
app.post("/webhooks/crm/deal-stage-change", async (req, res) => {
  const { deal, contact } = req.body;

  // Only trigger on the NDA stage
  if (deal.stage !== "nda_required") {
    return res.json({ skipped: true });
  }

  // Render the NDA with CRM data
  const nda = renderTemplate(NDA_TEMPLATE, {
    effective_date: new Date().toISOString().split("T")[0],
    company_a_name: "Your Company Inc",
    company_a_state: "Delaware",
    company_a_entity_type: "Corporation",
    company_a_address: "100 Main St, Wilmington, DE 19801",
    company_b_name: contact.company,
    company_b_state: contact.state || "N/A",
    company_b_entity_type: contact.entity_type || "Company",
    company_b_address: contact.address,
    purpose: deal.description || "Evaluating a potential business relationship",
    term_years: "2",
    survival_years: "3",
    governing_law_jurisdiction: "the State of Delaware",
  });

  // Send via Signbee API
  const response = await fetch("https://signb.ee/api/v1/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${SIGNBEE_API_KEY}`,
    },
    body: JSON.stringify({
      markdown: nda,
      recipient_name: contact.name,
      recipient_email: contact.email,
      title: `NDA — ${contact.company}`,
      expires_in_days: 7,
      metadata: {
        deal_id: deal.id,
        crm_source: "hubspot",
      },
    }),
  });

  const { document_id, signing_url } = await response.json();

  console.log(`NDA dispatched: ${document_id} → ${contact.email}`);
  res.json({ document_id, signing_url });
});

// Signbee webhook — fires when signer completes the NDA
app.post("/webhooks/signbee/completed", async (req, res) => {
  const { event, document_id, signed_pdf_url, metadata } = req.body;

  if (event !== "document.completed") {
    return res.json({ ignored: true });
  }

  // Download the signed PDF
  const pdfResponse = await fetch(signed_pdf_url);
  const pdfBuffer = Buffer.from(await pdfResponse.arrayBuffer());

  // Upload to S3 (using AWS SDK v3)
  const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
  const s3 = new S3Client({ region: "us-east-1" });

  const key = `signed-ndas/${metadata.deal_id}/${document_id}.pdf`;
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: pdfBuffer,
    ContentType: "application/pdf",
    ServerSideEncryption: "AES256",
  }));

  console.log(`Signed NDA filed: s3://${process.env.S3_BUCKET}/${key}`);
  res.json({ filed: true, s3_key: key });
});

app.listen(3000, () => console.log("NDA automation server running on :3000"));

Two endpoints, zero manual steps. The CRM webhook handler renders and dispatches. The Signbee webhook handler downloads and files. Between them, the NDA flows from template to signed-and-archived without anyone opening an email client.

Step 3: Python Implementation (FastAPI)

The same workflow in Python using FastAPI and httpx. This version adds Pydantic models for type-safe webhook payloads and uses google-cloud-storage for filing to GCS instead of S3:

Python — FastAPI webhook handler with GCS filing
import os
import httpx
from datetime import date
from pathlib import Path
from fastapi import FastAPI, Request
from pydantic import BaseModel
from google.cloud import storage

app = FastAPI()

NDA_TEMPLATE = Path("nda-template.md").read_text()
SIGNBEE_API_KEY = os.environ["SIGNBEE_API_KEY"]
GCS_BUCKET = os.environ["GCS_BUCKET"]


class Contact(BaseModel):
    name: str
    email: str
    company: str
    address: str
    state: str = "N/A"
    entity_type: str = "Company"


class Deal(BaseModel):
    id: str
    stage: str
    description: str = "Evaluating a potential business relationship"


class CRMWebhook(BaseModel):
    deal: Deal
    contact: Contact


def render_template(template: str, variables: dict) -> str:
    result = template
    for key, value in variables.items():
        result = result.replace(f"{{{{{key}}}}}", value)
    return result


@app.post("/webhooks/crm/deal-stage-change")
async def handle_crm_webhook(payload: CRMWebhook):
    if payload.deal.stage != "nda_required":
        return {"skipped": True}

    nda = render_template(NDA_TEMPLATE, {
        "effective_date": date.today().isoformat(),
        "company_a_name": "Your Company Inc",
        "company_a_state": "Delaware",
        "company_a_entity_type": "Corporation",
        "company_a_address": "100 Main St, Wilmington, DE 19801",
        "company_b_name": payload.contact.company,
        "company_b_state": payload.contact.state,
        "company_b_entity_type": payload.contact.entity_type,
        "company_b_address": payload.contact.address,
        "purpose": payload.deal.description,
        "term_years": "2",
        "survival_years": "3",
        "governing_law_jurisdiction": "the State of Delaware",
    })

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://signb.ee/api/v1/send",
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {SIGNBEE_API_KEY}",
            },
            json={
                "markdown": nda,
                "recipient_name": payload.contact.name,
                "recipient_email": payload.contact.email,
                "title": f"NDA — {payload.contact.company}",
                "expires_in_days": 7,
                "metadata": {
                    "deal_id": payload.deal.id,
                    "crm_source": "hubspot",
                },
            },
        )

    data = response.json()
    return {"document_id": data["document_id"], "signing_url": data["signing_url"]}


@app.post("/webhooks/signbee/completed")
async def handle_signed_webhook(request: Request):
    body = await request.json()

    if body.get("event") != "document.completed":
        return {"ignored": True}

    document_id = body["document_id"]
    signed_pdf_url = body["signed_pdf_url"]
    deal_id = body.get("metadata", {}).get("deal_id", "unknown")

    # Download signed PDF
    async with httpx.AsyncClient() as client:
        pdf_response = await client.get(signed_pdf_url)

    # Upload to Google Cloud Storage
    gcs = storage.Client()
    bucket = gcs.bucket(GCS_BUCKET)
    blob_path = f"signed-ndas/{deal_id}/{document_id}.pdf"
    blob = bucket.blob(blob_path)
    blob.upload_from_string(pdf_response.content, content_type="application/pdf")

    return {"filed": True, "gcs_path": f"gs://{GCS_BUCKET}/{blob_path}"}

CRM Trigger Patterns

The automation only works if the CRM webhook fires at the right moment. Here are the three most common trigger patterns and when to use each:

Pattern 1: Deal stage change. The most common trigger. When a deal moves to a specific pipeline stage (e.g., "Qualified" or "Contract Sent"), the CRM fires a webhook to your server. This works well in HubSpot (via Workflows), Salesforce (via Process Builder or Flow), and Pipedrive (via Automations). The deal record contains the contact's company, email, and address — everything your NDA template needs.

Pattern 2: New contact created with specific properties. Some teams send NDAs immediately when a new vendor or partner contact is created with a "Relationship Type = Vendor" property. This is useful for procurement workflows where every new vendor relationship starts with an NDA, regardless of deal pipeline position.

Pattern 3: Manual trigger via internal tool. Not every NDA can be fully automated. For edge cases — custom NDA terms, unusual jurisdiction, non-standard confidentiality scope — your internal tool can present a form that pre-fills template variables from the CRM record and lets the user review before clicking "Send." This is a hybrid approach: the template rendering and API dispatch are still automated, but a human approves the final document.

Webhook Completion and Auto-Filing

When the signer opens the email, clicks the signing link, draws or types their signature, and confirms — Signbee fires a document.completed webhook event to your registered endpoint. The payload includes the signed_pdf_url (a time-limited CDN link to the completed, tamper-proof PDF), the original metadata you attached at send time, and a full audit trail with timestamps, IP addresses, and the document's SHA-256 hash.

Your webhook handler should: (1) verify the webhook signature to confirm it originated from Signbee, (2) download the signed PDF before the CDN link expires (24 hours), (3) upload it to your permanent storage (S3, GCS, or Azure Blob), and (4) update the CRM deal record with the storage URL and completion timestamp. Both code examples above implement steps 2 and 3 — adding CRM updates is a single API call to your CRM's REST endpoint.

For a deeper dive into handling webhook events — including retry logic, idempotency, and failure recovery — see the full webhook events guide.

Error Handling and Retry Strategy

Production NDA automation needs resilience at every stage. Here's what can fail and how to handle it:

CRM webhook delivery failure. Most CRMs retry webhook delivery 3-5 times with exponential backoff. Make your endpoint idempotent — store the deal.id in a deduplication set and skip re-processing if you've already dispatched an NDA for that deal. A Redis set with a 24-hour TTL works well for this.

Signbee API error. If the /send endpoint returns a 4xx or 5xx error, log the payload and queue for retry. Rate limit errors (429) include a Retry-After header — respect it. Validation errors (400) indicate a problem with your template variables (missing required field, invalid email format) and should surface to your monitoring dashboard rather than retry blindly.

Signed PDF download failure. The CDN URL in the document.completed webhook is valid for 24 hours. If your handler fails to download it, you can retry anytime within that window. After expiration, use the GET /api/v1/documents/{document_id}/pdf endpoint to request a fresh download link.

Performance: What "Under 60 Seconds" Means

CRM webhook delivery<500ms
Template rendering (string replace)<10ms
Signbee API dispatch<2s
Email delivery to signer<30s
Total: trigger to signer inbox<35s
Signer response time (variable)5–30 min
Webhook + filing after signature<5s
Traditional email-PDF workflow1–3 days

The "under 60 seconds" refers to compute time — the total machine time from CRM trigger to signed-and-filed, excluding the human signer's response time. The signer typically completes the document in 5-30 minutes. Compare this to 1-3 days for the traditional email-attachment-scan-rename workflow, and you're looking at a 99%+ reduction in total turnaround.

Security Considerations

Webhook signature verification. Always verify that incoming webhooks originate from the expected source. Signbee webhooks include an X-Signbee-Signature header — an HMAC-SHA256 hash of the request body using your webhook secret. Verify this before processing any payload.

API key storage. Never hardcode API keys. Use environment variables or a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault). Rotate keys quarterly and use separate keys for development and production environments.

Signed document storage. Enable server-side encryption on your S3 bucket or GCS bucket. Use IAM policies to restrict access to your application's service account only. Enable versioning to prevent accidental deletion or overwrite of signed documents.

Frequently Asked Questions

How do I trigger an NDA automatically when a deal reaches a specific CRM stage?

Most modern CRMs — HubSpot, Salesforce, Pipedrive — support webhook-based workflow automations that fire when a deal moves to a specific pipeline stage. You configure a workflow rule (e.g., "When Deal Stage = Contract Sent") that sends an HTTP POST to your server with the deal payload including contact name, email, company, and deal metadata. Your server receives this webhook, extracts the relevant fields, renders the NDA markdown template with those variables, and calls the Signbee API to dispatch the document for signature. The entire trigger-to-send chain executes in under two seconds with no human intervention. For CRMs without native webhooks, tools like Zapier or Make can bridge the gap by listening for stage changes and forwarding the payload to your endpoint.

What happens if the signer doesn't complete the NDA — can I set up automatic reminders?

Yes. The Signbee API supports automatic reminder emails at configurable intervals. When you send the initial API request, you can include a reminder schedule — for example, reminders at 24 hours, 72 hours, and 7 days after dispatch. If the signer hasn't completed the document by each interval, they receive a follow-up email with the same signing link. You can also listen for the document.reminder_sent webhook event to track reminder delivery in your own system. If the document expires (controlled via the expires_in_days parameter), the document.expired webhook fires, letting your server trigger a re-send or notify your sales team.

Is it safe to store signed NDAs in S3 or Google Cloud Storage automatically?

Yes, storing signed NDAs in cloud object storage like AWS S3 or Google Cloud Storage is both safe and recommended — provided you configure proper access controls. Use server-side encryption (AES-256 or AWS KMS) on the bucket, restrict access via IAM policies to only your application's service account, and enable versioning so no signed document can be accidentally overwritten or deleted. When your webhook handler receives the document.completed event, it downloads the signed PDF from the Signbee CDN URL (valid for 24 hours), uploads it to your storage bucket with a structured key path, and logs the storage location back to your CRM. This gives you a tamper-proof, auditable archive that satisfies most regulatory retention requirements.

Start automating NDAs for free — 5 documents/month, no credit card required.

Last updated: July 4, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.

Related resources