Skip to content

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.

  • 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
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-pdf

Source files:

  • backend/app/Services/Signaturit/SignaturitService.php — API client (multi-recipient)
  • backend/app/Services/Signaturit/ContractPdfGenerator.php — PDF generation
  • backend/app/Services/Signaturit/SignaturitApiException.php — Error type
  • backend/app/Services/SupplierContractService.php — Orchestration (e-signature methods)
  • backend/app/Http/Controllers/Webhook/SignaturitWebhookController.php — Webhook handler
  • backend/app/Http/Controllers/Webhook/ContractPdfController.php — PDF download routes
  • backend/resources/views/pdf/supplier-contract.blade.php — PDF template with both anchors

Required env vars: SIGNATURIT_API_TOKEN, SIGNATURIT_BASE_URL

Optional env vars: SIGNATURIT_WEBHOOK_SECRET, SIGNATURIT_BRANDING_ID

Config file: config/services.php -> signaturit

VariablePurposeDefault
SIGNATURIT_API_TOKENBearer token for Signaturit API
SIGNATURIT_BASE_URLAPI base URL (sandbox or production)https://api.sandbox.signaturit.com/v3
SIGNATURIT_WEBHOOK_SECRETOptional token appended as ?token= query param for webhook validation
SIGNATURIT_BRANDING_IDOptional 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.

  1. Admin opens a contract in Filament (Edit Supplier Contract page) and clicks Send for Signature
  2. SupplierContractService::sendForSignature() validates that:
    • The contract is in Draft or Sent status
    • It has at least one service line item
    • The supplier resolves to an email address
    • VolareEntity::current()->email is set (otherwise it throws InvalidArgumentException: “Volare entity has no email configured. Set one at Markets → Volare Entity before sending the contract.”)
  3. ContractPdfGenerator::generate() renders pdf.supplier-contract Blade 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
  4. PDF is stored locally at contracts/{id}/contract-{reference}.pdf
  5. SignaturitService::createSignatureRequest() uploads the PDF with two recipients and signing_mode=sequential:
    • recipients[0] — supplier, widget pinned to __SIGN_HERE__
    • recipients[1] — Volare, widget pinned to __SIGN_HERE_VOLARE__
  6. Contract record is updated with signaturit_signature_id, signaturit_document_id, signaturit_status, and signaturit_sent_at
  7. If the contract was in Draft, it transitions to Sent
  8. Sequential signing: the supplier (index 0) is emailed first. Only after the supplier signs does Signaturit email the Volare countersigner (index 1)
  9. Each time a recipient signs, Signaturit sends POST /api/webhooks/signaturit. SupplierContractService::handleSignaturitWebhook() routes it:
    • document_signed — one recipient signed; routed to handlePartialSignature(), which only logs. The contract stays at Sent (no DB writes, no status transition, no PDF download)
    • document_completed — every recipient has signed; routed to handleDocumentSigned(), which downloads the fully countersigned PDF, stores it at contracts/{id}/signed-{reference}.pdf, sets signed_at/signed_by, and transitions the contract to Signed
    • document_declined — sets signaturit_status to declined, logs the event
    • document_canceled — sets signaturit_status to canceled

The webhook endpoint at POST /api/webhooks/signaturit uses optional token-based validation:

  • If SIGNATURIT_WEBHOOK_SECRET is 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_url when the URL contains localhost to avoid Signaturit delivery errors

The Edit Supplier Contract page (EditSupplierContract.php) provides header actions:

ActionVisible WhenEffect
Send for SignatureDraft/Sent, no pending signature, supplier has email, Volare entity has emailGenerates 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 EmailPending signature exists (hasPendingSignature())Calls POST /signatures/{id}/reminder.json — same signaturit_signature_id, no PDF re-upload; updates signaturit_sent_at
Cancel SignaturePending signature existsCancels via Signaturit API
Download Contract PDFcontract_pdf_path is setDownloads the generated PDF
Download Signed PDFsigned_pdf_path is setDownloads the fully countersigned PDF (both certificates embedded)

Source: backend/app/Filament/Resources/Suppliers/SupplierContracts/Pages/EditSupplierContract.php

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

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

The signature request always carries two recipients.

Resolved from the supplier in this priority order:

  1. commercial_contact_name / commercial_contact_email
  2. contact_person / email
  3. name (name only fallback)

If no email is available, sendForSignature() throws InvalidArgumentException (“Supplier must have an email address to receive the signature request.”).

Resolved from VolareEntity::current():

  • Email: volareEntity->email (required — hard failure if missing)
  • Name: trade_namelegal_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.

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 columnRight 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.

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.

Terminal window
# Replay a document_completed event against your local app
curl -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.

  • 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