Skip to main content
Hat Boy Software

DocuSign JWT Grant: the failures you will actually hit

Wiring DocuSign embedded signing with JWT Grant: why invalid_grant hides the cause, the consent step, demo vs production, CSP, return URLs and branding.

We wired DocuSign embedded signing into a client’s compliance pipeline using JWT Grant. Signing the agreement is the last step before a contributor is allowed to work. The code is short. Most of the time went on failures whose messages don’t name the cause. The worst is invalid_grant, which DocuSign returns for several different mistakes and which looks the same for each of them. Here is what we hit, in the order you are likely to hit it, and what we built so the next person doesn’t have to guess.

If you are starting from scratch, begin with our step-by-step guide, Adding DocuSign embedded signing to your application. This post covers what goes wrong.

Why a provider at all

The client could have signed agreements in-product: render the PDF on the server, capture a typed name and a drawn signature, hash the result and store it. We built that as an interim, and it is defensible. But a signed contributor agreement is the document a client’s diligence team and counsel actually examine. A provider’s completion certificate and audit trail are evidence that reader already recognizes. Once we had to tell them “we render and hash our own PDFs”, the conversation turned into an argument we had to win.

Before committing, we tested the premise the decision rested on: can the completion certificate actually be retrieved and stored? Against a demo account, GET /envelopes/{id}/documents/certificate returned a 109,981-byte PDF that hashes like any other object, along with the combined signed document and an audit-events trail. That settled two other things:

  • Use the direct REST API with JWT Grant. We looked at a cloud provider’s managed connector for DocuSign. It documents sending and status, but not certificate retrieval, which is the one thing this integration exists to get.
  • Use an embedded signer. Setting clientUserId on the signer means DocuSign sends no email; the person signs inside your page. Leave it out and DocuSign emails them instead, which is a different product. Our envelope builder refuses to run without it.

The flow, briefly

JWT Grant lets a server act as a DocuSign user without anyone signing in each time:

  1. Sign a JWT assertion with your app’s RSA private key: iss is the integration key, sub is the impersonated user’s ID, aud is the OAuth host, and scope is signature impersonation. Keep the assertion’s lifetime to an hour.
  2. Exchange it at https://<oauth host>/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer.
  3. Create an envelope with the rendered PDF and an embedded signer, request a recipient view URL, and show the signer that URL.
  4. On completion, fetch the combined document and the certificate, hash both, and store the envelope ID, its status and the hashes.

Four values drive it: the integration key, the user ID, the API account ID, and the private key. Only the last is a secret.

The failures

invalid_grant tells you nothing about which value is wrong

We measured this directly. An unknown integration key and an unknown user ID return the same 400 invalid_grant. We sent a made-up integration key as a control and got exactly the response the real one produced. A probe that looks conclusive isn’t. You cannot test one value to rule out another.

The usual causes, in the order we would check them:

  1. Environment mismatch. Demo and production are separate systems with separate OAuth hosts: account-d.docusign.com for demo and account.docusign.com for production. A production key sent to the demo host fails as invalid_grant, which reads exactly like a bad key. Our config defaults to demo, so moving to production is one explicit setting, and forgetting it is the classic mistake.
  2. The user ID is an email address. It must be the user’s GUID (the “API Username” in DocuSign admin), not their email.
  3. The account ID is the short account number. It must be the API Account ID GUID.

Read every ID off the DocuSign admin pages. Don’t infer them.

JWT impersonation fails until the impersonated user has consented to the scopes, once, in a browser. The API cannot do this for you. If you see consent_required rather than invalid_grant, your key and user are right and only consent is missing. Our client surfaces it by name, so nobody spends an afternoon rotating a key that was fine.

Consent is a URL, opened while signed in as the impersonated user:

https://account.docusign.com/oauth/auth
  ?response_type=code
  &scope=signature%20impersonation
  &client_id=<integration key>
  &redirect_uri=<a redirect URI registered on the app>

Three details matter:

  • The redirect URI must be registered on the app, even though the integration itself never redirects. Consent won’t grant without one.
  • Demo consent does not carry over to production. Different host, different account, separate consent.
  • Prefer administrative consent if the account is organization-managed. It survives the impersonated user changing, and the user you impersonate should be a service-style account you control, not a person who might leave.

A private key flattened to one line fails without saying so

The key is shown once when you generate it. Save it straight to a file and load that file into your secret store. Pasting it through a form or an environment variable tends to collapse the line breaks, and a flattened PEM fails to sign with an error that doesn’t mention the key’s format. After storing it, read it back and confirm the first line is the -----BEGIN line on its own.

Secrets that never reach the process

Our runner passes only explicitly named keys from the secret store into the app’s environment. The DocuSign keys were provisioned correctly and simply never arrived, because nobody had added their names to that list. The config loader now names every missing value and fails at start-up, rather than halfway through someone signing.

A relative return URL gets a bare 400

When you request the recipient view, returnUrl is where DocuSign sends the signer afterwards. It must be absolute. A relative or malformed URL is not “a redirect to the wrong place”: DocuSign refuses the view outright with a 400 and no useful body. We now derive the return URL from the environment’s configured public host, so one environment’s origin cannot leak into another’s.

The CSP must name both hosts in the signing chain

If you frame the signing page, your Content Security Policy’s frame-src must allow it. The URL DocuSign gives you is on one host and redirects to another, so allowing only the first gives a blank signing panel at the redirect. List both production hosts (www.docusign.net and apps.docusign.com) alongside the demo pair from the start, and switching environments becomes a config change, not a CSP change.

Signers see your account’s branding, not your app’s

Driving a real signing session on the demo account, we found the consent dialog headed “Agreement to do business with” followed by the DocuSign account’s company name. That name was not the brand the signers knew. Nothing in the API sends it; it comes from the account profile. Set the account’s company name and branding before a real person sees it, because a signer told they’re dealing with a company they’ve never heard of has a good reason to stop.

The provider is now a processor

An e-signature provider sits in the signer’s data path. Name it in your privacy notice and your retention schedule, and state where it processes data. A demo account’s region is not necessarily your production account’s. If they differ, the privacy notice changes in the same release, because it is a statement to people about where their data goes.

What we built to make it boring

  • A read-only preflight script that runs inside the production pod, checks each value is present (printing lengths, never contents), sets the environment, requests a token and reports either success or consent_required by name. A green infrastructure apply proves the values are in the cluster; the preflight proves DocuSign accepts them.
  • Redacted errors. Failures carry the HTTP status and the error code only, never the response body or the request, so an assertion or token can’t end up in a log line.
  • A status reconciler that reads an envelope’s status directly, for signatures completed while your app wasn’t watching.
  • The swap made separately. The client, the credentials and the preflight shipped first and changed no behavior. Replacing the interim signing path is its own change, merged only once the paid account exists and the flow has run end to end. Signing is the one irreversible act in the pipeline, so its mechanism changes alone.

How we can help

We build compliance pipelines that produce evidence an auditor or a diligence team accepts without argument, and we wire up the third-party services they depend on. If you are integrating e-signature, identity or payments and the failures aren’t telling you why, talk to us.

Share this article

Related articles

Cloud Security & Compliance

Adding DocuSign embedded signing to your application

A step-by-step guide to DocuSign embedded signing with JWT Grant: account setup, consent, tokens, envelopes, the signing view, evidence capture and going live.

← All articles

Not sure where to start? Start with an assessment.

A senior review of your app, cloud estate, or AI platform. Scoped and quoted before work starts. It ends in a prioritized plan, so you decide what to fix and when.

Talk to an engineer