tucker-eric/laravel-docusign
Laravel DocuSign offers a clean integration with the DocuSign eSignature API, making it easy to send envelopes, manage templates, enable embedded signing, and handle webhooks directly from your Laravel application.
There was no prior assessment, so this is the baseline. Version 6.5.0 is an additive release: Laravel 13 support plus a widened constraint allowing rest-client/esign-client v6 alongside v5. No breaking changes are announced, but see the upgrade note under Gotchas before treating it as a drop-in.
Install and publish
composer require tucker-eric/laravel-docusign
php artisan vendor:publish --tag=docusign # verify the tag after install
The published config/docusign.php is the first file to read — it encodes the whole integration surface (auth mode, credentials, redirect URI, environment/host). Cross-reference it with the package README, and keep it out of version control expectations by driving every value from .env.
Minimum environment set
DOCUSIGN_CLIENT_ID / DOCUSIGN_CLIENT_SECRET (the integration key)DOCUSIGN_REDIRECT_URI (auth-code mode only; must match the DocuSign app config exactly)DOCUSIGN_PRIVATE_KEY (JWT mode)DOCUSIGN_ACCOUNT_ID and the impersonated user ID / usernameaccount-d.docusign.com) and production (account.docusign.com)Decide the auth mode before writing any code. JWT grant (server-to-server, one impersonated user, no browser flow) is the right default for backend automations. Authorization-code grant is only worth the complexity if you genuinely need per-end-user consent. This choice dictates your token storage, your first-run onboarding, and half your error handling.
First use case. Send a single PDF to one signer and let them sign embedded in your app:
EnvelopeDefinition (document base64 + recipient + status => 'sent').completed without polling.That loop exercises every part of the package you will use daily and surfaces the two things that bite people first: JWT consent and webhook signature verification.
Treat the facade as transport, not domain. Wrap it in one application service (App\Services\Docusign\EnvelopeService or similar). Everything above that class should speak your own DTOs, so SDK model changes, ApiException shapes, and the v5→v6 migration stay in one file.
Mirror envelope state locally. Keep a table with envelope_id, status, sent_at, completed_at, raw_payload. DocuSign is the source of truth but is a terrible query engine — every list view in your app should read your table, not the API.
Prefer templates over hand-built tabs. Constructing tabs with absolute x/y positions is verbose and fragile across document revisions. Define the envelope in the DocuSign UI as a server template, then send templateRoles with role name, email, name, and tab values. This is also what lets non-developers change the document.
Embedded signing URLs are ephemeral. A recipient view URL is single-use and short-lived (minutes). Generate it inside the controller action that renders the signing page — never store or email it. Re-requesting after expiry is normal and expected.
Webhook-first status, poll as reconciliation. Handle Connect events synchronously (verify, persist raw, dispatch a job, return 200 fast), and run a nightly listStatusChanges call with a from_date window to catch anything the webhook missed. DocuSign retries failed webhooks for a while, but not forever.
Queue everything that touches the API. Envelope creation and document downloads routinely take seconds. A CreateEnvelope job with tries = 3 and an explicit backoff keeps request latency sane and gives you a retry story.
Download documents as a separate, deliberate step. getDocument returns base64 in the response body; decode and stream to disk in a job with a generous memory limit rather than holding signed PDFs in memory during a web request.
Local development is demo-account-only. There is no Stripe-style simulator. Budget for a demo developer account, and seed your tests with fakes at your service boundary rather than hitting the API.
JWT consent is a one-time browser step. Until an admin visits the consent URL for the integration key and user, every call fails with consent_required. Adding scopes later invalidates consent — re-consent is required. This is the single most common "the package is broken" report.
Private keys in .env are a trap. Multi-line RSA keys need literal \n escapes (and a str_replace('\n', "\n", ...) in config), or a file path, or a secret manager. Truncated newlines produce an opaque signature error, not a clear one.
Cache the access token. Tokens live ~1 hour. Re-authenticating on every job burns latency and rate budget; store per-user tokens in your cache with an expiry buffer and refresh proactively.
Log the response body on failure. DocuSign errors arrive as ApiException with a generic message; the actionable detail (error, error_description, errorCode) is in getResponseBody(). Without that in your exception handler you will debug blind.
6.5.0 upgrade note — where you could break. Widening to rest-client v6 means Composer may now resolve a new SDK major where it previously pinned v5. If your code only touches the facade, risk is low. If you type-hint or instantiate SDK classes directly (ApiClient, EnvelopesApi, request/response models, Configuration), pin the major explicitly in your own composer.json and run your suite before deploying — SDK majors have reorganised namespaces and changed signatures in the past. Likewise, Laravel 13 support means checking your own dependencies and PHP floor, and re-verifying any container binding or decorator you placed around the package's client.
Webhook HMAC verification needs the raw body. Read $request->getContent() and verify X-DocuSign-Signature-1 against your Connect key before any middleware or parsing touches the payload. Exclude the route from CSRF.
Webhooks arrive out of order and duplicated. Dedupe on envelope ID plus statusChangedDateTime, and never assume monotonic status transitions (delivered can land after completed).
Status strings are lowercase and exact. sent, delivered, completed, declined, voided, corrected. Connect payloads occasionally differ in casing from API responses — normalise before comparing.
Watch the rate limit. Limits are per account per rolling hour and shared across every integration on that account. Handle 429 with exponential backoff; do not let three queue workers retry in lockstep.
Void, don't delete, anything already sent. Sent envelopes cannot be removed, and audit trail expectations make hiding them worse than voiding with a reason.
Mind the timeout stack. If the HTTP timeout exceeds your queue worker's timeout, the worker is killed mid-request and the envelope may exist with no local record. Set the HTTP timeout well below the worker timeout, and reconcile by envelope ID on retry.
How can I help you explore Laravel packages today?