DocuSign 'AUTHORIZATION_INVALID_TOKEN' after the access token expired mid-polling
Fixes DocuSign AUTHORIZATION_INVALID_TOKEN when a polling loop outlives the one-hour JWT access token - confirm expiry, add a refresh-on-401 wrapper, persist the polling cursor, and prefer Connect webhooks for events. Use when 401s start about an hour into a run. Not for first-request 401s or rate limiting.
TL;DR
DocuSign JWT access tokens expire after one hour, so any polling loop that runs longer dies mid-run with AUTHORIZATIONINVALIDTOKEN. The fix is not a longer token - it is a refresh-on-401 wrapper: catch the 401, generate a fresh JWT assertion, fetch a new access token, and resume polling from the last saved cursor. Persist the cursor so a resume never reprocesses or skips envelopes. Better yet, replace long polling with a Connect webhook and keep polling only as the backstop.
The query
DocuSign 'AUTHORIZATION_INVALID_TOKEN' after the access token expired mid-pollingUse this when
- A long-running polling loop starts returning AUTHORIZATIONINVALIDTOKEN after about an hour
- The first requests succeed and only later ones fail with 401
- You poll envelope status on a schedule and the job dies overnight
Not for
- 401s on the very first request - that is a bad key, bad assertion, or missing consent, not expiry
- USERAUTHENTICATIONFAILED, which is a credentials problem rather than a token-lifetime problem
- Rate-limit (429) responses during polling, which need backoff rather than a new token
Steps
1. Confirm the failure is expiry, not credentials
Decode the JWT (or check your token-issue timestamp): if the token is older than an hour when the 401s start, it is expiry. If the first request with a fresh token also 401s, stop and fix the integration key, consent grant, or assertion instead.
import time, jwt
claims = jwt.decode(access_token, options={"verify_signature": False})
print("expires_at:", claims["exp"], "now:", int(time.time()))Expected output: the exp timestamp is in the past exactly when the failures began.
2. Wrap the API client with refresh-on-401
Centralize token use in one client method: on a 401, regenerate the JWT assertion, call the token endpoint, store the new token, and retry the original request once. Never scatter token logic across call sites - one wrapper means one place to fix.
def api_get(path, retries=1):
r = requests.get(path, headers=auth_headers())
if r.status_code == 401 and retries:
refresh_access_token() # new JWT assertion -> token endpoint
return api_get(path, retries=0)
return rExpected output: polling continues past the one-hour mark with a single transparent token swap.
3. Persist the polling cursor before each page
Save the last processed envelopeId and timestamp to durable storage (database row, not process memory) before fetching the next page. A refresh that restarts the loop from scratch will reprocess envelopes; a saved cursor resumes exactly where it stopped. Expected output: after a restart, the poller logs 'resuming from [CURSOR]' and processes only new envelopes.
4. Consider replacing polling with Connect
If the loop exists to learn about envelope events, a Connect webhook delivers them without any polling or token-lifetime pressure. Keep a short polling sweep as a reconciliation backstop (hourly, with refresh-on-401) rather than the primary mechanism. Expected output: event latency drops from poll-interval to near real time, and the 401 class of failure disappears.
Tool compatibility
DocuSign eSignature REST API v2.1 with JWT grant authentication. The one-hour access-token lifetime is the same on demo and production. Any HTTP client works; the pattern is client-side.
Variant phrasings
- DocuSign AUTHORIZATIONINVALIDTOKEN access token expired polling
- DocuSign JWT token expires after one hour polling loop 401
- DocuSign polling returns 401 after an hour how to refresh token
- AUTHORIZATIONINVALIDTOKEN DocuSign long running job
Why it happens
JWT access tokens are short-lived by design: a leaked token is only useful for an hour. DocuSign does not offer a refresh-token flow for the JWT grant - the intended pattern is to mint a fresh assertion whenever you need a token. Polling loops that treat the first token as permanent fight this design; the refresh-on-401 wrapper works with it instead.
Edge cases / pitfalls
- Clock skew between your server and DocuSign can make a token look expired early - sync with NTP.
- Regenerating the assertion on every 401 without a retry cap can loop forever if the real problem is revoked consent; cap retries at one and alert.
- Multiple poller instances sharing one token can stampede the token endpoint on simultaneous 401s - serialize refreshes with a lock.
- The private key used to sign the assertion must stay readable only by the service account - a refresh wrapper is no excuse to loosen key permissions.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_pHm1f4mtqCDBGzG2JlrV2w