Aadhaar eSign API in C# and ASP.NET Core
This guide uses a typed HttpClient registered through IHttpClientFactory for API calls and an ASP.NET Core minimal API endpoint for webhooks. The webhook code reads the request stream into bytes and checks the HMAC with CryptographicOperations.FixedTimeEquals.
Updated 2026-10-01
What you need
- .NET 8 SDK.
- 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: Register a typed client
Keep secrets in user-secrets locally and environment variables in production. Set the Bearer header per request rather than on DefaultRequestHeaders so the same client never leaks it to the PDF download host.
// dotnet user-secrets set "SignYu:ApiKey" "sk_live_your_api_key"
// dotnet user-secrets set "SignYu:WebhookSecret" "your_endpoint_secret"
builder.Services.AddHttpClient<SignYuClient>(c =>
{
c.BaseAddress = new Uri("https://signyu.com/api/v1/");
c.Timeout = TimeSpan.FromSeconds(60);
});Step 2: Create, add signers and send
Set the PDF part's ContentType explicitly; ByteArrayContent has none by default. Note the relative paths without a leading slash, so they resolve under BaseAddress.
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
public record Signer(string Name, string Phone, string Email);
public record CreatedDocument(string DocumentId, string Name, string Status);
public record SentSigner(string SignerId, string Name, int SigningOrder, string SignUrl);
public record SendResult(string DocumentId, string Status, int CreditsRemaining, List<SentSigner> Signers);
public class SignYuException(HttpStatusCode status, string? code, string? message) : Exception(message)
{
public HttpStatusCode Status { get; } = status;
public string? Code { get; } = code;
}
public class SignYuClient(HttpClient http, IConfiguration config)
{
private HttpRequestMessage Request(HttpMethod method, string path, HttpContent? content = null) =>
new(method, path)
{
Content = content,
Headers = { Authorization = new AuthenticationHeaderValue("Bearer", config["SignYu:ApiKey"]) }
};
public async Task<SendResult> SendForSignatureAsync(byte[] pdf, string name, IEnumerable<Signer> signers)
{
var file = new ByteArrayContent(pdf);
file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
using var form = new MultipartFormDataContent { { file, "file", "document.pdf" }, { new StringContent(name), "name" } };
var doc = await ReadAsync<CreatedDocument>(await http.SendAsync(Request(HttpMethod.Post, "documents", form)));
var payload = JsonContent.Create(new { signers = signers.Select(s => new { name = s.Name, phone = s.Phone, email = s.Email }) });
await ReadAsync<object>(await http.SendAsync(Request(HttpMethod.Post, "documents/" + doc.DocumentId + "/signers", payload)));
return await ReadAsync<SendResult>(await http.SendAsync(Request(HttpMethod.Post, "documents/" + doc.DocumentId + "/send")));
}
private static async Task<T> ReadAsync<T>(HttpResponseMessage res)
{
if (res.IsSuccessStatusCode) return (await res.Content.ReadFromJsonAsync<T>())!;
var err = await res.Content.ReadFromJsonAsync<Dictionary<string, string>>();
throw new SignYuException(res.StatusCode, err?.GetValueOrDefault("error"), err?.GetValueOrDefault("message"));
}
}Step 3: Handle the error codes
Catch SignYuException and branch on the status. 402 means you need credits, 409 means it was already sent.
try
{
var sent = await signyu.SendForSignatureAsync(pdfBytes, "Vendor agreement, Shree Logistics", signers);
return Results.Ok(new { sent.DocumentId, links = sent.Signers.Select(s => s.SignUrl) });
}
catch (SignYuException ex) when (ex.Status == HttpStatusCode.PaymentRequired)
{
return Results.Problem("Not enough eSign credits.", statusCode: 402);
}
catch (SignYuException ex) when (ex.Status == HttpStatusCode.Conflict)
{
return Results.Conflict("Already sent.");
}
catch (SignYuException ex) when (ex.Status is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
logger.LogError("SignYu auth failed: {Code}", ex.Code);
return Results.Problem("eSign is temporarily unavailable.");
}Verify webhooks
Copy Request.Body into a byte array before doing anything else, then compute HMACSHA256.HashData over those bytes. Convert.ToHexString returns uppercase, so lowercase it before comparing. CryptographicOperations.FixedTimeEquals gives a constant-time comparison.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
app.MapPost("/webhooks/signyu", async (HttpRequest request, IConfiguration config, IBackgroundQueue queue) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var raw = buffer.ToArray();
var secret = Encoding.UTF8.GetBytes(config["SignYu:WebhookSecret"]!);
var expected = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, raw)).ToLowerInvariant();
var received = request.Headers["X-SignSetu-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(received)))
return Results.BadRequest();
using var doc = JsonDocument.Parse(raw);
var evt = doc.RootElement.GetProperty("event").GetString();
var documentId = doc.RootElement.GetProperty("documentId").GetString()!;
if (evt == "document.completed")
await queue.EnqueueAsync(documentId); // download the signed PDF in a background service
return Results.Ok();
});Common mistakes
- Binding the webhook to a record parameter makes ASP.NET Core deserialize the stream first; take HttpRequest and read the bytes yourself.
- Comparing an uppercase hex digest with the lowercase header always fails. Call ToLowerInvariant().
- Setting DefaultRequestHeaders.Authorization on a shared HttpClient sends your key to the presigned download host, which rejects the request.
- Creating new HttpClient() per call exhausts sockets under load; use IHttpClientFactory as shown.
- 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 this work with .NET Framework 4.8?
The HttpClient calls do. HMACSHA256.HashData and Convert.ToHexString need .NET 5 or newer; on .NET Framework use new HMACSHA256(key).ComputeHash and a manual hex encoder.
Can I use controllers instead of a minimal API?
Yes. In a controller action read Request.Body the same way, and do not add a [FromBody] parameter.
Why do I get 400 invalid_file when the PDF is fine?
The multipart file part has no content type. Set file.Headers.ContentType to application/pdf.
Where should background work run?
In a hosted BackgroundService reading from a Channel, or a job library such as Hangfire. The webhook should return 200 within 10 seconds.
API reference
Get your API key
Start a 3-day free trial of API access and send your first document today.