Signaturit E-Signature
Integrates Signaturit for digitally signing supplier contracts. Every contract is fully executed by both parties: the supplier signs first, then a Volare admin countersigns. A single Signaturit signature request carries two recipients in sequential signing mode, and the final PDF embeds both signatures’ certificates.
When to Use
Section titled “When to Use”- Sending a supplier contract for digital signature from the Filament admin panel
- Processing signature lifecycle events (partial signature, completion, decline, cancel) via webhooks
- Downloading original or fully countersigned contract PDFs
Architecture
Section titled “Architecture”SupplierContractService (orchestrator)├── ContractPdfGenerator → DomPDF → local storage│ └── Two anchors in PDF: __SIGN_HERE__ (supplier) + __SIGN_HERE_VOLARE__ (Volare)├── SignaturitService → Signaturit REST API v3│ └── Single request, recipients[0]=supplier, recipients[1]=Volare, signing_mode=sequential└── handleSignaturitWebhook() → routes events: ├── document_signed → handlePartialSignature() (log only, no DB writes) └── document_completed → handleDocumentSigned() (download signed PDF, finalize)
SignaturitWebhookController└── POST /api/webhooks/signaturit → SupplierContractService
ContractPdfController (authenticated web routes)├── GET /contracts/{contract}/pdf└── GET /contracts/{contract}/signed-pdfSource files:
backend/app/Services/Signaturit/SignaturitService.php— API client (multi-recipient)backend/app/Services/Signaturit/ContractPdfGenerator.php— PDF generationbackend/app/Services/Signaturit/SignaturitApiException.php— Error typebackend/app/Services/SupplierContractService.php— Orchestration (e-signature methods)backend/app/Http/Controllers/Webhook/SignaturitWebhookController.php— Webhook handlerbackend/app/Http/Controllers/Webhook/ContractPdfController.php— PDF download routesbackend/resources/views/pdf/supplier-contract.blade.php— PDF template with both anchors
Configuration
Section titled “Configuration”Required env vars: SIGNATURIT_API_TOKEN, SIGNATURIT_BASE_URL
Optional env vars: SIGNATURIT_WEBHOOK_SECRET, SIGNATURIT_BRANDING_ID
Config file: config/services.php -> signaturit
| Variable | Purpose | Default |
|---|---|---|
SIGNATURIT_API_TOKEN | Bearer token for Signaturit API | — |
SIGNATURIT_BASE_URL | API base URL (sandbox or production) | https://api.sandbox.signaturit.com/v3 |
SIGNATURIT_WEBHOOK_SECRET | Optional token appended as ?token= query param for webhook validation | — |
SIGNATURIT_BRANDING_ID | Optional Signaturit branding template for email customization | — |
In addition to the env vars, the Volare entity (managed at Markets → Volare Entity in Filament) must have an email set. That email becomes the Volare countersigner. Without it, sending a contract for signature fails fast.
Signature Flow
Section titled “Signature Flow”- Admin opens a contract in Filament (Edit Supplier Contract page) and clicks Send for Signature
SupplierContractService::sendForSignature()validates that:- The contract is in
DraftorSentstatus - It has at least one service line item
- The supplier resolves to an email address
VolareEntity::current()->emailis set (otherwise it throwsInvalidArgumentException: “Volare entity has no email configured. Set one at Markets → Volare Entity before sending the contract.”)
- The contract is in
ContractPdfGenerator::generate()renderspdf.supplier-contractBlade template via DomPDF. The agreement section contains two side-by-side signature blocks with text anchors:__SIGN_HERE_VOLARE__on the left and__SIGN_HERE__on the right- PDF is stored locally at
contracts/{id}/contract-{reference}.pdf SignaturitService::createSignatureRequest()uploads the PDF with two recipients andsigning_mode=sequential:recipients[0]— supplier, widget pinned to__SIGN_HERE__recipients[1]— Volare, widget pinned to__SIGN_HERE_VOLARE__
- Contract record is updated with
signaturit_signature_id,signaturit_document_id,signaturit_status, andsignaturit_sent_at - If the contract was in
Draft, it transitions toSent - Sequential signing: the supplier (index 0) is emailed first. Only after the supplier signs does Signaturit email the Volare countersigner (index 1)
- Each time a recipient signs, Signaturit sends
POST /api/webhooks/signaturit.SupplierContractService::handleSignaturitWebhook()routes it:document_signed— one recipient signed; routed tohandlePartialSignature(), which only logs. The contract stays atSent(no DB writes, no status transition, no PDF download)document_completed— every recipient has signed; routed tohandleDocumentSigned(), which downloads the fully countersigned PDF, stores it atcontracts/{id}/signed-{reference}.pdf, setssigned_at/signed_by, and transitions the contract toSigneddocument_declined— setssignaturit_statustodeclined, logs the eventdocument_canceled— setssignaturit_statustocanceled
Webhook Security
Section titled “Webhook Security”The webhook endpoint at POST /api/webhooks/signaturit uses optional token-based validation:
- If
SIGNATURIT_WEBHOOK_SECRETis set, the webhook URL includes?token={secret}and the controller rejects requests without a matching token - If no secret is configured, all requests are accepted (suitable for sandbox/local development)
- The webhook URL automatically excludes
events_urlwhen the URL containslocalhostto avoid Signaturit delivery errors
Filament UI Actions
Section titled “Filament UI Actions”The Edit Supplier Contract page (EditSupplierContract.php) provides header actions:
| Action | Visible When | Effect |
|---|---|---|
| Send for Signature | Draft/Sent, no pending signature, supplier has email, Volare entity has email | Generates PDF with both anchors, sends a single sequential request to Signaturit with supplier + Volare recipients. If the Volare entity has no email, surfaces a Filament warning notification: “Volare entity has no email configured. Set one at Markets → Volare Entity before sending the contract.” |
| Resend Email | Pending signature exists (hasPendingSignature()) | Calls POST /signatures/{id}/reminder.json — same signaturit_signature_id, no PDF re-upload; updates signaturit_sent_at |
| Cancel Signature | Pending signature exists | Cancels via Signaturit API |
| Download Contract PDF | contract_pdf_path is set | Downloads the generated PDF |
| Download Signed PDF | signed_pdf_path is set | Downloads the fully countersigned PDF (both certificates embedded) |
Source: backend/app/Filament/Resources/Suppliers/SupplierContracts/Pages/EditSupplierContract.php
Volare Entity
Section titled “Volare Entity”Contract PDFs include the company’s legal information from the VolareEntity model (single-row table volare_entities). It is also the source for the Volare countersigner in the e-signature flow. Managed via a dedicated Filament page at Markets > Volare Entity (admin-only).
Fields: legal_name, trade_name, tax_id, address, email, phone, website.
VolareEntity::current() returns the first (and only) row.
Source: backend/app/Models/VolareEntity.php, backend/app/Filament/Pages/VolareEntityPage.php
Contract Status Lifecycle
Section titled “Contract Status Lifecycle”Defined in SupplierContractStatus enum. The e-signature flow touches these transitions:
Draft -> Sent (on sendForSignature)Sent -> Signed (on document_completed webhook -- both parties have signed)A document_signed event (only one party has signed) does not advance the status. The contract stays at Sent until document_completed fires.
Full lifecycle: Draft -> Sent -> Negotiating -> Signed -> Active -> Expired/Terminated
Source: backend/app/Enums/SupplierContractStatus.php
Recipient Resolution
Section titled “Recipient Resolution”The signature request always carries two recipients.
Supplier resolution
Section titled “Supplier resolution”Resolved from the supplier in this priority order:
commercial_contact_name/commercial_contact_emailcontact_person/emailname(name only fallback)
If no email is available, sendForSignature() throws InvalidArgumentException (“Supplier must have an email address to receive the signature request.”).
Volare resolution
Section titled “Volare resolution”Resolved from VolareEntity::current():
- Email:
volareEntity->email(required — hard failure if missing) - Name:
trade_name→legal_name→'Volare'(fallback chain)
If VolareEntity::current() is null or its email is empty, sendForSignature() throws InvalidArgumentException: “Volare entity has no email configured. Set one at Markets → Volare Entity before sending the contract.” The Filament action catches this and surfaces it as a warning notification.
PDF Template
Section titled “PDF Template”The Blade template at backend/resources/views/pdf/supplier-contract.blade.php renders an “Agreement” section near the end with two side-by-side signature blocks inside a .signatures-table:
| Left column | Right column |
|---|---|
Title: Volare, anchor: __SIGN_HERE_VOLARE__ | Title: Supplier, anchor: __SIGN_HERE__ |
Each block is intentionally minimal — a small .signature-title label and a .sign-anchor containing only the anchor text (no name, title, or date lines). The anchors are hidden from view via the .sign-anchor { color: #ccc; font-size: 9px; } style but remain machine-readable so Signaturit can pin each recipient’s widget to the correct slot via word_anchor.
The @verbatim ... @endverbatim directive is used in Blade to keep the underscores raw.
Local Development
Section titled “Local Development”The webhook URL automatically excludes events_url when it contains localhost, so Signaturit will not call your machine. To exercise the finalization path locally, replay a webhook payload manually — and target document_completed (not document_signed), since document_signed only logs and document_completed is what flips the contract to Signed.
# Replay a document_completed event against your local appcurl -X POST 'http://localhost/api/webhooks/signaturit?token=YOUR_SECRET' \ -H 'Content-Type: application/json' \ -d '{ "type": "document_completed", "signature": { "id": "SIGNATURIT_SIGNATURE_ID" }, "document": { "id": "SIGNATURIT_DOCUMENT_ID", "email": "supplier@example.com" } }'Replace SIGNATURIT_SIGNATURE_ID / SIGNATURIT_DOCUMENT_ID with values from the contract row (signaturit_signature_id / signaturit_document_id). Drop the ?token= query if SIGNATURIT_WEBHOOK_SECRET is unset. Sending type: document_signed instead is also useful to confirm partial events are logged and do not finalize the contract.
Related
Section titled “Related”- Suppliers Service — Supplier data model and contracts
- Source:
backend/app/Services/Signaturit/SignaturitService.php - Source:
backend/app/Services/SupplierContractService.php - Blade template:
backend/resources/views/pdf/supplier-contract.blade.php