Skip to content
SignYu
TemplatesPricingAPIDocsAboutBlogContact
  1. Home
  2. API
  3. Integrations
  4. Next.js

Add Aadhaar eSign to a Next.js App Router Project

In a Next.js App Router app the natural split is a server action that forwards the uploaded PDF to SignYu, and a route handler that receives webhooks. This guide uses plain fetch so it works on Next.js 15.1 and newer, and it covers the two Next.js specific traps: reading the raw body and caching.

Updated 2026-10-01

What you need

  • Next.js 15.1 or newer with the App Router (after() and async params are used below).
  • 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: Keep the key server side

Add the key and secret to .env.local without the NEXT_PUBLIC_ prefix, so they never reach the client bundle. Add the same variables in your hosting provider.

SIGNYU_API_KEY=sk_live_your_api_key
SIGNYU_WEBHOOK_SECRET=your_endpoint_secret

Step 2: A small API helper

One helper adds the Bearer header, disables the fetch cache, and turns error responses into exceptions that carry the API's error code.

import "server-only";

const BASE = "https://signyu.com/api/v1";

export class SignYuApiError extends Error {
  constructor(public status: number, public code: string, message: string) {
    super(message);
  }
}

export async function signyu<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(BASE + path, {
    ...init,
    cache: "no-store",
    headers: { ...init.headers, Authorization: "Bearer " + process.env.SIGNYU_API_KEY },
  });
  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new SignYuApiError(res.status, body.error ?? "unknown", body.message ?? res.statusText);
  }
  return body as T;
}

Step 3: Server action: upload, add signers, send

The browser posts the PDF to a server action, which forwards the File as multipart. Do not set a Content-Type header yourself; fetch adds the multipart boundary. Server actions have a default 1MB body limit, so raise serverActions.bodySizeLimit in next.config if your PDFs go up to the API's 10MB.

"use server";

import { redirect } from "next/navigation";
import { signyu, SignYuApiError } from "@/lib/signyu";

type Signer = { name: string; phone: string; email: string };

export async function sendForSignature(formData: FormData) {
  const file = formData.get("file");
  if (!(file instanceof File) || file.type !== "application/pdf") {
    return { error: "Please upload a PDF." };
  }

  const upload = new FormData();
  upload.append("file", file, file.name);
  upload.append("name", String(formData.get("title") || file.name));

  let nextUrl: string;
  try {
    const { documentId } = await signyu<{ documentId: string }>("/documents", {
      method: "POST",
      body: upload,
    });

    const signers: Signer[] = [
      { name: String(formData.get("clientName")), phone: String(formData.get("clientPhone")), email: String(formData.get("clientEmail")) },
      { name: "Priya Sharma", phone: "9800000001", email: "legal@yourcompany.in" },
    ];
    await signyu("/documents/" + documentId + "/signers", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ signers }),
    });

    await signyu("/documents/" + documentId + "/send", { method: "POST" });
    // Persist documentId with the contract row here.
    nextUrl = "/contracts/" + documentId;
  } catch (err) {
    if (err instanceof SignYuApiError && err.code === "insufficient_credits") {
      return { error: "Out of signature credits. Please top up and try again." };
    }
    throw err;
  }
  redirect(nextUrl); // outside try: redirect() works by throwing
}

Step 4: Show live status in a Server Component

Render the document's progress straight from the API. The helper already opts out of the fetch cache, so the page always shows the current signer state and a fresh downloadUrl.

import { signyu } from "@/lib/signyu";

type Doc = {
  status: "PENDING" | "SENT" | "COMPLETED";
  downloadUrl: string | null;
  signers: { signerId: string; name: string; hasSigned: boolean; signUrl: string | null }[];
};

export default async function ContractPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const doc = await signyu<Doc>("/documents/" + id);

  return (
    <main>
      <h1>Status: {doc.status}</h1>
      <ul>
        {doc.signers.map((s) => (
          <li key={s.signerId}>
            {s.name}: {s.hasSigned ? "signed" : "waiting"}
          </li>
        ))}
      </ul>
      {doc.downloadUrl && <a href={doc.downloadUrl}>Download signed PDF</a>}
    </main>
  );
}

Verify webhooks

Route handlers give you the untouched body through await req.text(). Read it once, verify it, then JSON.parse the same string. Pin the handler to the Node.js runtime so node:crypto is available, and use after() to run follow-up work once the 200 has been sent.

import crypto from "node:crypto";
import { after } from "next/server";

export const runtime = "nodejs";

export async function POST(req: Request) {
  const raw = await req.text();
  const header = req.headers.get("x-signsetu-signature") ?? "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.SIGNYU_WEBHOOK_SECRET!).update(raw).digest("hex");

  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response("invalid signature", { status: 400 });
  }

  const event = JSON.parse(raw) as {
    event: "signer.signed" | "document.completed";
    documentId: string;
    signer?: { signerId: string };
  };

  after(async () => {
    if (event.event === "document.completed") {
      await archiveSignedPdf(event.documentId); // GET /documents/{id}, then download
    }
  });

  return new Response("ok");
}

Common mistakes

  • Calling req.json() before verifying loses the original bytes. Use req.text() once and parse that string.
  • If your middleware or proxy file protects /api/*, exclude the webhook path in its matcher or SignYu receives a redirect to your sign in page.
  • Never put the key in a NEXT_PUBLIC_ variable or call the API from a Client Component.
  • Server actions reject request bodies over 1MB by default; raise serverActions.bodySizeLimit for larger PDFs.
  • The downloadUrl is a temporary presigned storage link. Download it with a plain GET and no Authorization header (sending your Bearer key to it makes the request fail), and do not store the URL itself; call GET /api/v1/documents/{id} again for a fresh one.
  • 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

Should I use a server action or a route handler to create documents?

A server action is simplest for a form in your own UI. Use a route handler if a mobile app or another service needs to trigger signing over HTTP.

Does this work on the Edge runtime?

The API calls do, because they only use fetch. The webhook example uses node:crypto, so keep that route on the Node.js runtime, or port the HMAC to the Web Crypto API.

Why does my status page show old data?

Older Next.js versions cache fetch responses by default. Pass cache: "no-store" on API calls, as the helper above does.

Can I use the signyu npm package instead of fetch?

Yes. It works in server actions and route handlers. This guide uses fetch to show the raw requests; the Node.js guide shows the SDK.

API reference

  • Quickstart
  • Documents
  • Webhooks
  • Authentication

Other integration guides

  • Aadhaar eSign API in Node.js with Express
  • 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