✍️ 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
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.
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.
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_SIGNtask 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
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(extendedkeeps 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
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 carriesenvelopeSummary(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
| Term | Meaning |
|---|---|
| Integration key | The OAuth client_id for OnRamp's app. Non-secret; lives in the config map. |
| Demo vs prod | Developer sandbox is account-d.docusign.com (admin apps-d.docusign.com); production is account.docusign.com. Nothing transfers demo→prod. |
| Envelope | One signing transaction (documents + recipients + tabs). Has a globally-unique GUID. |
| Template | A reusable envelope definition (roles + tabs). OnRamp sends from a vendor's server template. |
| Recipient / role | A signer. Embedded signers use a clientUserId (OnRamp user id) instead of an email round-trip. |
| Tabs | Fields on a document (text/date/checkbox/…) — where prefilled values land. |
| Connect | DocuSign'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):
| Key | Meaning |
|---|---|
docusign_account_host | OAuth host — account-d.docusign.com (sandbox) / account.docusign.com (prod). |
docusign_client_id | The integration key. null = integration off for that env. |
docusign_connect_hmac_provisioned | true 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.
- 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. - Client secret (local shim): the local IAM user can't read SSM. A gated local-only override in
secrets.pyreadsDOCUSIGN_CLIENT_SECRET_LOCALfrom the env (only whennot is_deployed_env()). Launch the stack with that env var set. (Uncommitted local shim — never commit it.) - Enable the integration key: set
docusign_client_idin thedefaults:block ofenvironments.yaml(NOT a new top-level env block — that flipsis_deployed_env()true and arms the SSM boot check). (Also an uncommitted local shim.) - Enable the
docusignfeature for your local vendor; connect via Integrations+, sync templates, build aDOCU_SIGNtask, 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.upgrade → DuplicateTable) — 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
| Symptom | Likely cause | Where to look |
|---|---|---|
| App crash-loops at boot after enabling an env | docusign_client_id set non-null but the SSM client secret is missing/unreadable | Boot log (assert_docusign_boot_secrets); provision SSM first |
Connect fails with invalid_request: code challenge required | PKCE not sent | connect_service.py (should send code_challenge); confirm the app allows PKCE |
| Signing iframe: "…docusign.net refused to connect" | frameAncestors doesn't include the portal origin | recipient_view.py — origin comes from the vendor's customer_site_domain |
Webhook 401 … eventNotification without includeHMAC? | Connect HMAC secret not provisioned on the account | Provision via Admin API postConnectHmacSecret → SSM; set docusign_connect_hmac_provisioned: true |
Webhooks arrive + rows are processed but status never changes / no PDF | Payload has no envelopeSummary — eventNotification includeData empty | envelopes.py build_event_notification must request ["recipients"] |
integration_webhook_data stays empty despite DocuSign calling | Deliveries rejected before staging (bad/absent HMAC) | Backend logs on docusign_webhook; the receiver fails closed pre-staging |
| Status still stale but everything configured | Vendor lacks the docusign-webhooks flag → events PARK | Feature 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(/webhookreceiver),webhook_verification.py(HMAC),webhook_drain_service.py(apply),documents.py(signed-PDF store)
Frontend
SignatureControl.vue(+DocuSignSignatureControl.vue),stores/docuSign.store.js; builderTaskBuilderSteps/DocuSignControl.vue