Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Docusign Bundle Laravel Package

gheb/docusign-bundle

Symfony bundle for DocuSign eSignature integration, providing configuration, API client services, and helpers to send envelopes, manage templates and recipients, and build document signing workflows in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle is a thin Symfony DI wrapper around the official docusign/esign-client PHP SDK. It saves you from wiring ApiClient/EnvelopesApi/AuthenticationApi by hand and from re-implementing the JWT Grant flow; it does not model envelopes, recipients, or webhooks for you. Budget for learning the DocuSign object model (EnvelopeDefinition, Document, Signer, Tabs) — that's where 90% of the work lives.

Minimal setup

  1. Install

    composer require gheb/docusign-bundle
    

    No Flex recipe, so register the bundle manually in config/bundles.php:

    Gheb\DocuSignBundle\GhebDocuSignBundle::class => ['all' => true],
    

    As of 5.6.5 the bundle allows psr/log:^3.0, so it installs cleanly alongside Laravel 10/11 and Symfony 6.3+ (which require psr/log 3). Before 5.6.5 that constraint was the most common reason the bundle refused to install on a modern app — if you had it on a "won't install" list, re-check.

  2. Create the credentials on the DocuSign side (this is the part people underestimate):

    • Create an app in your DocuSign developer account → get the Integration Key.
    • Generate an RSA keypair, upload the public key to the app, keep the private key out of git.
    • Grant impersonation consent once by visiting:
      https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id={INTEGRATION_KEY}&redirect_uri={YOUR_URI}
      
      Consent is per (integration key, user) pair and only needs doing once per environment.
  3. Configure — dump the reference rather than trusting the README, since the extension alias is derived from the bundle name:

    bin/console config:dump-reference gheb_docu_sign
    bin/console debug:container --tag=container.service_subscriber | grep -i docusign
    

    Typical shape:

    # config/packages/gheb_docu_sign.yaml
    gheb_docu_sign:
        integration_key: '%env(DOCUSIGN_INTEGRATION_KEY)%'
        user_id:         '%env(DOCUSIGN_USER_ID)%'      # the impersonated GUID, not an email
        account_id:      '%env(DOCUSIGN_ACCOUNT_ID)%'
        private_key:     '%env(DOCUSIGN_PRIVATE_KEY)%'  # or a path, depending on the option
        base_path:       'https://demo.docusign.net/restapi'   # production: https://www.docusign.net/restapi
    
  4. First use case — embedded signing. The highest-value, lowest-risk flow to start with: create an envelope, get a recipient view URL, redirect the user to DocuSign, then handle the return.

    $definition = (new EnvelopeDefinition())
        ->setEmailSubject('Please sign the NDA')
        ->setStatus('sent')
        ->setDocuments([$document])
        ->setRecipients(new Recipients(['signers' => [$signer]]));
    
    $summary = $envelopesApi->createEnvelope($accountId, $definition);
    $view = $envelopesApi->createRecipientView($accountId, $summary->getEnvelopeId(), $viewRequest);
    return $this->redirect($view->getUrl());
    

Where to look first: the bundle's Resources/config/services.yaml (what's actually registered and with what arguments), then the DocuSign esign-client README and the "eSignature REST API" reference. The bundle source is small enough to read end-to-end in ten minutes — do that before fighting with autowiring.

Implementation Patterns

Inject the SDK APIs, not a facade

The bundle's real value is that ApiClient is pre-authenticated at container build time with JWT. Autowire EnvelopesApi, AccountsApi, and FoldersApi and keep your own domain service around them:

final class SignatureRequestSender
{
    public function __construct(
        private EnvelopesApi $envelopesApi,
        private string $accountId,
    ) {}
}

Keep the DocuSign DTOs at the boundary. Map your Contract entity → EnvelopeDefinition → back to an envelopeId string stored on the entity. Nothing else in your app should know about SignHere tabs.

The two flows, and when to use each

Flow Status on create Use when
Remote (email) sent Signer is external, no login, you just wait for the webhook. Simplest.
Embedded (recipient view) sent, then createRecipientView Signer is already logged into your app; you control the UX and the return URL.

Embedded signing requires a returnUrl that DocuSign will redirect to — it must be reachable and, for anything beyond demo, must match a domain you've configured/verified. recipientViewRequest needs authenticationMethod: 'none' or 'email', the signer's email/userName, and a clientUserId that matches the one you set on the Signer in the envelope. Mismatched clientUserId is the single most common embedded-signing failure — it silently falls back to an invalid-recipient error.

Webhook (DocuSign Connect) is the source of truth

Don't poll — or if you must, poll on a low frequency with a sentAt guard. Configure a Connect webhook in the DocuSign admin pointing at your controller:

#[Route('/webhook/docusign', methods: ['POST'])]
public function connect(Request $request, EnvelopeRepository $envelopes): Response
{
    $payload = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);

    foreach ($payload['data']['envelopeSummary'] ?? [] as $summary) {
        $envelope = $envelopes->findOneByDocusignId($summary['envelopeId']);
        if (!$envelope) { continue; }              // unknown envelope: ignore, don't 500
        if ($envelope->getStatus() === $summary['status']) { continue; } // idempotent
        $envelope->transitionTo($summary['status']);
    }

    return new Response('', 204);   // always 204/200, or DocuSign retries
}

Include an HMAC check (DocuSign Connect supports an HMAC key) and, better, re-fetch the envelope from the API before mutating your state — the webhook body is attacker-controllable if the endpoint is public and you haven't verified it.

Status is a state machine, mirror it

created → sent → delivered → completed | declined | voided | correct. Map it to your own enum and only allow forward transitions. Handle declined and voided explicitly — they're the ones that leave dangling "pending signature" banners in your UI.

Fetching the signed PDF

On completed, download and store it yourself; don't rely on DocuSign's retention.

$tmp = $envelopesApi->getDocument($accountId, 'combined', $envelopeId);
$filesystem->write($path, file_get_contents($tmp->getPathname()));

combined gives you the merged PDF with the certificate of completion; use the per-document documentId if you need individual files.

Testing

Mock your own SignatureRequestSender interface, not the DocuSign SDK — the SDK classes are generated and painful to mock. For an end-to-end smoke test, point base_path at demo and use a sandbox account; assert you can create and void an envelope rather than asserting on exact payloads.

Gotchas and Tips

The package moves slowly, but it is not abandoned. 5.6.5 (a CS fix plus psr/log:^3.0 support) landed after a long quiet period, so the project is still receiving maintenance PRs — just at a low cadence and without feature work. Practical consequences: pin an explicit version ("gheb/docusign-bundle": "5.6.5") rather than a floating constraint, and before adopting run composer why docusign/esign-client to check whether the bundle's docusign/esign-client constraint collides with anything else in your tree. That constraint is the remaining install-time risk; 5.6.5 solved the psr/log side of the problem, not the SDK side. If it does collide, the bundle is small enough that copy-pasting its service definitions into your own services.yaml (or writing a 40-line factory) is often the cheaper path than fighting the constraint.

Private key handling. PEM keys break YAML and env parsing. Two safe options: a path to a file outside the repo (private_key: '%kernel.project_dir%/config/docusign/private.pem') or base64 in an env var decoded in a small factory. If keys were pasted into .env with literal \n, they will fail with a cryptic OpenSSL error — check for real newlines first.

Token lifetime. JWT access tokens expire in one hour. If the bundle authenticates once at container bootstrap you'll be fine for a request-per-process model (PHP-FPM), but long-running workers (Messenger consumers) will keep a stale ApiClient. Re-authenticate inside the consumer, or build a fresh client per message.

Demo vs production base paths are different hosts, not a flag. account-d.docusign.com / demo.docusign.net vs account.docusign.com / www.docusign.net. Mixing them produces 401s that look like auth bugs. Also, demo data does not migrate — envelopes you created in sandbox are gone.

userId vs accountId. userId is the GUID of the impersonated user (find it in DocuSign admin → Users, or via GET /oauth/userinfo), not their email. Wrong GUID → consent_required, which means consent wasn't granted for that exact user, not that your key is bad.

Error handling. The SDK throws DocuSign\eSign\ApiException. Always surface $e->getResponseBody() — it contains errorCode and message from DocuSign (RECIPIENT_NOT_IN_ORDER, ENVELOPE_CANNOT_BE_VOIDED, INVALID_USER_ID). Log the X-DocuSign-TraceToken response header; DocuSign support will ask for it.

Non-idempotent creates. Double-clicking "send for signature" creates two envelopes. Put a unique constraint on your side or generate your own envelope key (DocuSign supports EnvelopeDefinition custom fields) and check before creating.

Rate limits are real. Demo accounts are throttled hard (~1,000 calls/hour per integration key historically). A getEnvelope inside a loop over 500 contracts will fail. Batch, cache, or lean on the webhook.

Document tabs need anchors or explicit coordinates. SignHere with no anchorString and no xPosition/yPosition is silently dropped — the envelope goes out with nothing to sign. Use anchorString with anchorIgnoreIfNotPresent, or supply absolute coordinates against a fixed-size PDF.

Recipient ordering. Set routingOrder on Signers; without it, DocuSign signs in parallel and the order in your array is irrelevant.

Extension points. The bundle has no event system. To add cross-cutting behaviour (logging, metrics, retry) either decorate the SDK API classes via a compiler pass, or — cleaner — wrap them in your own service and never inject the SDK directly outside that layer. This also makes the eventual migration off the bundle a one-file change.

Debugging autowiring. If EnvelopesApi won't inject, run bin/console debug:autowiring docusign. The alias depends on the service IDs the bundle registered, and those can drift between versions.

Upgrading within 5.6.x. 5.6.5 contains no behavioural or API changes — the two PRs are a code-style fix and a dependency constraint widening. A patch bump like this is a safe, no-regression update and worth taking purely to unblock psr/log:^3.0 on modern Laravel/Symfony versions. Still run your existing envelope-creation smoke test afterwards, since the composer resolution it enables can pull in transitive dependency updates.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo