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

Aadhaar eSign API in Node.js with Express

This guide uses the official signyu package from npm to upload a PDF, add signers and send it for Aadhaar OTP eSign from a Node.js service. The webhook part uses Express, where the one thing that usually goes wrong is the body parser, so we cover that in detail.

Updated 2026-10-01

What you need

  • Node.js 18 or newer (the SDK uses the built-in fetch).
  • 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: Install the SDK and set your key

Install signyu and express, and keep the API key and webhook secret in environment variables. Never ship the key to a browser bundle; every call below runs on your server.

npm install signyu express

export SIGNYU_API_KEY=sk_live_your_api_key
export SIGNYU_WEBHOOK_SECRET=your_endpoint_secret

Step 2: Upload the PDF

documents.create accepts a Buffer, Uint8Array, ArrayBuffer or Blob, not a file path or stream, so read the file first. The document starts in PENDING state. Keep the returned documentId in your database next to your own record.

import { readFile } from "node:fs/promises";
import { SignYu } from "signyu";

const signyu = new SignYu({ apiKey: process.env.SIGNYU_API_KEY });

const doc = await signyu.documents.create({
  file: await readFile("./contracts/service-agreement.pdf"),
  fileName: "service-agreement.pdf",
  name: "Service Agreement, Acme Traders",
});

console.log(doc.documentId, doc.status); // "PENDING"

Step 3: Add signers in signing order

Signers get their signingOrder from the order you pass them. Phone must be digits only (at least 10) and email is required. A document holds up to 6 signers.

await signyu.documents.addSigners(doc.documentId, [
  { name: "Asha Rao", phone: "9876543210", email: "asha@example.com" },
  { name: "Vikram Nair", phone: "9812345678", email: "vikram@example.com" },
]);

Step 4: Send and keep the signing links

send deducts one credit per signer, emails every signer their link, and returns a signUrl per signer. Store those links if you want to show a Sign now button in your own app or share them over SMS.

const sent = await signyu.documents.send(doc.documentId);

console.log("Credits left:", sent.creditsRemaining);
for (const s of sent.signers) {
  // Persist s.signUrl against your own user or order record.
  console.log(s.signingOrder, s.name, s.signUrl);
}

Step 5: Handle API errors by code

The SDK throws SignYuError with the HTTP status and the API's error code. Branch on err.code rather than on message text.

import { SignYuError } from "signyu";

try {
  await signyu.documents.send(documentId);
} catch (err) {
  if (!(err instanceof SignYuError)) throw err;

  switch (err.code) {
    case "insufficient_credits": // 402: nothing was sent, top up and retry
      await notifyBilling(err.message);
      break;
    case "invalid_state": // 409: already sent, safe to treat as done
      break;
    case "unauthorized": // 401: key missing, wrong or deleted
    case "api_access_not_enabled": // 403: API subscription inactive
      throw new Error("Check SIGNYU_API_KEY and the API subscription");
    default:
      throw err;
  }
}

Step 6: Fetch the signed PDF

Once the document is COMPLETED, documents.get returns a downloadUrl. Fetch it without your API key and write the bytes to disk or object storage.

import { writeFile } from "node:fs/promises";

const latest = await signyu.documents.get(documentId);
if (latest.status === "COMPLETED" && latest.downloadUrl) {
  const res = await fetch(latest.downloadUrl); // no Authorization header
  if (!res.ok) throw new Error("Download failed: " + res.status);
  await writeFile("./signed/" + documentId + ".pdf", Buffer.from(await res.arrayBuffer()));
}

Verify webhooks

SignYu signs the exact bytes it sends. In Express that means the webhook route must receive a Buffer, so mount express.raw on that route only and register express.json after it. Compare signatures with crypto.timingSafeEqual, return 200 quickly, and do slow work (downloading the PDF, sending emails) after responding.

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/signyu",
  express.raw({ type: "application/json" }), // req.body is a Buffer here
  (req, res) => {
    const header = req.get("X-SignSetu-Signature") || "";
    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", process.env.SIGNYU_WEBHOOK_SECRET)
        .update(req.body)
        .digest("hex");

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

    const event = JSON.parse(req.body.toString("utf8"));
    res.sendStatus(200);

    if (event.event === "signer.signed") {
      markSignerSigned(event.documentId, event.signer.signerId);
    } else if (event.event === "document.completed") {
      queueSignedPdfDownload(event.documentId);
    }
  }
);

// JSON parsing for the rest of the app goes AFTER the webhook route.
app.use(express.json());

app.listen(3000);

Common mistakes

  • A global app.use(express.json()) registered before the webhook route consumes the stream, req.body becomes an object, and every signature check fails. Keep express.raw on the webhook route.
  • Passing a path string or fs.createReadStream to documents.create does not work; read the file into a Buffer first.
  • Send is one way. A second POST to /send returns 409 invalid_state, and signers cannot be added after sending. Add every signer (up to 6) before you call send.
  • 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

Does the signyu package work with TypeScript?

Yes. It ships its own types, so responses like doc.documentId and sent.signers are typed. The examples here are plain ESM JavaScript, but they run unchanged in a .ts file.

Can I use it with CommonJS require?

Use a dynamic import or switch the project to ESM. Top-level await in the examples also needs an ES module, so wrap the calls in an async function in CommonJS.

Why does my webhook signature never match in Express?

Almost always because a JSON body parser ran first. The HMAC is computed over the raw bytes; re-serializing the parsed object changes spacing or key order and produces a different hash.

Can I call the API from a browser or React Native app?

No. The API key would be exposed. Call SignYu from your Node backend and pass only the signUrl to the client.

API reference

  • Quickstart
  • Documents
  • Webhooks
  • Errors

Other integration guides

  • Add Aadhaar eSign to a Next.js App Router Project
  • Aadhaar eSign API in Python with FastAPI Webhooks
  • Aadhaar eSign Automation with n8n

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