laulamanapps / document-signer-laravel
Laravel integration for the document signer SDK: config, service provider, manager, facade, and webhook routes for ValidSign and DocuSign.
Package info
github.com/LauLamanApps/document-signer-laravel
pkg:composer/laulamanapps/document-signer-laravel
Requires
- php: ^8.5
- illuminate/contracts: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- laulamanapps/document-signer-sdk: ^1.0
Requires (Dev)
- laulamanapps/document-signer-docusign: ^1.0
- laulamanapps/document-signer-validsign: ^1.0
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^11.0
- spatie/browsershot: ^4.0 || ^5.0
- spatie/laravel-pdf: ^1.0 || ^2.0
Suggests
- laulamanapps/document-signer-docusign: Required to use the `docusign` driver.
- laulamanapps/document-signer-validsign: Required to use the `validsign` driver.
- spatie/laravel-pdf: Required to use the `laravel-pdf` PDF renderer (uses the application's spatie/laravel-pdf configuration instead of plain Browsershot).
README
Laravel integration for the Document Signer SDK: config, service provider, driver manager, facade, and verified webhook routes for both ValidSign and DocuSign.
Install
composer require laulamanapps/document-signer-laravel
Then add at least one provider package — the laravel package treats both as optional:
composer require laulamanapps/document-signer-validsign # for the validsign driver composer require laulamanapps/document-signer-docusign # for the docusign driver
Publish the config:
php artisan vendor:publish --tag=document-signer-config
Configure
config/document-signer.php reads from .env. Minimum for ValidSign:
VALIDSIGN_API_KEY=your-base64-key
Minimum for DocuSign:
DOCUSIGN_INTEGRATION_KEY=... DOCUSIGN_USER_ID=... DOCUSIGN_ACCOUNT_ID=... DOCUSIGN_PRIVATE_KEY_PATH=/path/to/private.pem
Default driver
DOCUMENT_SIGNER_DRIVER is optional. If left unset the manager auto-selects
the sole configured driver — i.e. the one whose primary credential
(VALIDSIGN_API_KEY or DOCUSIGN_INTEGRATION_KEY) is present. Set the
variable explicitly only when you configure both providers in the same app:
# Both configured — pick the implicit default: DOCUMENT_SIGNER_DRIVER=validsign
Without an explicit choice in the "both configured" case, the first implicit
DocumentSigner::send() call throws with the list of configured drivers.
Sending an envelope
use LauLamanApps\DocumentSigner\Laravel\Facades\DocumentSigner; use LauLamanApps\DocumentSigner\Sdk\Document\Document; use LauLamanApps\DocumentSigner\Sdk\Envelope\Envelope; use LauLamanApps\DocumentSigner\Sdk\Signer\Signer; $receipt = DocumentSigner::send(new Envelope( name: 'NDA', documents: [new Document( id: 'nda', name: 'NDA', html: '<p>{[signature:counterparty:sig]} on {[date:counterparty:signdate]}</p>', )], signers: [new Signer(key: 'counterparty', name: 'Jane Doe', email: 'jane@example.com')], emailSubject: 'Please sign the NDA', )); // Switch driver at runtime: $receipt = DocumentSigner::driver('docusign')->send($envelope);
You can also type-hint the manager directly:
use LauLamanApps\DocumentSigner\Laravel\DocumentSignerManager; public function __construct(private DocumentSignerManager $signer) {}
Blade components
The raw {[type:signer:name]} syntax is safe to type inside .blade.php
files — {[ / ]} doesn't collide with Blade's {{ }} echo tags. For
ergonomics, though, the package ships five anonymous components that compile
to the raw placeholder token so contracts read like normal HTML:
<h1>Mutual NDA</h1> <p>I, <x-document-signer::text signer="counterparty" name="fullname" />, agree.</p> <p>Signed: <x-document-signer::signature signer="counterparty" name="sig" /> on <x-document-signer::date signer="counterparty" name="signdate" /></p> <p><x-document-signer::checkbox signer="counterparty" name="opt_in" /> I would like to receive updates.</p>
| Component | Compiles to |
|---|---|
<x-document-signer::signature signer="…" name="…" /> |
{[signature:…:…]} |
<x-document-signer::initials signer="…" name="…" /> |
{[initials:…:…]} |
<x-document-signer::text signer="…" name="…" /> |
{[text:…:…]} |
<x-document-signer::date signer="…" name="…" /> |
{[date:…:…]} |
<x-document-signer::checkbox signer="…" name="…" /> |
{[checkbox:…:…]} |
The components are registered under the document-signer:: namespace by the
service provider. Both signer and name are required attributes. After the
view renders, the resulting HTML is what you pass to Document::$html — the
SDK parser sees the literal {[type:signer:name]} tokens and proceeds as
usual.
PDF renderer
The manager wires a PdfRenderer
into every driver it resolves. By default it uses the SDK's
BrowsershotPdfRenderer. Two other options are built in.
Default: install spatie/browsershot
The SDK bundles the BrowsershotPdfRenderer class but not the Composer
dependency — you need to install it explicitly if you want to keep the
default:
composer require spatie/browsershot
Without it the manager throws an InvalidArgumentException pointing at the
install command the first time it tries to build the renderer.
Use spatie/laravel-pdf
If your application already configures spatie/laravel-pdf — custom Node binary, default paper size, headers/footers, Browsershot tweaks — switch the SDK over so it picks up that configuration:
composer require spatie/laravel-pdf
DOCUMENT_SIGNER_PDF_RENDERER=laravel-pdf
The SDK then renders every envelope document through the Pdf facade. If
laravel-pdf isn't installed when this option is selected, the manager
throws an InvalidArgumentException pointing at the install command.
Bind a fully custom renderer
For any other engine (wkhtmltopdf, Gotenberg, an external service, a tuned
Browsershot setup), implement the SDK's PdfRenderer interface and bind it
in a service provider — the manager picks up the container binding first and
ignores the config value:
use LauLamanApps\DocumentSigner\Sdk\Pdf\PdfRenderer; use App\Pdf\GotenbergRenderer; $this->app->bind(PdfRenderer::class, GotenbergRenderer::class);
See Writing a custom renderer for the interface and an example.
Webhooks
A webhook route is auto-registered for each driver whose primary credential is configured. There is no separate enable flag — if you set up DocuSign, you get the DocuSign webhook; if you set up ValidSign, you get the ValidSign webhook. If neither driver is configured, no routes are registered at all.
DocuSign only:
DOCUSIGN_INTEGRATION_KEY=... DOCUSIGN_CONNECT_HMAC_SECRET=...
ValidSign only:
VALIDSIGN_API_KEY=... VALIDSIGN_CALLBACK_SECRET=...
The common prefix (default document-signer/webhooks) and middleware
(default ['api']) live under document-signer.webhooks in the config file.
| Provider | Registered when | Route name | URL | Signature mechanism |
|---|---|---|---|---|
| DocuSign | DOCUSIGN_INTEGRATION_KEY is set |
document-signer.webhooks.docusign |
POST /document-signer/webhooks/docusign |
HMAC-SHA256 of raw body in X-DocuSign-Signature-1..N |
| ValidSign | VALIDSIGN_API_KEY is set |
document-signer.webhooks.validsign |
POST /document-signer/webhooks/validsign |
Shared secret in ?token=, X-Callback-Key, or X-Callback-Token |
Both verifiers use hash_equals for constant-time comparison and reject
unverified requests with HTTP 401. The webhook will still 401 every request
until you also set its signing secret (DOCUSIGN_CONNECT_HMAC_SECRET /
VALIDSIGN_CALLBACK_SECRET).
Listen to the event in app/Providers/EventServiceProvider.php:
use LauLamanApps\DocumentSigner\Laravel\Events\DocumentSignerWebhookReceived; protected $listen = [ DocumentSignerWebhookReceived::class => [ \App\Listeners\HandleSignerWebhook::class, ], ];
use LauLamanApps\DocumentSigner\Laravel\Events\DocumentSignerWebhookReceived; final class HandleSignerWebhook { public function handle(DocumentSignerWebhookReceived $event): void { match ($event->driver) { 'docusign' => $this->onDocuSign($event->payload), 'validsign' => $this->onValidSign($event->payload), }; } }
Testing
Swap the live provider for a fake in tests:
use LauLamanApps\DocumentSigner\Laravel\Facades\DocumentSigner; use LauLamanApps\DocumentSigner\Sdk\Envelope\EnvelopeStatus; use LauLamanApps\DocumentSigner\Sdk\Provider\EnvelopeReceipt; use LauLamanApps\DocumentSigner\Sdk\Provider\SignatureProvider; DocumentSigner::set('validsign', new class implements SignatureProvider { public function send($envelope): EnvelopeReceipt { return new EnvelopeReceipt( provider: 'validsign', providerEnvelopeId: 'test-id', status: EnvelopeStatus::Sent, ); } public function getStatus(string $id): EnvelopeStatus { return EnvelopeStatus::Completed; } public function downloadSigned(string $id): \SplFileInfo { return new \SplFileInfo('/dev/null'); } public function downloadAudit(string $id): \SplFileInfo { return new \SplFileInfo('/dev/null'); } public function getFieldValues(string $id): array { return []; } public function cancel(string $id, ?string $reason = null): void {} });
For full end-to-end provider mocking, see Extending the SDK.
Requirements
- PHP 8.5
- Laravel 13
laulamanapps/documentsigner-sdklaulamanapps/documentsigner-validsignorlaulamanapps/documentsigner-docusign(each is optional; installed only for the drivers you actually use — the manager throws a clearcomposer requirehint if a missing driver is requested)- Node.js + Puppeteer (for the default Browsershot renderer)