|
| 1 | +# DocuSign eSignature |
| 2 | + |
| 3 | +Send documents out for signature, read envelopes and follow their signature |
| 4 | +status from your flows. The app calls DocuSign's eSignature REST API in the |
| 5 | +DocuSign **developer sandbox** (`https://demo.docusign.net/restapi`) through a |
| 6 | +connector built from DocuSign's own OpenAPI document, and exposes three |
| 7 | +envelope operations as connector actions. It signs in to DocuSign with OAuth, |
| 8 | +using an integration key your administrator registers for your deployment: |
| 9 | +the app carries no key of anyone's. |
| 10 | + |
| 11 | +## What it adds |
| 12 | + |
| 13 | +- **A connector, `docusign`.** Its actions are DocuSign's own operation ids: |
| 14 | + |
| 15 | + | Action | Request | What it does | |
| 16 | + |:---|:---|:---| |
| 17 | + | `Envelopes_PostEnvelopes` | `POST /v2.1/accounts/{accountId}/envelopes` | Creates an envelope; with `status: "sent"` DocuSign emails it to the signers at once | |
| 18 | + | `Envelopes_GetEnvelope` | `GET /v2.1/accounts/{accountId}/envelopes/{envelopeId}` | Reads one envelope, its `status` included | |
| 19 | + | `Envelopes_GetEnvelopes` | `GET /v2.1/accounts/{accountId}/envelopes` | Lists envelopes, for example those whose status changed since `from_date` | |
| 20 | + |
| 21 | + No other DocuSign operation is in the package, so none can reach your flows |
| 22 | + or agents. |
| 23 | +- **An object, `docusign_envelope`**, one record per envelope the app sent: |
| 24 | + `envelope_id` (DocuSign's envelope id), `signature_status` (DocuSign's own |
| 25 | + status word: `sent`, `delivered`, `completed`, `declined`, `voided`, …), |
| 26 | + `email_subject` and `status_changed_at`. The status is DocuSign's, mirrored |
| 27 | + each time a flow below reads it. Its list view, **DocuSign Envelopes**, |
| 28 | + shows the newest status change first. |
| 29 | +- **Three flows that use them**, ready to run or to copy: |
| 30 | + - `docusign_send_envelope` takes `account_id`, `email_subject`, |
| 31 | + `document_name` (for example `contract.pdf`), `document_base64`, |
| 32 | + `signer_name` and `signer_email`, sends the document to that signer and |
| 33 | + records the envelope. DocuSign's answer is the run's `send.body` output. |
| 34 | + - `docusign_get_envelope` takes `account_id` and `envelope_id`, reads the |
| 35 | + envelope and refreshes its record's `signature_status`. DocuSign's |
| 36 | + envelope is the run's `envelope.body` output. |
| 37 | + - `docusign_poll_status_changes` takes `account_id` and `from_date` (an ISO |
| 38 | + 8601 instant), lists the envelopes whose status changed since then (at |
| 39 | + most 100) and copies each status onto its record. DocuSign's list is the |
| 40 | + run's `changes.body` output. See [Callbacks](#callbacks) for why this |
| 41 | + poll exists. |
| 42 | + |
| 43 | +`account_id` is your DocuSign account's API account id (a GUID): in the |
| 44 | +DocuSign developer console it is **API Account ID** on **Apps and Keys**. |
| 45 | + |
| 46 | +An install adds the connector, the object, its list view and the three |
| 47 | +flows, and nothing else: no sample data and no demo users. |
| 48 | + |
| 49 | +## Credentials |
| 50 | + |
| 51 | +The app needs one OAuth client, which **you register with DocuSign for your |
| 52 | +own deployment**. ObjectStack ships none: a DocuSign OAuth client is an |
| 53 | +integration key that accepts only the redirect URIs registered on it, and the |
| 54 | +redirect URI belongs to your deployment. Until an administrator connects the |
| 55 | +`docusign` connector, it is listed as degraded "not connected" and its flows |
| 56 | +fail, naming the connector. |
| 57 | + |
| 58 | +| What | What it is | Where to get it | |
| 59 | +|:---|:---|:---| |
| 60 | +| Integration key | DocuSign's client id for your app (a GUID) | DocuSign developer console (<https://admindemo.docusign.com/apps-and-keys>) → **Apps and Keys** → **Add App and Integration Key** | |
| 61 | +| Secret key | The client secret DocuSign issues for that integration key, shown once | The same app → **Authentication** → **Add Secret Key** | |
| 62 | +| Redirect URI | Your deployment's OAuth callback, which DocuSign must accept | The connector's connection read (below); register it on the same app under **Additional settings** → **Redirect URIs** | |
| 63 | + |
| 64 | +1. Read your deployment's redirect URI. It is your deployment's origin plus |
| 65 | + `/api/v1/automation/connectors/oauth/callback`, and the server answers the |
| 66 | + exact value, before any client exists, as `redirectUri` of |
| 67 | + `GET /api/v1/automation/connectors/docusign/connection`; the Console's |
| 68 | + **Connect** dialog (step 3) shows the same value. Use that answer, never |
| 69 | + your browser's address bar: behind a proxy, or when `OS_AUTH_URL` / |
| 70 | + `OS_BASE_URL` name another origin, the two differ. |
| 71 | +2. In the DocuSign developer console, add an app and integration key, choose |
| 72 | + **Authorization Code Grant**, add a secret key, and add the redirect URI |
| 73 | + from step 1. The app needs the `signature` scope, which is DocuSign's |
| 74 | + default for the authorization code grant. |
| 75 | +3. Connect the connector: on **Setup → Integrations → Connector Connections**, |
| 76 | + choose **Connect** on `docusign` and enter the integration key and the |
| 77 | + secret key; or begin the connect yourself with |
| 78 | + `POST /api/v1/automation/connectors/docusign/connection` and a body of |
| 79 | + `{ "clientId": "<integration key>", "clientSecret": "<secret key>" }`, then |
| 80 | + open the `authorizationUrl` it answers. Sign in to DocuSign and allow |
| 81 | + access; DocuSign returns to your redirect URI and the connection is |
| 82 | + stored. |
| 83 | + |
| 84 | +Connecting takes the organization-scoped `manage_connector_connections` |
| 85 | +capability (organization owners and admins hold it), from a signed-in |
| 86 | +session; an agent or an API key is refused. Each organization connects once. |
| 87 | +The platform keeps the integration key and its secret encrypted on the |
| 88 | +organization's connection, shows the key but never the secret, and uses them |
| 89 | +for the authorization, the code exchange and every token refresh. A reconnect |
| 90 | +that names no client reuses the stored one; disconnecting removes it. |
| 91 | + |
| 92 | +The token is only ever sent to DocuSign. Neither it nor your secret key is |
| 93 | +stored in the app's metadata. |
| 94 | + |
| 95 | +## Running it |
| 96 | + |
| 97 | +```bash |
| 98 | +curl -X POST "$OS_URL/api/v1/automation/docusign_send_envelope/trigger" \ |
| 99 | + -H "Authorization: Bearer $SESSION_TOKEN" -H 'Content-Type: application/json' \ |
| 100 | + -d '{"params": {"account_id": "<API account id>", "email_subject": "Please sign the contract", |
| 101 | + "document_name": "contract.pdf", "document_base64": "<the PDF, base64>", |
| 102 | + "signer_name": "Ada Lovelace", "signer_email": "ada@example.com"}}' |
| 103 | +``` |
| 104 | + |
| 105 | +To use an answer elsewhere, add a `connector_action` node with |
| 106 | +`connectorId: 'docusign'` to your own flow and read `<nodeId>.body` in a later |
| 107 | +node. An action takes its path parameters as one `path` object, its query |
| 108 | +parameters as one `query` object and its request body as one `body` value, |
| 109 | +for example |
| 110 | +`path: { dialect: 'cel', source: '{"accountId": record.docusign_account_id, "envelopeId": record.envelope_id}' }`. |
| 111 | + |
| 112 | +A contract app that follows its own records' signatures keeps the same two |
| 113 | +fields on its record (`envelope_id`, `signature_status`) and copies them from |
| 114 | +the `docusign_envelope` record, or from the action's answer, in its own flow. |
| 115 | + |
| 116 | +A DocuSign error answer does not fail the step: it lands as |
| 117 | +`<nodeId>.status`, `<nodeId>.ok` false and DocuSign's error in |
| 118 | +`<nodeId>.body`. The three flows check `ok` and write no record when DocuSign |
| 119 | +refused the call. |
| 120 | + |
| 121 | +## Callbacks |
| 122 | + |
| 123 | +DocuSign can post envelope events to a URL you name (DocuSign Connect). This |
| 124 | +app does **not** take those posts, and you should not point Connect at a |
| 125 | +deployment's `api` flow door: Connect signs each post with its own HMAC |
| 126 | +header, and the `api` door verifies only the platform's own signature |
| 127 | +scheme, so a Connect post cannot be told from a forged one there. Until the |
| 128 | +platform verifies vendor callback schemes, follow status changes by polling: |
| 129 | +run `docusign_poll_status_changes` on a schedule, passing the time of the |
| 130 | +previous run as `from_date`, or `docusign_get_envelope` for one envelope. |
| 131 | + |
| 132 | +## Limits |
| 133 | + |
| 134 | +- **Sandbox only.** The connector points at DocuSign's developer environment: |
| 135 | + sign-in at `account-d.docusign.com`, API calls at `demo.docusign.net`. A |
| 136 | + production DocuSign account signs in at `account.docusign.com` and has an |
| 137 | + API host of its own (DocuSign answers it as `base_uri` from |
| 138 | + `/oauth/userinfo`), which this package cannot name for you; production is |
| 139 | + not supported yet. |
| 140 | +- One document and one signer per `docusign_send_envelope` run. The action |
| 141 | + itself takes DocuSign's whole envelope definition; copy the flow to send |
| 142 | + more. |
| 143 | +- `docusign_poll_status_changes` reads one page of at most 100 changed |
| 144 | + envelopes. Poll often enough that one page covers the interval. |
| 145 | +- Not measured live: the tests run against a fake DocuSign. A round trip |
| 146 | + against the DocuSign sandbox (connect, send, read, poll) is owed to the |
| 147 | + acceptance run, and the DocuSign console steps under Credentials follow |
| 148 | + DocuSign's documentation. |
| 149 | +- The request body and response schemas in the packaged document are not |
| 150 | + expanded (see Provenance): `Envelopes_PostEnvelopes` takes DocuSign's |
| 151 | + envelope definition as one JSON object, described by its type and |
| 152 | + description only, so its fields are DocuSign's documented ones (the |
| 153 | + `docusign_send_envelope` flow shows the minimal set). The answer itself is |
| 154 | + DocuSign's full JSON. |
| 155 | + |
| 156 | +## Provenance |
| 157 | + |
| 158 | +`openapi/docusign-esignature.openapi.json` is generated, not written by hand. |
| 159 | +It is DocuSign's eSignature REST API document, |
| 160 | +`docusign/OpenAPI-Specifications` `esignature.rest.swagger-v2.1.json` at |
| 161 | +commit `858a3ae59b0edbc8beea4fa3a6d7fe803833dd68` (Swagger 2.0, 414 |
| 162 | +operations), converted to OpenAPI 3.0 and trimmed to the three operations |
| 163 | +above by this repository's offline normalizer: |
| 164 | + |
| 165 | +```bash |
| 166 | +pnpm --filter @objectstack/connector-openapi normalize-openapi \ |
| 167 | + --in https://raw.githubusercontent.com/docusign/OpenAPI-Specifications/858a3ae59b0edbc8beea4fa3a6d7fe803833dd68/esignature.rest.swagger-v2.1.json \ |
| 168 | + --include-pattern '^Envelopes_(PostEnvelopes|GetEnvelope|GetEnvelopes)$' \ |
| 169 | + --trim --empty-consumes application/json --max-depth 0 \ |
| 170 | + --out packages/apps/connector-docusign/openapi/docusign-esignature.openapi.json |
| 171 | +``` |
| 172 | + |
| 173 | +It printed OpenAPI 3.0.4, 304,080 bytes, 3 operations trimmed from 414, 187 |
| 174 | +empty `consumes` lists filled with `application/json`, 7 schema references cut |
| 175 | +at `--max-depth 0` and none left, and sha256 |
| 176 | +`b741ded5cfc4d0291d7f0e26559f8583f8c9a783f8711d8f9a98405cb27f7cb4`, the hash |
| 177 | +of the committed file. The package's tests recompute that hash and fail when |
| 178 | +the file and this record disagree. |
| 179 | + |
| 180 | +`--max-depth 0` keeps every parameter schema as DocuSign wrote it, and |
| 181 | +replaces each named schema (the envelope definition the create operation |
| 182 | +takes, and each response) with its type, title and description. At the |
| 183 | +default depth of 2 the same three operations write 553,922 bytes (sha256 |
| 184 | +`e1051eb4bbc381d879e979beac8cb64ead24531c93a19fc26e950a12d9fc961d`), with the |
| 185 | +envelope definition's fields expanded two levels. |
| 186 | + |
| 187 | +`--empty-consumes application/json` is required: DocuSign's document clears |
| 188 | +the request media type on its operations (`consumes: []`), which would leave |
| 189 | +`Envelopes_PostEnvelopes` with no request body at all. The document's |
| 190 | +`servers[0].url` is DocuSign's production host, |
| 191 | +`https://www.docusign.net/restapi`; the connector's `providerConfig.baseUrl` |
| 192 | +points it at the sandbox instead. The document declares no security scheme, |
| 193 | +so the OAuth token is sent on every operation. |
| 194 | + |
| 195 | +Re-normalize from a newer commit by running the command with that commit, |
| 196 | +then update the commit and the sha256 here in the same change. DocuSign's |
| 197 | +document is MIT-licensed; its licence notice is `openapi/LICENSE`. |
0 commit comments