"Send the contract when the deal hits Closed Won" sounds like a one-line automation. In practice, it is the most over-engineered moment in the sales process, because what you actually want is rarely "send one envelope". You want an order form, an MSA if the customer has not signed one, a DPA if they are in the EU, routed through legal if the discount exceeds a threshold, and the whole thing logged against the deal record.
The native CRM-side Docusign apps were not built for that. They are built for a rep to click a button on a record. To drive a real workflow off a stage change, you fire a webhook from the CRM into Docusign Workflow Builder and let Workflow Builder handle the branching.
Here is how that works in HubSpot, Salesforce, and Zoho CRM, and where each one quietly breaks.
The pattern in one diagram #
CRM deal stage = "Closed Won"
|
v
(CRM workflow / trigger / custom function)
|
v POST {dealId, amount, region, ...}
Webhook URL -----> Baton -----> Workflow Builder
(HMAC verify, (routing, parallel envelopes,
param match) conditional signers)The CRM only does one job: emit an HTTP POST when a deal moves to the trigger stage. Everything else, the routing logic, the document selection, the signer order, lives in Workflow Builder where it belongs. Baton sits between them, verifies the signature, and matches the payload fields to the workflow's parameter names.
That separation is the whole point. Sales ops can rename a stage without touching legal's routing rules. Legal can add a DPA branch without bothering the Salesforce admin.
HubSpot: workflow action vs deal property webhook #
HubSpot gives you two real options, and one of them is a trap.
Option 1: the "Send a webhook" workflow action. This is the clean path. You build a deal-based workflow with the enrollment trigger Deal stage is any of [Closed Won], then add a "Send a webhook" action pointing at your Baton webhook URL. According to HubSpot's developer docs, this action is exclusive to Operations Hub Professional and Enterprise. The HubSpot integration partner reference confirms the same gating.
Trigger: Deal property "Deal stage" has changed to "Closed Won (Pipeline X)"
Action: Send a webhook
Method: POST
URL: https://app.iambaton.com/hooks/<your-id>
Body: include deal properties (deal_id, amount, country, ...)Option 2: the Webhooks API subscription. Available on developer apps on every tier, but you subscribe to coarse object events like deal.propertyChange, not "stage moved to Closed Won". You get every property change on every deal, then you filter server side. This is fine if you are already running an integration app, painful if you are not.
That retry behavior is your safety net for transient Workflow Builder hiccups. It is not a substitute for monitoring; if your endpoint returns a 200 to a malformed payload, HubSpot considers the delivery successful and moves on.
The trap: the native HubSpot + Docusign integration is documented as a way to "create, customize, send, and track Docusign envelopes from a contact, company, or deal record". It is a sidebar UI for a human, not a stage-change automation surface. Don't try to bend it into one.
Salesforce: managed package vs Flow callout vs platform event vs outbound message #
Salesforce gives you four reasonable ways to fire a webhook to Baton on Opportunity StageName change. Pick one based on who owns the integration and how much Baton-specific plumbing you want the platform to handle for you.
1. Baton Managed Package (recommended for the standard case). Install Baton from AppExchange and the package adds a Send to Baton action to Flow Builder, plus a Lightning admin tab where you wire up connections in a five-step wizard. The action ships with named inputs for every common slot - accountId, opportunityId, amount, currencyIsoCode, recipientEmail, closeDate, and ~30 more - so admins fill in fields instead of constructing JSON. HMAC signing, UUID idempotency keys, retry-on-transient (1m / 5m / 15m backoff), audit log, error log, and per-connection rate limiting are all baked in. For the standard "trigger Baton from Salesforce" case, this is the cheapest path with the strongest reliability guarantees.
Record-Triggered Flow on Opportunity
Entry condition: ISCHANGED(StageName) AND StageName = 'Closed Won'
Action: Send to Baton
Connection Name: OpportunityClosedWonMSA
Trigger Object Type: Opportunity
Trigger Record Id: {!$Record.Id}
Account Id: {!$Record.AccountId}
Amount: {!$Record.Amount}
Recipient Email: {!$Record.Account.PrimaryContact.Email}The downside: it's Baton-specific. If you also need to fan webhooks out to non-Baton destinations, layer #2 alongside it. Best for Salesforce-admin-owned automations, orgs running 10+ Baton triggers, and regulated industries that need an in-org audit trail.
2. HTTP Callout from Flow. Since the HTTP Callout action became GA in Flow Builder, you can POST JSON directly from a record-triggered Flow with no Apex and no package install. You construct the JSON, point it at the Baton webhook URL, and handle the signing header yourself.
Record-Triggered Flow on Opportunity
Entry condition: ISCHANGED(StageName) AND StageName = 'Closed Won'
Action: HTTP Callout (POST)
URL: https://app.iambaton.com/hooks/<your-id>
Body: { "opportunityId": "{!$Record.Id}",
"amount": {!$Record.Amount},
"country": "{!$Record.Account.BillingCountry}" }Best when you don't want a managed package installed, the trigger is a one-off, or the same Flow needs to fan out to Baton plus a non-Baton receiver. You forfeit the audit log and retry queue you'd get from the managed package - that's the trade.
3. Platform Event published from Flow. Define a DealClosedWon__e platform event with the fields you want, then publish it from a record-triggered Flow. Subscribe from your relay via CometD or the Pub/Sub API and forward to Baton. Heavy for a CRM admin; appropriate if you already have event-bus discipline and Baton is one subscriber among many.
4. Outbound Message. The original mechanism, still alive. From a record-triggered Flow call an Outbound Message action, which posts a SOAP XML envelope. Cheap, click-only, no Apex - but SOAP. Baton's webhook ingress accepts both SOAP and JSON, so it works; a generic webhook receiver typically won't.
The native Docusign for Salesforce package, like HubSpot's, is optimized around clicking a button on a record. Triggering from stage change traditionally meant Apex triggers calling the Apex Toolkit - that works, but it puts envelope-sending logic in Salesforce code, which is the wrong place for it. The Baton managed package replaces that pattern with a Flow Builder action: the trigger lives in Salesforce where the deal data is, and the orchestration lives in Docusign Workflow Builder (Maestro) where the agreement logic belongs.
Zoho CRM: custom function webhook on stage change #
Zoho is the simplest of the three to wire up. The Workflow Webhook action is in every paid edition, no separate add-on.
Workflow Rule
Module: Deals
Execute on: Field update (Stage)
Condition: Stage = "Closed Won"
Instant Action: Webhook
Method: POST
URL: https://app.iambaton.com/hooks/<your-id>
Parameters (JSON body):
deal_id = ${Deals.Deal Id}
amount = ${Deals.Amount}
country = ${Deals.Account Name.Billing Country}If you need anything Zoho's UI cannot express - a computed field, a fan-out to multiple endpoints, a guard against duplicate fires - graduate to a Custom Function written in Deluge and call invokeurl from there. Same trigger, more headroom.
Why route through Workflow Builder instead of a direct "send" action #
Once the webhook reaches Baton, it triggers a Workflow Builder workflow rather than directly creating an envelope. That distinction matters, and it is the reason this pattern beats "wire CRM directly to the Docusign envelopes API".
A Workflow Builder workflow can:
- Route conditionally based on payload fields (region, amount, product line).
- Fan out to multiple parallel envelopes (order form + MSA + DPA) from one trigger.
- Insert approval steps, web forms, or extension app calls between envelopes.
- Be edited by a non-engineer without redeploying anything.
Docusign's how-to-trigger documentation lists multiple supported trigger methods; the HTTP/API trigger is the one Baton drives. None of that flexibility exists if your CRM is wired straight to the eSignature API: you would be reimplementing routing in Apex or HubSpot custom code.
Fan-out: order form, MSA, and DPA from the same deal #
This is where the deal-stage pattern earns its keep. A typical Closed Won for a mid-market SaaS deal might need:
- Order form - always sent, signed by buyer's procurement.
- MSA - sent only if
account.has_active_msa = false. - DPA - sent only if
account.region = "EU"oraccount.processes_pii = true.
In a CRM-only world you would express that as three branches in HubSpot/Salesforce/Zoho, each calling the Docusign API, each maintaining its own template ID and signer mapping. Three places to break.
In the deal-stage-as-trigger pattern, the CRM fires one webhook with the deal's properties. Workflow Builder reads those properties as parameters, and a single workflow contains the three branches with conditional steps. The CRM does not know there are three documents. Legal can add a fourth (a country-specific tax form) without filing a Salesforce ticket.
The payload your CRM sends should be deliberately flat and stable:
{
"deal_id": "0061x00000abc",
"account_id": "0011x00000xyz",
"amount": 84000,
"currency": "USD",
"country": "DE",
"has_active_msa": false,
"processes_pii": true,
"buyer_email": "procurement@acme.example",
"buyer_name": "Sam Buyer"
}Baton matches each top-level field name to a Workflow Builder workflow parameter of the same name. No mapping UI, no JSONPath. Naming the CRM field country and the workflow parameter country is the entire integration.
What breaks when sales reps backdate stages #
The single most common silent failure in this pattern is not a network issue. It is human behavior.
- A rep moves a deal to Closed Won, your webhook fires, three envelopes go out. The rep then notices the amount was wrong, reverts to Negotiation, edits, and pushes Closed Won again. You just sent six envelopes.
- A rep marks Closed Won on a deal that was already manually contracted last quarter. You send envelopes for an account that already has signed paper.
- A migration script bulk-updates a thousand old deals to a new pipeline stage at 3 a.m. Your webhook fires a thousand times.
Defenses, in order of effort:
- Include a "trigger version" or
signed_atfield in the payload and have Workflow Builder skip if the deal already has a completed envelope for the same template within N days. This is idempotency at the workflow level, not the webhook level. - Require an additional CRM field to flip true (e.g.,
Send Contract = Yes) before the webhook fires. Stage alone is too noisy. - Whitelist pipeline stages explicitly per pipeline. Multi-pipeline orgs accidentally inherit "Closed Won" semantics across pipelines that should not trigger.
Webhook-level idempotency catches duplicates that come from the network (CRM retried because your endpoint timed out). Workflow-level idempotency catches duplicates that come from humans. You need both. For the network half, our piece on silent webhook failures walks through the common shapes.
Putting it together #
The deal-stage-as-trigger pattern is the same in all three CRMs:
| CRM | Recommended emitter | Gating |
|---|---|---|
| HubSpot | "Send a webhook" workflow action | Operations Hub Professional+ |
| Salesforce | HTTP Callout from record-triggered Flow | Available in standard editions; Outbound Message and Platform Event are valid alternates |
| Zoho CRM | Workflow Rule webhook (or Custom Function) | Paid editions |
The CRM emits one webhook with deal properties. Baton verifies the HMAC, matches fields to parameters, and triggers a Workflow Builder workflow. The workflow handles branching, fan-out, approvals, and the actual envelopes. When a deal stage moves and nothing happens, the question "did the trigger fire, did the workflow run, did the envelope send" splits cleanly across three systems instead of being tangled inside one Apex class or HubSpot custom-code action.
If you are wiring the first one of these, start from the platform-specific tutorial and come back here when you need fan-out.