Skip to content
SignYu
TemplatesPricingAPIDocsAboutBlogContact
  1. Home
  2. API
  3. Integrations
  4. PHP (Laravel)

Aadhaar eSign API in PHP with Laravel

Laravel's Http client sits on top of Guzzle and handles multipart uploads, Bearer tokens and JSON in a few lines. This guide builds a small SignYu service class, sends a document from a controller, and receives webhooks on an API route with the raw body.

Updated 2026-10-01

What you need

  • Laravel 10 or 11 (PHP 8.1 or newer).
  • A SignYu API key (starts with sk_live_). API access costs ₹999/month with a 3-day free trial; create the key under Developers in the dashboard.
  • Signature credits for your signers, from ₹15 per signature (₹15 per signature on packs of 10 or more). Sending uses one credit per signer.
  • A webhook endpoint added under Developers, and its signing secret.

Step 1: Configure credentials

Add the values to .env and expose them through config/services.php, so config caching works in production.

// .env
// SIGNYU_API_KEY=sk_live_your_api_key
// SIGNYU_WEBHOOK_SECRET=your_endpoint_secret

'signyu' => [
    'key' => env('SIGNYU_API_KEY'),
    'webhook_secret' => env('SIGNYU_WEBHOOK_SECRET'),
],

Step 2: A service class

attach() switches the request to multipart. Pass the content type as the fourth argument, otherwise Guzzle guesses it from the file name, which fails for files without a .pdf extension.

<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

class SignYu
{
    private function client(): PendingRequest
    {
        return Http::baseUrl('https://signyu.com/api/v1')
            ->withToken(config('services.signyu.key'))
            ->acceptJson()
            ->timeout(60);
    }

    public function sendForSignature(string $pdfPath, string $name, array $signers): array
    {
        $doc = $this->client()
            ->attach('file', file_get_contents($pdfPath), basename($pdfPath), ['Content-Type' => 'application/pdf'])
            ->post('/documents', ['name' => $name])
            ->throw()
            ->json();

        $this->client()
            ->post("/documents/{$doc['documentId']}/signers", ['signers' => $signers])
            ->throw();

        return $this->client()
            ->post("/documents/{$doc['documentId']}/send")
            ->throw()
            ->json();
    }

    public function get(string $documentId): array
    {
        return $this->client()->get("/documents/{$documentId}")->throw()->json();
    }
}

Step 3: Send from a controller and handle errors

throw() raises RequestException. Inspect the response status and the error field to decide what the user sees.

use App\Services\SignYu;
use Illuminate\Http\Client\RequestException;

public function send(Agreement $agreement, SignYu $signyu)
{
    try {
        $sent = $signyu->sendForSignature(
            storage_path("app/agreements/{$agreement->id}.pdf"),
            "Rental agreement #{$agreement->id}",
            [
                ['name' => $agreement->tenant_name, 'phone' => $agreement->tenant_phone, 'email' => $agreement->tenant_email],
                ['name' => $agreement->owner_name, 'phone' => $agreement->owner_phone, 'email' => $agreement->owner_email],
            ],
        );
    } catch (RequestException $e) {
        $error = $e->response->json('error');
        return match ($e->response->status()) {
            402 => back()->withErrors('Not enough eSign credits. Please top up.'),
            409 => back()->with('status', 'This agreement was already sent.'),
            401, 403 => abort(500, 'SignYu API key or subscription problem: ' . $error),
            default => throw $e,
        };
    }

    $agreement->update(['signyu_document_id' => $sent['documentId'], 'esign_status' => $sent['status']]);
    return back()->with('status', 'Sent for Aadhaar eSign.');
}

Step 4: Store the signed PDF

Download from downloadUrl with a fresh Http call that has no token, and save it to a private disk.

public function handle(SignYu $signyu): void
{
    $doc = $signyu->get($this->documentId);
    if ($doc['status'] !== 'COMPLETED') {
        return;
    }

    $pdf = Http::timeout(60)->get($doc['downloadUrl'])->throw()->body();
    Storage::disk('local')->put("signed/{$this->documentId}.pdf", $pdf);
}

Verify webhooks

Register the route in routes/api.php so the web CSRF middleware does not run (in Laravel 11, run php artisan install:api first). $request->getContent() returns the raw body; hash_hmac and hash_equals do the rest. Dispatch a queued job and return 200 straight away.

<?php

// routes/api.php
// Route::post('/webhooks/signyu', SignYuWebhookController::class);

namespace App\Http\Controllers;

use App\Jobs\ArchiveSignedAgreement;
use App\Models\Agreement;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class SignYuWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        $raw = $request->getContent();
        $expected = 'sha256=' . hash_hmac('sha256', $raw, config('services.signyu.webhook_secret'));

        if (! hash_equals($expected, (string) $request->header('X-SignSetu-Signature'))) {
            abort(400, 'invalid signature');
        }

        $event = json_decode($raw, true);
        $key = "signyu:{$event['documentId']}:{$event['event']}:" . ($event['signer']['signerId'] ?? '');

        if (Cache::add($key, true, now()->addDay())) {
            Agreement::where('signyu_document_id', $event['documentId'])
                ->update(['esign_status' => $event['status']]);

            if ($event['event'] === 'document.completed') {
                ArchiveSignedAgreement::dispatch($event['documentId']);
            }
        }

        return response()->noContent();
    }
}

Common mistakes

  • Putting the webhook in routes/web.php triggers CSRF protection and a 419 response. Use routes/api.php or exclude the path from CSRF.
  • Using $request->all() or $request->json() to rebuild the body changes it; always hash $request->getContent().
  • Passing the arguments to hash_equals in the wrong order still works, but passing null (missing header) throws a TypeError; cast the header to string.
  • Reusing the client() with withToken for the downloadUrl sends your API key to the storage host and the download fails.
  • Webhooks are retried when your endpoint fails or times out after 10 seconds, so the same event can arrive more than once. Key your processing on documentId plus event plus signerId and ignore repeats.

Frequently asked questions

Can I use plain Guzzle without Laravel?

Yes. Use a multipart option with name, contents, filename and headers set to Content-Type application/pdf, plus an Authorization: Bearer header. The Laravel Http client is a thin wrapper around the same thing.

Does this work on shared hosting?

Yes, as long as outbound HTTPS is allowed and your webhook URL is reachable over HTTPS. Without a queue worker, use the sync queue driver and keep the webhook handler light.

Which PHP version do I need?

The examples use match expressions and named arguments, so PHP 8.0 or newer; Laravel 10 itself needs PHP 8.1.

How do I test the webhook locally with Laravel Sail or Valet?

Valet's valet share or a tunnel like ngrok gives you a public HTTPS URL. Add it under Developers and send a real document to yourself.

API reference

  • Documents
  • Webhooks
  • Errors
  • Quickstart

Other integration guides

  • Aadhaar eSign in Django: Models, Views and Webhooks
  • Aadhaar eSign API in C# and ASP.NET Core
  • Add Aadhaar eSign to WordPress

Get your API key

Start a 3-day free trial of API access and send your first document today.

Start free trialSee API pricing and features
SignYu

Pay-per-use Aadhaar eSign for Indian businesses, landlords, and individuals. Sign PDFs in 2 minutes at ₹15 per signature.

LinkedIn →

Product

  • Aadhaar eSign
  • Pricing
  • Templates
  • Rent Agreement eSign
  • Verify Signature
  • eSign Quiz
  • API
  • API Docs

Company

  • About
  • eSign Guide
  • Blog
  • Press
  • FAQ
  • Contact
Powered by eMudhra (CCA-licensed ESP)·IT Act 2000 Compliant·Aadhaar OTP Authenticated·Made in India 🇮🇳

© 2026 BN Habitat Pvt Ltd·CIN: U45400CH2010PTC043443·GST: 03AAECB5185C1Z3

Regd. Office: H.NO. 3355, 2nd Floor, Sector 37-D, Chandigarh, Chandigarh - 160036

Op. Office: Office 34, 13th Floor, Sushma Infinium, Chandigarh Ambala Expressway, Zirakpur, Punjab - 140603

TermsPrivacyRefundCookie