Back to Blog
LaravelPHPDigital SignatureAPI

Laravel Digital Signature: How to Sign PDFs with a Certificate Using PDFSignify

PDFSignify TeamFebruary 14, 202610 min read

If you're building a Laravel application that needs to digitally sign PDF documents — contracts, invoices, compliance forms — you need a reliable way to apply cryptographic signatures using a digital certificate. PHP alone doesn't handle this well, but with the PDFSignify API, you can sign PDFs from Laravel in a single HTTP request.

This guide walks through a complete implementation: a Laravel service class, a controller, and all the configuration you need to start signing PDFs in production.

How PDFSignify Works

PDFSignify is a synchronous API. You send a PDF file and a .pfx/.p12 digital certificate, and you get back a signed PDF immediately. There are no webhooks, no document IDs, no stored files on PDFSignify's side. Your server sends the files, receives the result, and stores it however you want.

  • POST your PDF + certificate to the API
  • Receive the signed PDF binary in the response
  • Store the signed file in your Laravel storage
  • No async workflows, no polling, no callbacks

Prerequisites

  • A Laravel 10+ application
  • A PDFSignify account with your AccessKey and SecretKey
  • A .pfx or .p12 digital certificate file and its password
  • PHP 8.1+ with the HTTP client (built into Laravel)

Step 1: Configure Your Environment

Add your PDFSignify credentials to your .env file. Never hardcode API keys in your source code.

bash
PDFSIGNIFY_ACCESS_KEY=your_access_key_here
PDFSIGNIFY_SECRET_KEY=your_secret_key_here
PDFSIGNIFY_CERTIFICATE_PASSWORD=your_certificate_password

Step 2: Create a PDFSignify Service Class

Encapsulate the API interaction in a dedicated service class. This keeps your controllers clean and makes testing easier.

php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;

class PdfSignifyService
{
    private string $baseUrl = 'https://api.pdfsignify.com/api/v1';

    public function signPdf(
        string $pdfPath,
        string $certificatePath,
        array $options = []
    ): string {
        $response = Http::withHeaders([
            'AccessKey' => config('services.pdfsignify.access_key'),
            'SecretKey' => config('services.pdfsignify.secret_key'),
        ])
        ->attach('pdf', fopen($pdfPath, 'r'), 'document.pdf')
        ->attach(
            'certificate',
            fopen($certificatePath, 'r'),
            'certificate.pfx'
        )
        ->attach(
            'certificatePassword',
            '',
            null,
            ['Content-Type' => 'text/plain']
        )
        ->post($this->baseUrl . '/sign-pdf', array_merge([
            'certificatePassword' => config('services.pdfsignify.certificate_password'),
            'signaturePageAppearance' => $options['page'] ?? -1,
            'signatureMessage' => $options['message'] ?? 'Digitally signed',
            'signatureHeight' => $options['height'] ?? 100,
            'signatureWidth' => $options['width'] ?? 150,
            'signatureXPosition' => $options['x'] ?? 180,
            'signatureYPosition' => $options['y'] ?? 100,
        ], $options));

        if (!$response->successful()) {
            throw new \RuntimeException(
                'PDFSignify signing failed: ' . $response->body()
            );
        }

        return $response->body();
    }

    public function checkCertificatePassword(
        string $certificatePath,
        string $password
    ): bool {
        $response = Http::withHeaders([
            'AccessKey' => config('services.pdfsignify.access_key'),
            'SecretKey' => config('services.pdfsignify.secret_key'),
        ])
        ->attach(
            'certificate',
            fopen($certificatePath, 'r'),
            'certificate.pfx'
        )
        ->post($this->baseUrl . '/check-certificate-password', [
            'certificatePassword' => $password,
        ]);

        return $response->successful();
    }
}

Step 3: Add Service Configuration

Register your PDFSignify credentials in config/services.php so the service class can access them cleanly.

php
// config/services.php

'pdfsignify' => [
    'access_key' => env('PDFSIGNIFY_ACCESS_KEY'),
    'secret_key' => env('PDFSIGNIFY_SECRET_KEY'),
    'certificate_password' => env('PDFSIGNIFY_CERTIFICATE_PASSWORD'),
],

Step 4: Build the Controller

Create a controller that accepts a PDF upload, signs it via PDFSignify, and returns the signed file to the user.

php
<?php

namespace App\Http\Controllers;

use App\Services\PdfSignifyService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class SignDocumentController extends Controller
{
    public function __construct(
        private PdfSignifyService $pdfSignify
    ) {}

    public function sign(Request $request)
    {
        $request->validate([
            'pdf' => 'required|file|mimes:pdf|max:10240',
        ]);

        $pdfPath = $request->file('pdf')->store('uploads');
        $certPath = storage_path('app/certificates/company.pfx');

        $signedPdfContent = $this->pdfSignify->signPdf(
            storage_path('app/' . $pdfPath),
            $certPath,
            [
                'message' => 'Signed by ' . auth()->user()->name,
                'page' => -1,
            ]
        );

        $signedPath = 'signed/' . now()->timestamp . '_signed.pdf';
        Storage::put($signedPath, $signedPdfContent);

        Storage::delete($pdfPath);

        return response()->download(
            storage_path('app/' . $signedPath),
            'signed_document.pdf',
            ['Content-Type' => 'application/pdf']
        );
    }
}

Step 5: Define the Route

php
// routes/web.php
use App\Http\Controllers\SignDocumentController;

Route::post('/documents/sign', [SignDocumentController::class, 'sign'])
    ->middleware('auth');

Step 6: Build a Blade Upload Form

A minimal form that lets authenticated users upload a PDF and receive the signed version back.

html
<form action="/documents/sign" method="POST" enctype="multipart/form-data">
    @csrf
    <label for="pdf">Select PDF to sign:</label>
    <input type="file" name="pdf" id="pdf" accept=".pdf" required>
    <button type="submit">Sign Document</button>
</form>

Advanced: Bulk Signing with Laravel Queues

If you need to sign many documents at once (monthly invoices, batch contracts), dispatch each signing operation as a queued job. Even though the PDFSignify API is synchronous, running many requests in parallel through Laravel's queue system keeps your application responsive.

php
<?php

namespace App\Jobs;

use App\Services\PdfSignifyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Storage;

class SignDocumentJob implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(
        private string $pdfPath,
        private string $certPath,
        private array $options = []
    ) {}

    public function handle(PdfSignifyService $pdfSignify): void
    {
        $signed = $pdfSignify->signPdf(
            storage_path('app/' . $this->pdfPath),
            $this->certPath,
            $this->options
        );

        $signedPath = 'signed/' . basename($this->pdfPath);
        Storage::put($signedPath, $signed);
    }
}

Advanced: Setting PDF Metadata

PDFSignify also lets you set PDF metadata fields — author, title, keywords, and more. This is useful for compliance and document management systems.

php
$response = Http::withHeaders([
    'AccessKey' => config('services.pdfsignify.access_key'),
    'SecretKey' => config('services.pdfsignify.secret_key'),
])
->attach('pdf', fopen($pdfPath, 'r'), 'document.pdf')
->post('https://api.pdfsignify.com/api/v1/set-pdf-metadata', [
    'pdfMetadataAuthor' => 'Acme Corp',
    'pdfMetadataTitle' => 'Service Agreement',
    'pdfMetadataKeywords' => 'contract, agreement, 2026',
]);

$updatedPdf = $response->body();

Security Best Practices

  • Store your .pfx certificate outside the public directory — never in public/
  • Keep AccessKey and SecretKey in .env, never in version control
  • Validate uploaded files strictly (mimes:pdf, max size)
  • Delete temporary upload files after signing
  • Use HTTPS for all API calls (PDFSignify enforces this)
  • Restrict the signing route with authentication middleware

Error Handling

The most common errors are an incorrect certificate password, an invalid certificate file, or exceeding the file size limit. Always wrap the API call in a try-catch and provide clear feedback to the user.

php
try {
    $signedPdf = $this->pdfSignify->signPdf($pdfPath, $certPath);
} catch (\RuntimeException $e) {
    return back()->withErrors([
        'signing' => 'Failed to sign the document. Please try again.',
    ]);
}

Why This Approach Works

Unlike eSignature platforms that require webhooks, document IDs, signer email workflows, and complex state management, PDFSignify is a utility API. You send files in, you get a signed PDF back. This makes the Laravel integration remarkably simple — no background polling, no webhook routes, no status tracking tables.

Your Laravel app stays in full control of the workflow. You decide when to sign, where to store the result, and how to deliver it to the user.

The simplest architecture wins. Send the PDF, get it signed, store the result. That's the entire workflow.