PHPackages                             la-souris/document-signer-laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. la-souris/document-signer-laravel

ActiveLibrary

la-souris/document-signer-laravel
=================================

Laravel integration for the document signer SDK: config, service provider, manager, facade, and webhook routes for ValidSign and DocuSign.

v1.0.0(yesterday)03↑2900%MITPHP ^8.5

Since Jul 22Compare

[ Source](https://github.com/la-souris/package-document-signer-laravel)[ Packagist](https://packagist.org/packages/la-souris/document-signer-laravel)[ RSS](/packages/la-souris-document-signer-laravel/feed)WikiDiscussions Synced today

READMEChangelog (1)Dependencies (11)Versions (2)Used By (0)

Laravel integration for the document signer SDK
===============================================

[](#laravel-integration-for-the-document-signer-sdk)

Laravel integration for the [Document Signer SDK](https://github.com/la-souris/package-document-signer-sdk): config, service provider, driver manager, facade, and verified webhook routes for both ValidSign and DocuSign.

Contents
--------

[](#contents)

- [Install](#install)
- [Configure](#configure)
    - [Default driver](#default-driver)
- [Sending an envelope](#sending-an-envelope)
- [Recipient override (dev / staging)](#recipient-override-dev--staging)
    - [Strategy](#strategy)
    - [Scope](#scope)
- [Blade components](#blade-components)
- [PDF renderer](#pdf-renderer)
    - [Default: install spatie/browsershot](#default-install-spatiebrowsershot)
    - [Use spatie/laravel-pdf](#use-spatielaravel-pdf)
    - [Bind a fully custom renderer](#bind-a-fully-custom-renderer)
- [Webhooks](#webhooks)
    - [Translated event labels](#translated-event-labels)
- [Custom providers](#custom-providers)
    - [Custom webhooks](#custom-webhooks)
- [Testing](#testing)
- [Requirements](#requirements)
- [Documentation](#documentation)

Install
-------

[](#install)

```
composer require la-souris/document-signer-laravel
```

Then add at least one provider package — the laravel package treats both as optional:

```
composer require la-souris/document-signer-validsign   # for the validsign driver
composer require la-souris/document-signer-docusign    # for the docusign driver
```

Publish the config:

```
php artisan vendor:publish --tag=document-signer-config
```

Configure
---------

[](#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
```

DocuSign fields are anchored to their `{[type:signer:name]}` placeholders and land on the placeholder's top edge (matching ValidSign). If a whole document still sits slightly high or low on your account, nudge every field without a code change — positive moves fields down, negative up:

```
DOCUSIGN_ANCHOR_Y_OFFSET_PIXELS=6
```

### Default driver

[](#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
-------------------

[](#sending-an-envelope)

```
use LaSouris\DocumentSigner\Laravel\Facades\DocumentSigner;
use LaSouris\DocumentSigner\Sdk\Document\Document;
use LaSouris\DocumentSigner\Sdk\Envelope\Envelope;
use LaSouris\DocumentSigner\Sdk\Signer\Signer;

$receipt = DocumentSigner::send(new Envelope(
    name:         'NDA',
    documents:    [new Document(
        id:   'nda',
        name: 'NDA',
        html: '{[signature:counterparty:sig]} on {[date:counterparty:signdate]}',
    )],
    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 LaSouris\DocumentSigner\Laravel\DocumentSignerManager;

public function __construct(private DocumentSignerManager $signer) {}
```

Recipient override (dev / staging)
----------------------------------

[](#recipient-override-dev--staging)

Seeded and development data is full of reserved domains — `example.com`, `*.test`, and friends — that providers like ValidSign reject outright, so an otherwise-valid envelope fails the moment you try to send it. The recipient override rewrites every signer's email **before** the envelope reaches the provider, redirecting those addresses to a real inbox you control instead.

It is off by default and adds zero overhead when disabled — the real provider is used directly, no wrapper installed. **Keep it off in production.**

```
DOCUMENT_SIGNER_OVERRIDE_ENABLED=true
DOCUMENT_SIGNER_OVERRIDE_TO=you@yourdomain.test
```

That's the whole setup for the default (`catch_all`) strategy. It applies on every send path — `DocumentSigner::send()`, `DocumentSigner::driver('validsign')->send()`, and a manually resolved instance alike.

### Strategy

[](#strategy)

`DOCUMENT_SIGNER_OVERRIDE_STRATEGY` chooses *how* each address is rewritten. Given `DOCUMENT_SIGNER_OVERRIDE_TO=dev@you.test` (or `DOCUMENT_SIGNER_OVERRIDE_DOMAIN=you.test`):

Strategy`alice@example.com` becomesNotes`catch_all` *(default)*`dev+alice=example.com@you.test`Everyone lands in one inbox, but the original address is folded into a `+tag` so each signer stays a **distinct** recipient. ValidSign rejects duplicate recipients on one package, so this is the safe default for multi-signer envelopes. Requires `…_TO`.`domain``alice@you.test`Keeps the local part, swaps only the domain. Recipients stay unique; you need a catch-all inbox on that domain to actually receive the mail. Requires `…_DOMAIN`.`redirect``dev@you.test`Sends everyone to `…_TO` verbatim. Simplest, but multiple signers on one envelope collapse to the same address (a provider may reject that) — use for single-signer flows only. Requires `…_TO`.A misconfigured-but-enabled override (e.g. `catch_all` with no `…_TO`) throws a clear `InvalidArgumentException` on the first send, rather than silently misrouting mail.

### Scope

[](#scope)

By default (`only_domains` empty) **every** signer address is rewritten once the override is enabled — the `enabled` flag is the only guard, so keep it off in production. Add entries only to *narrow* the rewrite to specific domains and let all others pass through untouched:

```
// config/document-signer.php — rewrite only seeded/test data, leave the rest
'only_domains' => ['example.com', '*.test', '*.local'],
```

Entries match case-insensitively; a `*.` prefix does a suffix match (`*.test`matches `foo.test`).

Blade components
----------------

[](#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:

```
Mutual NDA

I, , agree.

Signed:
   on

   I would like to receive updates.
```

ComponentCompiles to```{[signature:…:…]}````{[initials:…:…]}````{[text:…:…]}````{[date:…:…]}````{[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
------------

[](#pdf-renderer)

The manager wires a [`PdfRenderer`](https://github.com/la-souris/package-document-signer-sdk/blob/main/src/Pdf/PdfRenderer.php)into every driver it resolves. By default it uses the SDK's `BrowsershotPdfRenderer`. Two other options are built in.

### Default: install spatie/browsershot

[](#default-install-spatiebrowsershot)

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

[](#use-spatielaravel-pdf)

If your application already configures [spatie/laravel-pdf](https://github.com/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

[](#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 LaSouris\DocumentSigner\Sdk\Pdf\PdfRenderer;
use App\Pdf\GotenbergRenderer;

$this->app->bind(PdfRenderer::class, GotenbergRenderer::class);
```

See [Writing a custom renderer](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/pdf-rendering.md)for the interface and an example.

Webhooks
--------

[](#webhooks)

The signing secret **is** the on/off switch. A webhook route is registered for each driver whose secret is set:

- `DOCUSIGN_CONNECT_HMAC_SECRET` set → `POST /document-signer/webhooks/docusign`
- `VALIDSIGN_CALLBACK_SECRET` set → `POST /document-signer/webhooks/validsign`

If neither is set, no webhook routes are registered at all — a webhook with no secret would 401 every request anyway, so exposing it wouldn't be useful. This is also the switch to use when callbacks are handled by a separate service (queue worker, edge function, external ingest): just leave the secret unset.

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.routing` in the config file. Each provider's secret lives in its own entry, under `webhook`.

ProviderRegistered whenRoute nameURLSignature mechanismDocuSign`DOCUSIGN_CONNECT_HMAC_SECRET` is set`document-signer.webhooks.docusign``POST /document-signer/webhooks/docusign`HMAC-SHA256 of raw body in `X-DocuSign-Signature-1..N`ValidSign`VALIDSIGN_CALLBACK_SECRET` is set`document-signer.webhooks.validsign``POST /document-signer/webhooks/validsign`Shared secret in `Authorization: Basic ` — accepted as `base64("user:secret")`, `base64(secret)`, or the raw stringBoth verifiers use `hash_equals` for constant-time comparison and reject unverified requests with HTTP 401.

Listen to the event in `app/Providers/EventServiceProvider.php`:

```
use LaSouris\DocumentSigner\Laravel\Events\DocumentSignerWebhookReceived;

protected $listen = [
    DocumentSignerWebhookReceived::class => [
        \App\Listeners\HandleSignerWebhook::class,
    ],
];
```

Every event carries the originating provider on the envelope as `$event->provider` (a class-string) — always set, regardless of the event below. The controller also resolves the payload's event token against the provider's `WebhookEvent` enum before dispatching, so listeners can classify events without doing the enum look-up themselves. `WebhookEvent` is a `BackedEnum`; both first-party providers (ValidSign and DocuSign) ship an enum with an `Unknown` case, so unmatched tokens resolve to a non-null, semantically-inert value rather than `null`. Only a custom provider that returns `null` (or ships no enum) yields `$event->event === null`, so keep the null-safe operator:

```
use LaSouris\DocumentSigner\Laravel\Events\DocumentSignerWebhookReceived;

final class HandleSignerWebhook
{
    public function handle(DocumentSignerWebhookReceived $event): void
    {
        // Provider-agnostic — the same code serves DocuSign and ValidSign.
        // Null-safe: a null event / Unknown case falls through to `default`.
        match (true) {
            $event->event?->isCompleted() => $this->onCompleted($event->payload),
            $event->event?->isDeclined()  => $this->onDeclined($event->payload),
            $event->event?->isFailure()   => $this->pageOncall($event->payload),
            default                       => null,
        };
    }
}
```

Need the originating provider? It's on the envelope as `$event->provider`(always present, even when `$event->event` is `null`), so you can branch on it unambiguously:

```
use LaSouris\DocumentSigner\ValidSign\ValidSignProvider;

if ($event->provider === ValidSignProvider::class) {
    // ValidSign-specific handling; short name is $event->provider::NAME
}
```

The raw payload is still available on `$event->payload` (and the original `Illuminate\Http\Request` on `$event->request`) if you need vendor-specific fields.

### Translated event labels

[](#translated-event-labels)

Each `WebhookEvent` can be rendered as a human-readable string via `EventTranslator`, backed by the package's translation files:

```
use LaSouris\DocumentSigner\Laravel\Webhook\EventTranslator;

public function handle(
    DocumentSignerWebhookReceived $event,
    EventTranslator               $labels,
): void {
    // Nothing to label when the provider ships no enum.
    if ($event->event === null) {
        return;
    }

    // "Package complete" in en, "Pakket voltooid" in nl.
    Log::info($labels->label($event->event, $event->provider));

    // Or force a locale:
    $subject = $labels->label($event->event, $event->provider, locale: 'nl');
}
```

The package ships English (`en`) and Dutch (`nl`) translations for every ValidSign and DocuSign event out of the box. To add a locale or override the wording, publish the translations and edit the copies:

```
php artisan vendor:publish --tag=document-signer-translations
# → lang/vendor/document-signer/{en,nl}/{validsign,docusign}-events.php
```

Translation keys mirror the enum's raw provider tokens verbatim (`document-signer::validsign-events.PACKAGE_COMPLETE`, `document-signer::docusign-events.envelope-completed`), so you can also reach them via `trans()` directly.

Custom providers
----------------

[](#custom-providers)

Providers are configured as a list under `document-signer.providers`, each entry naming a `class`. Nothing about the built-in providers is privileged — to add your own signing integration, point an entry at your class:

```
// config/document-signer.php
'providers' => [
    // ...validsign / docusign entries...
    [
        'class'  => \App\Signing\AcmeSignProvider::class,
        'config' => [
            'api_key'  => env('ACME_API_KEY'),
            'base_url' => env('ACME_BASE_URL', 'https://api.acme.example'),
        ],
        'webhook' => [
            'secret' => env('ACME_WEBHOOK_SECRET'),
        ],
    ],
],
```

Your class needs two things:

1. **Implement the SDK's `SignatureProvider`** — the manager type-guarantees every resolved driver against it. (An entry whose class doesn't implement it is ignored.)
2. **Declare a `NAME` constant** — this short name is how the provider is selected (`DOCUMENT_SIGNER_DRIVER=acme`, `DocumentSigner::driver('acme')`) and the last segment of its webhook route. A listed class without it throws a clear error.

The manager builds your provider **through the container**, so ordinary dependencies auto-wire. Two arguments are supplied by name when you declare them — take either, both, or neither:

- `array $config` — the entry's `config` block, so credentials come straight from configuration (no need to read `env()` yourself).
- `PdfRenderer $pdfRenderer` — the integration-managed renderer (§ *PDF renderer*), for providers that render envelope documents.

```
namespace App\Signing;

use LaSouris\DocumentSigner\Sdk\Pdf\PdfRenderer;
use LaSouris\DocumentSigner\Sdk\Provider\SignatureProvider;

final class AcmeSignProvider implements SignatureProvider
{
    public const string NAME = 'acme';

    public function __construct(
        private readonly array $config,        // ['api_key' => ..., 'base_url' => ...]
        private readonly PdfRenderer $pdfRenderer,
    ) {}

    // send(), getStatus(), downloadSigned(), cancel(), ...
}
```

If it's the only provider with a credential set, it's auto-selected as the default; otherwise select it with `DOCUMENT_SIGNER_DRIVER=acme`. For wiring that's easier to express in code than in config (closures, conditional construction), register a factory instead:

```
use LaSouris\DocumentSigner\Laravel\DocumentSignerManager;
use LaSouris\DocumentSigner\Sdk\Pdf\PdfRenderer;

$this->app->afterResolving(DocumentSignerManager::class, function (DocumentSignerManager $manager) {
    $manager->extend('acme', fn ($app, array $config) => new AcmeSignProvider(
        $config,
        $app->make(PdfRenderer::class),
    ));
});
```

The closure receives the container and the entry's `config` block, and owns the whole construction — so it also has to provide the `PdfRenderer` itself (bind one, or drop the argument if your provider doesn't render).

### Custom webhooks

[](#custom-webhooks)

To have the package register and verify a webhook route for your provider, implement `ProvidesWebhook`. As soon as the entry's `webhook` block holds a secret, `POST /{prefix}/{NAME}` is registered — identically to the built-ins, including `route:cache` support — and every request is verified before your listeners run.

```
use LaSouris\DocumentSigner\Laravel\Http\SignatureVerification\WebhookSignatureVerifier;
use LaSouris\DocumentSigner\Laravel\Http\Webhook\ProvidesWebhook;
use LaSouris\DocumentSigner\Sdk\Provider\SignatureProvider;
use LaSouris\DocumentSigner\Sdk\Webhook\WebhookEvent;
use Illuminate\Http\Request;

final class AcmeSignProvider implements SignatureProvider, ProvidesWebhook
{
    public const string NAME = 'acme';

    // Built without constructing the provider — no credentials needed just to
    // authenticate a callback. Receives the entry's `webhook` config block.
    public static function webhookVerifier(array $webhookConfig): WebhookSignatureVerifier
    {
        return new AcmeWebhookVerifier($webhookConfig['secret'] ?? '');
    }

    // Map the payload to a WebhookEvent so listeners can use the semantic
    // predicates; return null if you have no event enum.
    public static function resolveWebhookEvent(array $payload): ?WebhookEvent
    {
        return null;
    }
}
```

Your `WebhookSignatureVerifier::verify(Request $request): bool` should compare secrets with `hash_equals` for constant-time safety and return `false` for anything unproven — the controller turns that into an HTTP 401. Verified callbacks are dispatched as the same `DocumentSignerWebhookReceived` event (with `driver` set to your `NAME`), so existing listeners handle them unchanged.

Testing
-------

[](#testing)

Swap the live provider for a fake in tests:

```
use LaSouris\DocumentSigner\Laravel\Facades\DocumentSigner;
use LaSouris\DocumentSigner\Sdk\Envelope\EnvelopeStatus;
use LaSouris\DocumentSigner\Sdk\Provider\EnvelopeReceipt;
use LaSouris\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 downloadSignedDocument(string $id, string $documentId): \SplFileInfo { return new \SplFileInfo('/dev/null'); }
    public function hasAuditTrail(): bool { return true; }
    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](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/extending.md).

Requirements
------------

[](#requirements)

- PHP 8.5
- Laravel 13
- `la-souris/document-signer-sdk` ^1.0
- `la-souris/document-signer-validsign` *or* `la-souris/document-signer-docusign` (each is optional; installed only for the drivers you actually use — the manager throws a clear `composer require` hint if a missing driver is requested)
- Node.js + Puppeteer (for the default Browsershot renderer)

Documentation
-------------

[](#documentation)

- [SDK README](https://github.com/la-souris/package-document-signer-sdk)
- [Getting started](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/getting-started.md)
- [ValidSign provider guide](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/providers/validsign.md)
- [DocuSign provider guide](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/providers/docusign.md)
- [Placeholder syntax](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/placeholders.md)
- [Extending the SDK](https://github.com/la-souris/package-document-signer-sdk/blob/main/docs/extending.md)

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/930ca3b1756d00a63c8ff3363e81f16f961cf3ef79ff0c9d362670c36d97e456?d=identicon)[LauLaman](/maintainers/LauLaman)

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/la-souris-document-signer-laravel/health.svg)

```
[![Health](https://phpackages.com/badges/la-souris-document-signer-laravel/health.svg)](https://phpackages.com/packages/la-souris-document-signer-laravel)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77022.3M184](/packages/laravel-mcp)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k30.2M151](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1235.9k21](/packages/fleetbase-core-api)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
