document-signer-laravel maintained by laulamanapps
Laravel integration for the document signer SDK
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 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)