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

Document Signer Sdk Laravel Package

la-souris/document-signer-sdk

Laravel/PHP SDK for document signing workflows—create, send, and track signature requests directly from your app. Wraps the Document Signer API with a clean client, typed DTOs, and framework-ready configuration for quick integration into Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

v1.0.0 is a fresh package, so treat the first session as an integration spike rather than a full rollout.

Minimum viable setup

  1. composer require the package (PHP 8.2+ is required — check your CI matrix and any platform.php pin in composer.json first).
  2. Install Browsershot's runtime dependency outside PHP: Node, Puppeteer, and a Chromium binary. In Laravel Sail/Forge/Docker this is the step that most often breaks CI and production; verify node -v and a Chromium path on every environment before writing contract code.
  3. Publish/define config for the chosen provider and bind a concrete SignatureProvider implementation in a service provider (or via the package's config if it ships a driver-style config).
  4. Author a trivial template — an NDA is the ideal first case — using the {[type:signer:name]} placeholder syntax.
  5. Render to PDF with BrowsershotPdfRenderer, send via send(), store the returned envelope/document identifier on your own model, then poll or wait for the webhook and call downloadSignedDocument() and downloadAudit().

Where to look first

  • SignatureProvider — this is the contract that defines your whole integration surface (send(), downloadSignedDocument(), downloadAudit(), webhook handling).
  • PlaceholderParser and AbstractAnchorPlaceholderReplacer — understand how placeholders become signature anchors before you design templates.
  • BrowsershotPdfRenderer — the header/footer and page-decoration options are where your branding lives.
  • The exception hierarchy — especially SignedDocumentUnavailableException, which shapes your retry design.

First use case

A single-signer NDA or contractor agreement: render HTML → PDF → send() → persist Document::$id → receive webhook → downloadSignedDocument($document->id) → store on disk → optionally downloadAudit().


Implementation Patterns

Bind the provider behind config, not new

Keep the concrete provider out of controllers and jobs. Resolve SignatureProvider from the container, selected by a config value, so switching from DocuSign to ValidSign is a config/env change rather than a code change. This is the entire point of the abstraction — don't undermine it in your first controller.

$this->app->bind(SignatureProvider::class, fn () =>
    match (config('esign.provider')) {
        'docusign' => new DocuSignProvider(config('esign.docusign')),
        'validsign' => new ValidSignProvider(config('esign.validsign')),
    }
);

Persist your own Document record before sending

Because downloadSignedDocument() accepts your Document::$id uniformly across providers, your local documents table is the source of truth. Store your id, the provider's envelope id, status, timestamps, and storage paths. Never rely on the provider dashboard as your state store.

Model the lifecycle as a state machine

draft → rendered → sent → delivered → signed → completed → downloaded. Use a status column plus guarded transitions, and drive transitions from webhooks and from your download jobs. This keeps you sane when a provider fires events out of order or duplicates them.

Put sending and downloading on the queue

Both are slow, network-bound operations. The natural pattern:

  • SendContractJob — renders via Browsershot, calls send(), persists the envelope id.
  • FetchSignedDocumentJob — calls downloadSignedDocument(), catches SignedDocumentUnavailableException, and releases with backoff.
public function handle(SignatureProvider $provider): void
{
    try {
        $pdf = $provider->downloadSignedDocument($this->document->id);
        Storage::disk('contracts')->put($this->document->signedPath(), $pdf);
        $this->document->markSigned();
    } catch (SignedDocumentUnavailableException $e) {
        $this->release(now()->addMinutes(5));
    }
}

Webhooks as a thin controller, thick job

Route the provider callback to a controller that verifies the payload and dispatches an event or job. Keep signature verification and payload normalisation inside the provider implementation where possible, so your controller doesn't grow vendor-specific branches. Make handlers idempotent — webhooks retry.

Pair the signed document with the audit certificate

downloadAudit() returns the provider's human-readable completion certificate as PDF. Store it alongside the signed PDF and reference both from your document record. This is exactly the artefact legal/compliance will ask for later, and it's much easier to fetch at completion time than months afterward.

Template management

Keep contract templates as Blade/HTML views or in the database, with the {[type:signer:name]} placeholders in plain text. Version your templates (a template_version column) so you can prove which wording a signer actually saw. Render with the same BrowsershotPdfRenderer in tests as in production so layout regressions are caught.

Testing

Write a FakeSignatureProvider implementing the same interface, returning fixture PDFs. Bind it in TestCase. This lets you test the full job/webhook/state-machine flow without hitting a vendor sandbox, and it validates that you're genuinely decoupled from the provider.


Gotchas and Tips

Browsershot is the most fragile dependency

Puppeteer/Chromium issues dominate local-vs-CI-vs-production drift: missing shared libraries in slim Docker images, Chromium sandbox flags in containers, memory limits on large templates, and timeouts on slow first renders. Pin the Chromium path via env, run a render smoke test in CI, and give the job a generous timeout. Consider rendering in a dedicated queue with a higher memory limit.

Anchor-based replacement is only as good as the PDF text layer

Placeholders are located by their text anchor, so anything that mangles extracted text breaks the mapping: custom/subset fonts, ligatures, letter-spacing, non-breaking spaces, line wrapping inside a placeholder, or CSS that splits a placeholder across elements. Keep placeholders on a single line, in a plain font, with normal spacing — and add a test that renders a template and asserts every placeholder resolves. Silent missing anchors are far worse than a loud failure.

SignedDocumentUnavailableException is expected, not exceptional

It fires when a signed document isn't yet available for a finalised envelope — a normal timing race, not a bug. Catch it explicitly before your generic ProviderException handler and treat it as retryable. Because PHP exception order matters, order your catch blocks from most specific to most general.

Don't conflate your Document::$id with the provider's envelope id

The uniform downloadSignedDocument($document->id) signature is a real convenience, but only if you keep two columns: your id (what you pass around) and the provider's id (what the provider's API expects, used internally and for webhook correlation). Storing only one will hurt.

Map the exception hierarchy onto Laravel's retry semantics

  • ProviderTransientException / ProviderRateLimitException → $this->release() with exponential backoff; respect Retry-After if the provider exposes it.
  • ProviderAuthenticationException → refresh the token/credential and retry once; if it fails again, fail loudly — usually an expired or misconfigured secret.
  • ProviderValidationException → do not retry; it's your payload or template at fault.
  • ProviderNotFoundException / ProviderException → log with context, alert.

Webhook security and idempotency

Verify the provider's signature/HMAC before trusting any payload, and dedupe on the provider's event id. Assume at-least-once delivery. Also confirm webhook routes are excluded from CSRF (api middleware group or VerifyCsrfToken::$except), which is the classic first-day 419 error.

Timezones and date formats in the audit PDF

The completion certificate carries provider-rendered dates, often in the provider's timezone. Store your own signed_at timestamp from the webhook payload rather than parsing dates out of the audit PDF.

Storage hygiene

Signed PDFs and audit certificates are sensitive. Use a private disk (not public), generate temporary URLs for authorised viewers, and avoid streaming large PDFs through PHP responses in web requests — sign URLs instead.

Extension points worth knowing

If the built-in anchor replacer doesn't fit your template style, AbstractAnchorPlaceholderReplacer is the seam to subclass. Similarly, BrowsershotPdfRenderer is the place to customise headers/footers and page decoration. Both are preferable to forking the package.

Version pinning

First public release means the interface is not yet battle-tested. Pin to ^1.0 and read the changelog on every minor bump — a SignatureProvider interface change would be a breaking change across every implementation you've written.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin