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

Aadhaar eSign in Django: Models, Views and Webhooks

This guide wires the SignYu REST API into a Django project the Django way: a model that tracks each signature request, a service module that talks to the API with requests, and a webhook view that verifies the HMAC on request.body. It uses plain HTTP so you can see every request.

Updated 2026-10-01

What you need

  • Django 4.2 or newer and the requests library.
  • 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: Settings and model

Read the key and secret from the environment in settings.py. A small model stores the documentId and status so your app never has to poll for history.

# settings.py
# SIGNYU_API_KEY = os.environ["SIGNYU_API_KEY"]
# SIGNYU_WEBHOOK_SECRET = os.environ["SIGNYU_WEBHOOK_SECRET"]

from django.db import models


class SignatureRequest(models.Model):
    document_id = models.CharField(max_length=64, unique=True)
    status = models.CharField(max_length=16, default="PENDING")
    signed_pdf = models.FileField(upload_to="signed/", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)


class ProcessedWebhook(models.Model):
    """Deduplicates retried webhook deliveries."""
    key = models.CharField(max_length=200, unique=True)

Step 2: Service module with requests

Send the PDF as multipart with an explicit application/pdf content type in the files tuple. raise_for_status alone hides the API's error code, so wrap failures in your own exception.

import os

import requests
from django.conf import settings

BASE = "https://signyu.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {settings.SIGNYU_API_KEY}"


class SignYuError(Exception):
    def __init__(self, status, code, message):
        super().__init__(message)
        self.status, self.code = status, code


def _check(resp):
    if resp.ok:
        return resp.json()
    data = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
    raise SignYuError(resp.status_code, data.get("error"), data.get("message", resp.text))


def send_for_signature(django_file, name, signers):
    django_file.seek(0)
    doc = _check(session.post(
        f"{BASE}/documents",
        files={"file": (os.path.basename(django_file.name), django_file, "application/pdf")},
        data={"name": name},
        timeout=60,
    ))
    _check(session.post(f"{BASE}/documents/{doc['documentId']}/signers",
                        json={"signers": signers}, timeout=30))
    return _check(session.post(f"{BASE}/documents/{doc['documentId']}/send", timeout=30))

Step 3: Call it from a view

Save the SignatureRequest after send succeeds. Map 402 to a friendly message for staff instead of a 500.

from django.contrib import messages
from django.shortcuts import redirect

from .models import SignatureRequest
from .signyu import SignYuError, send_for_signature


def send_offer(request, candidate_id):
    candidate = Candidate.objects.get(pk=candidate_id)
    try:
        sent = send_for_signature(
            candidate.offer_pdf,
            f"Offer letter, {candidate.full_name}",
            [{"name": candidate.full_name, "phone": candidate.mobile, "email": candidate.email}],
        )
    except SignYuError as e:
        if e.code == "insufficient_credits":
            messages.error(request, "Not enough eSign credits. Ask an admin to top up.")
            return redirect("candidate-detail", candidate_id)
        raise
    SignatureRequest.objects.create(document_id=sent["documentId"], status=sent["status"])
    messages.success(request, "Offer sent for Aadhaar eSign.")
    return redirect("candidate-detail", candidate_id)

Verify webhooks

request.body holds the raw bytes, so verify against it before touching request.POST or json.loads. The view must be csrf_exempt because SignYu cannot send your CSRF token; the HMAC check replaces it. A unique key per event makes retries harmless.

import hashlib
import hmac
import json

from django.conf import settings
from django.db import IntegrityError, transaction
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .models import ProcessedWebhook, SignatureRequest


@csrf_exempt
@require_POST
def signyu_webhook(request):
    expected = "sha256=" + hmac.new(
        settings.SIGNYU_WEBHOOK_SECRET.encode(), request.body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(request.headers.get("X-SignSetu-Signature", ""), expected):
        return HttpResponseBadRequest("invalid signature")

    event = json.loads(request.body)
    signer_id = (event.get("signer") or {}).get("signerId", "")
    key = f"{event['documentId']}:{event['event']}:{signer_id}"

    try:
        with transaction.atomic():
            ProcessedWebhook.objects.create(key=key)
            SignatureRequest.objects.filter(document_id=event["documentId"]).update(
                status=event["status"]
            )
    except IntegrityError:
        pass  # duplicate delivery

    if event["event"] == "document.completed":
        archive_signed_pdf.delay(event["documentId"])  # e.g. a Celery task
    return HttpResponse("ok")

Common mistakes

  • Reading request.POST or request.data (in Django REST Framework) before request.body raises RawPostDataException or gives you parsed data. Verify on request.body first.
  • Forgetting csrf_exempt makes Django return 403 to every webhook, and SignYu keeps retrying.
  • A Django FileField that was already read once is at the end of the stream; call seek(0) before uploading it.
  • The file part must be sent with Content-Type application/pdf. A part labelled application/octet-stream is rejected with 400 invalid_file even if the bytes are a valid PDF.
  • 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.

Frequently asked questions

Can I use Django REST Framework for the webhook?

Yes, but read request.body before accessing request.data, and disable authentication and CSRF on that view with authentication_classes = [] and permission_classes = [AllowAny]. The HMAC is the authentication.

Should the API calls run in the request cycle?

Create and send are quick enough for a view. Downloading the signed PDF after document.completed belongs in Celery, RQ or Django Q so the webhook returns within 10 seconds.

Where should I store signed PDFs?

A private storage backend such as a non-public S3 bucket via django-storages. Signed agreements contain personal data and should not sit under a public MEDIA_URL.

Can I use the signyu Python SDK instead of requests?

Yes. The Python guide shows it. This guide uses requests so the multipart and error handling are visible.

API reference

  • Documents
  • Webhooks
  • Errors
  • Authentication

Other integration guides

  • Aadhaar eSign API in Python with FastAPI Webhooks
  • Aadhaar eSign API in PHP with Laravel
  • Aadhaar eSign API in Java with Spring Boot

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