Skip to content

✍️ DocuSign eSignature Integration

DocuSign is a second e-signature provider alongside Adobe Sign: a vendor connects their own DocuSign account, and OnRamp sends agreements from their templates, collects embedded signatures in the customer portal, and syncs signed status plus the signed PDF back automatically. It reaches feature parity with Adobe Sign and runs behind per-vendor feature flags.

TL;DR

🔌 What it is

A second e-sign provider

Vendors connect their own DocuSign account, attach their templates to a DOCU_SIGN task step, and collect signatures embedded in the portal.

🏢 Multi-tenant model

One OnRamp key per environment

OnRamp is the ISV. Every vendor authenticates against a single OnRamp-owned integration key; per-vendor tokens and one partner HMAC key keep them isolated.

🔄 Round trip

Send → sign → sync

OnRamp creates and sends an envelope, the signer signs in an embedded iframe, and DocuSign webhooks push status back and store the signed PDF on the task.

What it does

  • A vendor connects their own DocuSign account from Integrations+ (OAuth).
  • Their DocuSign templates sync into OnRamp and can be attached to a DOCU_SIGN task step.
  • When the step becomes active, OnRamp creates and sends an envelope from the template.
  • The signer signs inside the customer portal — an embedded "focused view" with no email round-trip.
  • DocuSign webhooks push status changes back; OnRamp updates the agreement/signer status and stores the signed PDF on the task.

How it works

The multi-tenant model

Read this first. OnRamp is the ISV (integrator). There is one OnRamp-owned integration key per environment. Every vendor authenticates against that key — they do not bring their own DocuSign app.

Consequences:

  • Each vendor's connection (tokens, account id, region) is stored per-vendor in integrations_esign_vendor_config (provider = 'docusign').
  • Webhooks are signed by one OnRamp HMAC key (DocuSign "HMAC for Partners" / integratorManaged: true) that validates every vendor's deliveries — there is no per-customer HMAC setup.
  • Everything customer-specific is keyed by vendor_id; envelopes route back by their globally-unique DocuSign envelope GUID. Vendors never collide.

Auth / account connection flow

mermaid
sequenceDiagram
    actor Admin as Vendor Admin
    participant OnRamp as OnRamp (Integrations+)
    participant DS as DocuSign
    participant SSM as AWS SSM
    Admin->>OnRamp: Click "Connect DocuSign"
    OnRamp->>OnRamp: Generate PKCE verifier + challenge (verifier held in session)
    OnRamp->>DS: Redirect to /oauth/auth (client_id, scope, code_challenge, redirect_uri)
    Admin->>DS: Log in with THEIR DocuSign creds + consent
    DS-->>OnRamp: Redirect to /integrations/oauth_callback/docusign?code=...
    OnRamp->>SSM: Read client secret (/onramp/{env}/docusign/client_secret)
    OnRamp->>DS: POST /oauth/token (code + code_verifier + client secret)
    DS-->>OnRamp: access_token + refresh_token (rotating)
    OnRamp->>DS: GET /oauth/userinfo
    DS-->>OnRamp: account_id, base_uri (region)
    OnRamp->>OnRamp: Persist connection (vendor_id, provider=docusign, tokens, account_id, base_uri)

Notes:

  • Auth-Code grant + PKCE (no JWT/RSA). Scopes are fixed: signature extended (extended keeps refresh tokens alive for offline API use).
  • Refresh tokens rotate on every use — persisted atomically with a single-flight row lock. A scheduled keep-alive refreshes idle connections so the ~30-day idle expiry never hits.

Send → sign → status flow

mermaid
sequenceDiagram
    participant OnRamp
    participant DS as DocuSign
    actor Signer
    participant Drain as Webhook drain
    OnRamp->>DS: Create + send envelope from template (per-envelope eventNotification, includeHMAC + integratorManaged, transactionId)
    DS-->>OnRamp: envelopeId, seed or_documents / or_signers
    Signer->>OnRamp: Open signing task in portal
    OnRamp->>DS: Mint recipient view (focused view, frameAncestors = vendor portal origin)
    DS-->>Signer: Embedded signing ceremony (iframe)
    Signer->>DS: Sign
    DS-->>OnRamp: POST /integrations/docusign/webhook (HMAC-signed, envelopeSummary + recipients)
    OnRamp->>Drain: Verify HMAC, stage, drain
    Drain->>OnRamp: Update or_documents/or_signers status + fetch and store signed PDF (File model)

Notes:

  • The webhook eventNotification must request includeData: ["recipients"] so the payload carries envelopeSummary (status + signers) that the drain applies.
  • A live "was-signed" check at task-save time (/signed-status) makes the completion UX webhook-independent — the task completes even before the webhook lands; the webhook is the source of truth for the persisted record and PDF.

Basic DocuSign concepts

TermMeaning
Integration keyThe OAuth client_id for OnRamp's app. Non-secret; lives in the config map.
Demo vs prodDeveloper sandbox is account-d.docusign.com (admin apps-d.docusign.com); production is account.docusign.com. Nothing transfers demo→prod.
EnvelopeOne signing transaction (documents + recipients + tabs). Has a globally-unique GUID.
TemplateA reusable envelope definition (roles + tabs). OnRamp sends from a vendor's server template.
Recipient / roleA signer. Embedded signers use a clientUserId (OnRamp user id) instead of an email round-trip.
TabsFields on a document (text/date/checkbox/…) — where prefilled values land.
ConnectDocuSign's webhook system. OnRamp uses per-envelope eventNotification (works on all plans) rather than account-level Connect.
HMAC (for Partners)Each webhook is signed with OnRamp's partner HMAC key so we can verify authenticity.

Configuration

Config map (app/config/environments.yaml, keyed on ENVIRONMENT_NAME):

KeyMeaning
docusign_account_hostOAuth host — account-d.docusign.com (sandbox) / account.docusign.com (prod).
docusign_client_idThe integration key. null = integration off for that env.
docusign_connect_hmac_provisionedtrue once the HMAC secret exists in SSM — arms the fail-closed boot check.

Secrets (AWS SSM, per env, SecureString): /onramp/{env}/docusign/client_secret, /onramp/{env}/docusign/connect_hmac_secret.

Feature flags (per vendor): docusign (shows the tile + gates the builder), docusign-webhooks (turns on webhook processing/drain).

Boot invariant. A non-null docusign_client_id makes the app fail closed at startup unless the paired SSM client secret is readable. Provision SSM before flipping the config key.

Running it on a local dev environment

Local can do the full connect → send → sign flow against a DocuSign developer sandbox. Webhooks cannot be delivered to localhost (DocuSign can't reach it), but the live save-time signed-status check makes the signing UX work regardless.

  1. Sandbox account + integration key: create an app in the demo account (Apps & Keys), capture the integration key + a secret. Register redirect URI http://localhost:3000/integrations/oauth_callback/docusign.
  2. Client secret (local shim): the local IAM user can't read SSM. A gated local-only override in secrets.py reads DOCUSIGN_CLIENT_SECRET_LOCAL from the env (only when not is_deployed_env()). Launch the stack with that env var set. (Uncommitted local shim — never commit it.)
  3. Enable the integration key: set docusign_client_id in the defaults: block of environments.yaml (NOT a new top-level env block — that flips is_deployed_env() true and arms the SSM boot check). (Also an uncommitted local shim.)
  4. Enable the docusign feature for your local vendor; connect via Integrations+, sync templates, build a DOCU_SIGN task, and sign.

To exercise webhooks (status sync + signed PDF), use deployed dev (dev.onramp.us) instead — it has a public URL, real SSM, provisioned HMAC, and the scheduled jobs.

Local DB traps: a stale legacy or_roles table can collide with the RBAC or_roles (make db.upgradeDuplicateTable) — drop the stale table + sequence, then re-upgrade. Flask --reload is flaky on multi-file edits; full-restart after adding a route or changing a signature.

Debugging paths

SymptomLikely causeWhere to look
App crash-loops at boot after enabling an envdocusign_client_id set non-null but the SSM client secret is missing/unreadableBoot log (assert_docusign_boot_secrets); provision SSM first
Connect fails with invalid_request: code challenge requiredPKCE not sentconnect_service.py (should send code_challenge); confirm the app allows PKCE
Signing iframe: "…docusign.net refused to connect"frameAncestors doesn't include the portal originrecipient_view.py — origin comes from the vendor's customer_site_domain
Webhook 401 … eventNotification without includeHMAC?Connect HMAC secret not provisioned on the accountProvision via Admin API postConnectHmacSecret → SSM; set docusign_connect_hmac_provisioned: true
Webhooks arrive + rows are processed but status never changes / no PDFPayload has no envelopeSummary — eventNotification includeData emptyenvelopes.py build_event_notification must request ["recipients"]
integration_webhook_data stays empty despite DocuSign callingDeliveries rejected before staging (bad/absent HMAC)Backend logs on docusign_webhook; the receiver fails closed pre-staging
Status still stale but everything configuredVendor lacks the docusign-webhooks flag → events PARKFeature flags; the drain cron releases parked events

Where to look, generally: backend logs for the docusign_webhook / get_recipient_view routes (OpenSearch/CloudWatch); the integration_webhook_data, or_documents, or_signers tables; the vendor's connection row.

Key code files

Auth / connect

  • docusign/connect_service.py, connect_routes.py, dataloader.py, secrets.py, constants.py

Send

  • envelopes.py (create-and-send, eventNotification, compositeTemplates)

Signing

  • recipient_view.py (focused-view mint, frameAncestors)

Webhooks

  • routes.py (/webhook receiver), webhook_verification.py (HMAC), webhook_drain_service.py (apply), documents.py (signed-PDF store)

Frontend

  • SignatureControl.vue (+ DocuSignSignatureControl.vue), stores/docuSign.store.js; builder TaskBuilderSteps/DocuSignControl.vue

Internal documentation — gated behind Cloudflare Access.