August 17, 2026 · Tutorial
HubSpot CRM Custom Code Workflows for E-Signatures via API (2026)
A comprehensive guide for HubSpot Operations Hub engineers: build programmable contract dispatch, dynamic deal-to-PDF generation, and automated deal stage progression using Node.js custom code actions and REST webhooks.
Founder, Signbee
TL;DR
Traditional HubSpot Marketplace e-signature add-ons charge heavy per-seat monthly subscriptions, restrict document formatting to clunky drag-and-drop editors, and introduce brittle iframe sync issues. With HubSpot Operations Hub Custom Code Actions (Node.js 18) and the Signbee REST API, you can automatically pull deal parameters (Amount, Company Name, Contact Email), compile dynamic Markdown agreements, send them for electronic signature, and advance the Deal stage to "Contract Signed" via inbound webhooks—in under 100 lines of code.
The Problem with Marketplace E-Signature Integrations
When growing sales and RevOps teams look to automate contract creation in HubSpot, the default recommendation is usually an App Marketplace integration such as DocuSign, PandaDoc, or Adobe Sign. While these apps offer fast click-and-point setups, they introduce substantial operational friction at scale:
- The Per-Seat Tax: Most marketplace vendors charge between $35 and $85 per user per month. If you have 30 sales reps, account executives, and onboarding managers triggering contracts, you are paying $15,000 to $30,000+ annually merely for the right to generate PDFs from CRM records. We explored these vendor trade-offs in detail in our PandaDoc API vs CRM automation guide.
- Rigid Visual Builders: Modifying line items, calculating volume-tiered discounts, or injecting dynamic payment milestones requires manual template gymnastics inside third-party UIs.
- Data Asynchrony & Broken Webhooks: Marketplace apps often suffer from delayed polling intervals or opaque sync errors, leaving sales reps wondering if a client actually executed their Master Services Agreement (MSA).
By leveraging HubSpot Custom Code Actions in combination with an API-first signing service like Signbee, you maintain 100% data sovereignty, pay only for documents dispatched, and generate clean, standardized contracts compiled directly from your CRM properties.
End-to-End Architectural Overview
The automated signing workflow operates on a clean, bi-directional request-response loop between HubSpot CRM, HubSpot Operations Hub, and the Signbee API:
Automated Lifecycle Architecture
- Trigger: Sales rep moves a HubSpot Deal to stage
Contract Requested(orDecision Maker Bought-In). - Custom Code Action: HubSpot executes a Node.js 18 serverless function. It extracts the Deal Amount, Deal Name, Deal ID, Company Name, and Primary Contact Email.
- Dynamic Contract Compilation: The function generates clean Markdown containing deal terms, payment schedules, and legal boilerplate.
- API Dispatch: The function posts the payload to
https://api.signb.ee/v1/documentswith metadata linking back to thehs_object_id. - Signer Experience: The recipient receives a responsive, mobile-optimised signing link with optional SMS OTP authentication.
- Webhook Callback: Upon document execution, Signbee posts a signed event to your webhook listener.
- CRM Stage Advancement: Your webhook endpoint authenticates the payload and updates the HubSpot Deal stage to
Contract Signedwhile attaching the final PDF link and audit trail SHA-256 hash.
Step 1: Configuring Custom Properties & Secrets in HubSpot
Before writing the custom code action, set up the required custom properties on your Deal object and configure your environment secrets in HubSpot.
1. Create Deal Properties
Navigate to Settings > Properties > Deal Properties and create three new single-line text fields:
signbee_document_id— Stores the unique Signbee UUID.signature_status— Options or string:pending,signed,declined.contract_pdf_url— URL to download the final executed PDF.
2. Store API Secrets
In HubSpot Workflow Builder, custom code actions can access encrypted secrets without exposing credentials in plaintext:
- Open your HubSpot Workflow and add a Custom Code action.
- Under Secrets, click Manage secrets.
- Add
SIGNBEE_API_KEYwith your live key (e.g.sb_live_...). - Add
HUBSPOT_ACCESS_TOKENwith a Private App access token withcrm.objects.deals.readandcrm.objects.contacts.readscopes.
Step 2: HubSpot Custom Code Action (Node.js 18)
Below is the complete, self-contained custom code action for HubSpot Operations Hub. It extracts deal parameters, compiles an executive Sales Agreement in Markdown, and creates the document via the Signbee REST API.
const hubspot = require('@hubspot/api-client');
exports.main = async (event, callback) => {
// 1. Retrieve environment secrets
const SIGNBEE_API_KEY = process.env.SIGNBEE_API_KEY;
const HUBSPOT_ACCESS_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;
if (!SIGNBEE_API_KEY) {
throw new Error("Missing SIGNBEE_API_KEY secret in HubSpot Workflow.");
}
// 2. Extract deal properties passed into the action
const {
hs_object_id: dealId,
dealname,
amount,
pipeline,
dealstage
} = event.inputFields;
const hubspotClient = new hubspot.Client({ accessToken: HUBSPOT_ACCESS_TOKEN });
// 3. Fetch associated Contact (Primary Signer) and Company
let signerEmail = "";
let signerName = "Authorized Representative";
let companyName = "Client Organization";
try {
const associations = await hubspotClient.crm.deals.associationsApi.getAll(dealId, 'contacts');
if (associations.results && associations.results.length > 0) {
const contactId = associations.results[0].id;
const contactRecord = await hubspotClient.crm.contacts.basicApi.getById(contactId, ['email', 'firstname', 'lastname']);
signerEmail = contactRecord.properties.email;
const first = contactRecord.properties.firstname || "";
const last = contactRecord.properties.lastname || "";
signerName = `${first} ${last}`.trim() || signerName;
}
const companyAssoc = await hubspotClient.crm.deals.associationsApi.getAll(dealId, 'companies');
if (companyAssoc.results && companyAssoc.results.length > 0) {
const companyId = companyAssoc.results[0].id;
const companyRecord = await hubspotClient.crm.companies.basicApi.getById(companyId, ['name']);
companyName = companyRecord.properties.name || companyName;
}
} catch (err) {
console.warn("Association lookup warning (fallback used):", err.message);
}
if (!signerEmail) {
throw new Error(`Cannot dispatch contract: No contact email found for Deal ID ${dealId}`);
}
// Format currency
const formattedAmount = Number(amount || 0).toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
});
const currentDate = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// 4. Construct dynamic Markdown contract template
const markdownContract = `# SOFTWARE SERVICE AGREEMENT
**Effective Date:** ${currentDate}
**Agreement Reference:** HUB-${dealId}
---
### PARTIES
This Software Service Agreement ("Agreement") is entered into by and between:
1. **Provider:** Acme Cloud Technologies Inc., a Delaware corporation ("Provider").
2. **Customer:** ${companyName}, represented by ${signerName} ("Customer").
---
### 1. SERVICES & COMMITMENT
Provider agrees to deliver enterprise platform capabilities as outlined in Deal "${dealname}". Customer agrees to pay the contract value detailed below.
| Description | Contract Value | Payment Term | Billing Cycle |
| :--- | :--- | :--- | :--- |
| **Enterprise Platform Access** | ${formattedAmount} | Net 30 | Annual Upfront |
### 2. TERM & TERMINATION
This Agreement shall commence on the Effective Date and continue for an initial term of twelve (12) months. Either party may terminate with 30 days written notice prior to renewal.
### 3. GOVERNING LAW & SIGNATURES
This agreement shall be governed by the laws of the State of Delaware. The parties acknowledge and agree that electronic signatures executed through this interface comply with the ESIGN Act and UETA regulations.
---
**CUSTOMER ACCEPTANCE:**
**Company:** ${companyName}
**Signer Name:** ${signerName}
**Email:** ${signerEmail}
`;
// 5. Dispatch to Signbee REST API
const signbeeResponse = await fetch("https://api.signb.ee/v1/documents", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SIGNBEE_API_KEY}`
},
body: JSON.stringify({
title: `Contract: ${dealname} - ${companyName}`,
markdown: markdownContract,
signers: [
{
name: signerName,
email: signerEmail,
role: "signer"
}
],
metadata: {
hubspot_deal_id: dealId,
company_name: companyName,
deal_amount: amount
},
require_consent: true,
expires_in_days: 14
})
});
if (!signbeeResponse.ok) {
const errorText = await signbeeResponse.text();
throw new Error(`Signbee API dispatch failed (${signbeeResponse.status}): ${errorText}`);
}
const signbeeData = await signbeeResponse.json();
// 6. Return output fields back to the HubSpot Workflow
callback({
outputFields: {
signbee_document_id: signbeeData.id,
signing_url: signbeeData.signing_url || "",
signature_status: "pending"
}
});
};In your HubSpot Workflow, follow the Custom Code action with an Edit Property action that copies the output field signbee_document_id into the Deal's signbee_document_id property and sets signature_status to pending.
Step 3: Building the Bi-Directional Webhook Listener
When the customer signs the agreement, Signbee dispatches an instant webhook event. To close the loop and advance your deal pipeline, host a lightweight webhook receiver in your serverless stack (such as a Next.js App Router endpoint or AWS Lambda).
The receiver validates the cryptographic signature using HMAC-SHA256 and calls the HubSpot CRM v3 API to update the deal stage and log the audit trail.
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
const SIGNBEE_WEBHOOK_SECRET = process.env.SIGNBEE_WEBHOOK_SECRET!;
const HUBSPOT_ACCESS_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN!;
// Contract Signed Deal Stage ID in your HubSpot Pipeline
const HUBSPOT_STAGE_CONTRACT_SIGNED = "contract_signed";
function verifyHmacSignature(rawBody: string, signatureHeader: string | null): boolean {
if (!signatureHeader || !SIGNBEE_WEBHOOK_SECRET) return false;
const expectedSignature = crypto
.createHmac("sha256", SIGNBEE_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, "utf8"),
Buffer.from(signatureHeader, "utf8")
);
}
export async function POST(req: NextRequest) {
try {
const rawBody = await req.text();
const signature = req.headers.get("x-signbee-signature");
// 1. Verify Webhook Authenticity
if (!verifyHmacSignature(rawBody, signature)) {
return NextResponse.json({ error: "Invalid HMAC signature" }, { status: 401 });
}
const payload = JSON.parse(rawBody);
const { event, data } = payload;
// We only process completed document executions
if (event !== "document.completed") {
return NextResponse.json({ received: true, ignored: true });
}
const dealId = data.metadata?.hubspot_deal_id;
if (!dealId) {
console.warn("No hubspot_deal_id found in document metadata:", data.id);
return NextResponse.json({ received: true, warning: "Missing deal ID" });
}
const downloadUrl = data.download_url;
const sha256Hash = data.sha256;
const completedAt = data.completed_at || new Date().toISOString();
// 2. Update HubSpot Deal via CRM v3 REST API
const hubspotUrl = `https://api.hubapi.com/crm/v3/objects/deals/${dealId}`;
const updateResponse = await fetch(hubspotUrl, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${HUBSPOT_ACCESS_TOKEN}`
},
body: JSON.stringify({
properties: {
dealstage: HUBSPOT_STAGE_CONTRACT_SIGNED,
signature_status: "signed",
contract_pdf_url: downloadUrl,
contract_signed_date: completedAt
}
})
});
if (!updateResponse.ok) {
const err = await updateResponse.text();
console.error(`Failed to update HubSpot deal ${dealId}:`, err);
return NextResponse.json({ error: "HubSpot update failed" }, { status: 500 });
}
// 3. Create an engagement Note on the Deal timeline
await fetch("https://api.hubapi.com/crm/v3/objects/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${HUBSPOT_ACCESS_TOKEN}`
},
body: JSON.stringify({
properties: {
hs_timestamp: new Date().toISOString(),
hs_note_body: `✅ **Contract Executed Electronically**<br/><br/>` +
`• **Signbee Doc ID:** ${data.id}<br/>` +
`• **Audit SHA-256:** `${sha256Hash}`<br/>` +
`• **Download PDF:** <a href="${downloadUrl}" target="_blank">View Executed PDF</a>`
},
associations: [
{
to: { id: dealId },
types: [
{
associationCategory: "HUBSPOT_DEFINED",
associationTypeId: 214 // Note to Deal association
}
]
}
]
})
});
return NextResponse.json({ success: true, dealId });
} catch (error: any) {
console.error("Webhook processing error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}If you are building specialized workflows such as Non-Disclosure Agreements or multi-tier enterprise contracts, check our guide on automating NDA signing workflows and our architectural overview of SaaS e-signature integrations.
Handling Complex Deal Structures & Line Items
Unlike static template builders where dynamic tables require messy custom merge tags, Markdown-based document generation enables effortless iteration over HubSpot Line Items and multi-currency quotes.
You can query the HubSpot Line Items API (/crm/v3/objects/line_items) associated with the deal, map over each item, and dynamically format a markdown table in your Node.js custom code action:
// Fetch line items associated with Deal
const lineItemsResponse = await hubspotClient.crm.deals.associationsApi.getAll(dealId, 'line_items');
let lineItemMarkdown = "| Item | Qty | Unit Price | Total |\n| :--- | :--- | :--- | :--- |\n";
for (const item of lineItemsResponse.results) {
const itemDetails = await hubspotClient.crm.lineItems.basicApi.getById(item.id, ['name', 'quantity', 'price', 'amount']);
const { name, quantity, price, amount } = itemDetails.properties;
lineItemMarkdown += `| ${name} | ${quantity} | $${price} | $${amount} |\n`;
}Reliability, Error Handling, and Replay Protection
When operating mission-critical contract automation at scale, production pipelines must account for network timeouts, retry policies, and idempotency:
- HubSpot Custom Code Limits: HubSpot enforces a 20-second hard timeout on custom code execution. Always make lean API calls and avoid nested synchronous lookups. Signbee responds in ~180ms, well within HubSpot execution constraints.
- Webhook Idempotency: Webhooks may occasionally be redelivered due to network retries. Store processed
data.idvalues in Redis or check if the Deal is already incontract_signedstatus before creating duplicate timeline notes. - Audit Hash Verification: Signbee returns an immutable SHA-256 digest of the final PDF. Storing this hash on the HubSpot Deal provides an unbroken, court-admissible audit trail proving the contract was not altered after signing.
Frequently Asked Questions
Why use HubSpot Custom Code Actions instead of Marketplace apps like DocuSign or PandaDoc?
HubSpot Marketplace e-signature integrations typically require expensive per-user monthly licenses ($40 to $80+ per sales rep) and enforce rigid iframe or visual builder constraints. For engineering and RevOps teams managing high deal volumes, this quickly scales into thousands of dollars in unnecessary software overhead. By writing a lightweight Node.js custom code action inside HubSpot Operations Hub and dispatching contracts via a modern e-signature REST API like Signbee, you gain total programmatic control over contract generation, dynamic line item rendering, custom branding, and multi-signer routing. You eliminate per-seat penalties, avoid vendor lock-in, and maintain a direct, tamper-proof bi-directional data flow between HubSpot deal properties and cryptographic document audit logs.
What HubSpot subscription tier is required to execute custom code workflows for e-signatures?
To run custom code workflow actions inside HubSpot, your portal requires Operations Hub Professional or Enterprise (or legacy Sales/Service Hub Enterprise containing programmable automation). The custom code action executes in a secure, sandboxed Node.js 18 runtime managed by HubSpot, with access to external HTTP calls and encrypted workflow secrets. If your organisation currently operates on Starter or basic Professional tiers without Operations Hub, you can alternatively use standard HubSpot Webhook workflow actions to trigger an external serverless endpoint (such as an AWS Lambda, Cloudflare Worker, or Next.js route handler) that orchestrates the deal data lookup and Signbee API document dispatch.
How do we secure the webhook endpoint that receives signature callbacks and updates HubSpot deals?
Securing the signature callback endpoint requires robust cryptographic signature verification and strict replay protection. When Signbee dispatches a document.completed or document.signed webhook event, it includes a signature header computed using HMAC-SHA256 over the raw request payload and a Unix timestamp. Your receiving server must independently compute the HMAC digest using your configured webhook signing secret and verify that the timestamp falls within a five-minute tolerance window to prevent replay attacks. Additionally, your webhook handler should authenticate to the HubSpot CRM v3 API using a private app access token with tightly scoped permissions (crm.objects.deals.write and crm.objects.contacts.read) stored in secure environment variables.
Ready to automate your HubSpot contract workflows? Free tier includes 5 documents per month.
Last updated: August 17, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.