August 4, 2026 · Tutorial
Add E-Signatures to ASP.NET Core & C# Applications via REST API (2026)
Stop struggling with 50MB legacy enterprise SDKs, brittle COM interop, or labyrinthine OAuth JWT envelope builders. Learn how to build a clean, typed e-signature client in C# and .NET 8, verify HMAC-SHA256 webhooks using constant-time cryptography, and persist legally binding audit trails in Entity Framework Core.
Founder, Signbee
ARCHITECTURAL OVERVIEW
When evaluating the best ASP.NET Core signature in the market for enterprise C# systems, modern software engineering teams prioritize zero dependency bloat, native System.Text.Json performance, seamless dependency injection via IHttpClientFactory, and tamper-proof cryptographic webhook validation. In this tutorial, we construct a production-ready e-signature workflow in .NET 8 without touching bloated third-party vendor libraries.
Why .NET Developers Are Moving Away from Legacy SDKs
For years, enterprise .NET teams needing document signing were forced into vendor SDKs like DocuSign or Adobe Sign. These packages bring significant architectural baggage:
- Massive dependency footprints: Bloated NuGet packages that pull in outdated dependencies and complicate security vulnerability audits.
- Complex authentication schemes: Cumbersome RSA private key parsing, OAuth 2.0 JWT assertion handshakes, and token cache refresh loops spanning hundreds of lines of boilerplate.
- Rigid coordinate-based templates: Manually positioning signature tabs with pixel X/Y offsets rather than generating dynamic documents from clean Markdown or HTML templates.
With the Signbee REST API, document dispatch is reduced to a single HTTP POST request. By combining native C# 12 records, HttpClient, and ASP.NET Core minimal APIs or controllers, you achieve complete two-party legally binding signing flows with full ESIGN, eIDAS, and UETA compliance in a fraction of the code.
| Feature / Metric | Signbee + .NET 8 REST | DocuSign / Legacy SDKs |
|---|---|---|
| Package Dependencies | 0 NuGet packages (Native .NET 8) | Heavy SDK (40+ MB transitive tree) |
| Authentication | Standard Bearer Token API Key | OAuth 2.0 JWT / Consent Flows |
| Template Engine | Dynamic Markdown with variables | Rigid PDF coordinate tab placement |
| Lines of C# for Basic Send | ~18 lines | 85–120 lines |
| Webhook Verification | Timing-safe HMAC-SHA256 header | DocuSign Connect XML / HMAC hashing |
Step 1: Configuration and Options Pattern
In idiomatic ASP.NET Core development, external service settings should be strongly typed using the Options Pattern. First, define your configuration model and bind it to appsettings.json.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Signbee": {
"ApiKey": "sb_live_your_actual_api_key_here",
"WebhookSecret": "whsec_your_webhook_signing_secret_here",
"BaseUrl": "https://signb.ee/api/v1/",
"TimeoutSeconds": 15
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=contracts_db;Username=postgres;Password=secret"
}
}Next, create the corresponding C# record to represent the options binding:
namespace AspNetCoreEsignature.Configuration;
public sealed record SignbeeOptions
{
public const string SectionName = "Signbee";
public string ApiKey { get; init; } = string.Empty;
public string WebhookSecret { get; init; } = string.Empty;
public string BaseUrl { get; init; } = "https://signb.ee/api/v1/";
public int TimeoutSeconds { get; init; } = 15;
}Step 2: Strongly-Typed C# Models with System.Text.Json
We define immutable C# 12 records for our API requests, responses, and webhook payloads using System.Text.Json.Serialization attributes. This ensures optimal serialization performance and Native AOT compatibility in .NET 8.
using System.Text.Json.Serialization;
namespace AspNetCoreEsignature.Models;
public sealed record SendDocumentRequest
{
[JsonPropertyName("markdown")]
public required string Markdown { get; init; }
[JsonPropertyName("recipient_name")]
public required string RecipientName { get; init; }
[JsonPropertyName("recipient_email")]
public required string RecipientEmail { get; init; }
[JsonPropertyName("title")]
public string? Title { get; init; }
[JsonPropertyName("webhook_url")]
public string? WebhookUrl { get; init; }
[JsonPropertyName("expires_in_days")]
public int? ExpiresInDays { get; init; } = 30;
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; init; }
}
public sealed record SendDocumentResponse
{
[JsonPropertyName("document_id")]
public string DocumentId { get; init; } = string.Empty;
[JsonPropertyName("signing_url")]
public string SigningUrl { get; init; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; init; }
}
public sealed record DocumentStatusResponse
{
[JsonPropertyName("document_id")]
public string DocumentId { get; init; } = string.Empty;
[JsonPropertyName("status")]
public string Status { get; init; } = string.Empty;
[JsonPropertyName("recipient_name")]
public string RecipientName { get; init; } = string.Empty;
[JsonPropertyName("recipient_email")]
public string RecipientEmail { get; init; } = string.Empty;
[JsonPropertyName("signed_pdf_url")]
public string? SignedPdfUrl { get; init; }
[JsonPropertyName("sha256_hash")]
public string? Sha256Hash { get; init; }
[JsonPropertyName("signed_at")]
public DateTimeOffset? SignedAt { get; init; }
}
public sealed record SignbeeWebhookPayload
{
[JsonPropertyName("event")]
public string Event { get; init; } = string.Empty;
[JsonPropertyName("timestamp")]
public DateTimeOffset Timestamp { get; init; }
[JsonPropertyName("data")]
public required WebhookData Data { get; init; }
}
public sealed record WebhookData
{
[JsonPropertyName("document_id")]
public string DocumentId { get; init; } = string.Empty;
[JsonPropertyName("signer_name")]
public string SignerName { get; init; } = string.Empty;
[JsonPropertyName("signer_email")]
public string SignerEmail { get; init; } = string.Empty;
[JsonPropertyName("signed_pdf_url")]
public string? SignedPdfUrl { get; init; }
[JsonPropertyName("sha256_certificate_hash")]
public string? Sha256CertificateHash { get; init; }
[JsonPropertyName("ip_address")]
public string? IpAddress { get; init; }
[JsonPropertyName("user_agent")]
public string? UserAgent { get; init; }
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; init; }
}Step 3: Typed HttpClient Service with Dependency Injection
Rather than instantiating new HttpClient() directly (which leads to socket exhaustion under heavy load), ASP.NET Core provides typed clients via IHttpClientFactory. Here is our interface and service implementation:
using AspNetCoreEsignature.Models;
namespace AspNetCoreEsignature.Services;
public interface ISignbeeClient
{
Task<SendDocumentResponse> SendDocumentAsync(
SendDocumentRequest request,
CancellationToken cancellationToken = default);
Task<DocumentStatusResponse> GetDocumentStatusAsync(
string documentId,
CancellationToken cancellationToken = default);
Task<byte[]> DownloadSignedPdfAsync(
string documentId,
CancellationToken cancellationToken = default);
}Now we implement SignbeeClient with robust error inspection, status code handling, and rate limit awareness:
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using AspNetCoreEsignature.Configuration;
using AspNetCoreEsignature.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AspNetCoreEsignature.Services;
public sealed class SignbeeClient : ISignbeeClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<SignbeeClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public SignbeeClient(
HttpClient httpClient,
IOptions<SignbeeOptions> options,
ILogger<SignbeeClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var config = options.Value;
_httpClient.BaseAddress = new Uri(config.BaseUrl.TrimEnd('/') + "/");
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", config.ApiKey);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.Timeout = TimeSpan.FromSeconds(config.TimeoutSeconds);
}
public async Task<SendDocumentResponse> SendDocumentAsync(
SendDocumentRequest request,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Sending contract for signature to {RecipientEmail}...", request.RecipientEmail);
using var response = await _httpClient.PostAsJsonAsync("send", request, JsonOptions, cancellationToken);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponseAsync(response, cancellationToken);
}
var result = await response.Content.ReadFromJsonAsync<SendDocumentResponse>(JsonOptions, cancellationToken);
return result ?? throw new InvalidOperationException("Signbee API returned an empty response.");
}
public async Task<DocumentStatusResponse> GetDocumentStatusAsync(
string documentId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(documentId);
using var response = await _httpClient.GetAsync($"documents/{documentId}", cancellationToken);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponseAsync(response, cancellationToken);
}
var result = await response.Content.ReadFromJsonAsync<DocumentStatusResponse>(JsonOptions, cancellationToken);
return result ?? throw new InvalidOperationException("Signbee API returned an empty status payload.");
}
public async Task<byte[]> DownloadSignedPdfAsync(
string documentId,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(documentId);
using var response = await _httpClient.GetAsync($"documents/{documentId}/download", cancellationToken);
if (!response.IsSuccessStatusCode)
{
await HandleErrorResponseAsync(response, cancellationToken);
}
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
}
private async Task HandleErrorResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
{
var rawContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5;
_logger.LogWarning("Signbee rate limit encountered (429). Retry after {Seconds}s.", retryAfter);
throw new HttpRequestException($"Signbee rate limit exceeded. Retry after {retryAfter}s.", null, HttpStatusCode.TooManyRequests);
}
_logger.LogError("Signbee API call failed with status {StatusCode}: {ErrorBody}", response.StatusCode, rawContent);
throw new HttpRequestException($"Signbee API error ({response.StatusCode}): {rawContent}", null, response.StatusCode);
}
}If you are also building microservices in other languages or orchestrating cross-platform workers, see our companion tutorials for Node.js e-signature integration and Python e-signature API integration.
Step 4: Register Services in Program.cs with Polly Resilience
In .NET 8, register the typed client in Program.cs and configure transient retry handling via Microsoft.Extensions.Http.Resilience or standard Polly handlers:
using AspNetCoreEsignature.Configuration;
using AspNetCoreEsignature.Data;
using AspNetCoreEsignature.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// 1. Bind Strongly-Typed Options
builder.Services.Configure<SignbeeOptions>(
builder.Configuration.GetSection(SignbeeOptions.SectionName));
// 2. Register EF Core DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// 3. Register Typed HttpClient with Resilience
builder.Services.AddHttpClient<ISignbeeClient, SignbeeClient>()
.ConfigureHttpClient((sp, client) =>
{
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();Step 5: Timing-Safe Webhook Controller with HMAC-SHA256 Verification
When a recipient views or signs a document, Signbee posts an event payload to your webhook URL. To prevent spoofing and man-in-the-middle tampering, Signbee signs every HTTP POST request with an HMAC-SHA256 signature in the X-Signbee-Signature header.
Never use standard string comparison (== or string.Equals) to validate HMAC signatures. Doing so leaks timing data that allows attackers to determine valid bytes one at a time. Always use System.Security.Cryptography.CryptographicOperations.FixedTimeEquals. Learn more in our Best E-Signature API Webhooks Guide (2026).
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using AspNetCoreEsignature.Configuration;
using AspNetCoreEsignature.Data;
using AspNetCoreEsignature.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AspNetCoreEsignature.Controllers;
[ApiController]
[Route("webhooks/signbee")]
public sealed class SignbeeWebhookController : ControllerBase
{
private readonly AppDbContext _dbContext;
private readonly SignbeeOptions _options;
private readonly ILogger<SignbeeWebhookController> _logger;
public SignbeeWebhookController(
AppDbContext dbContext,
IOptions<SignbeeOptions> options,
ILogger<SignbeeWebhookController> logger)
{
_dbContext = dbContext;
_options = options.Value;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> HandleWebhookAsync(
[FromHeader(Name = "X-Signbee-Signature")] string? signatureHeader,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(signatureHeader))
{
_logger.LogWarning("Webhook request rejected: Missing X-Signbee-Signature header.");
return Unauthorized("Missing signature header.");
}
// Enable buffering so the request body stream can be read for HMAC verification
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync(cancellationToken);
Request.Body.Position = 0;
if (!IsSignatureValid(rawBody, signatureHeader, _options.WebhookSecret))
{
_logger.LogWarning("Webhook request rejected: HMAC signature verification failed.");
return Unauthorized("Invalid webhook signature.");
}
var payload = JsonSerializer.Deserialize<SignbeeWebhookPayload>(rawBody);
if (payload is null)
{
return BadRequest("Invalid JSON payload.");
}
_logger.LogInformation("Processing Signbee webhook event '{Event}' for document {DocumentId}",
payload.Event, payload.Data.DocumentId);
// Fetch contract from EF Core
var contract = await _dbContext.Contracts
.FirstOrDefaultAsync(c => c.DocumentId == payload.Data.DocumentId, cancellationToken);
if (contract is null)
{
_logger.LogWarning("Contract with DocumentId {DocumentId} not found in database.", payload.Data.DocumentId);
// Return 200 to acknowledge webhook even if local entity is missing
return Ok(new { status = "ignored", reason = "contract_not_found" });
}
switch (payload.Event)
{
case "document.viewed":
contract.Status = ContractStatus.Viewed;
contract.LastViewedAtUtc = payload.Timestamp.UtcDateTime;
break;
case "document.signed":
contract.Status = ContractStatus.Signed;
contract.SignedAtUtc = payload.Timestamp.UtcDateTime;
contract.SignedPdfUrl = payload.Data.SignedPdfUrl;
contract.Sha256CertificateHash = payload.Data.Sha256CertificateHash;
contract.SignerIpAddress = payload.Data.IpAddress;
contract.AuditTrailJson = rawBody;
_logger.LogInformation("Contract {Id} successfully marked as SIGNED.", contract.Id);
break;
case "document.declined":
contract.Status = ContractStatus.Declined;
break;
default:
_logger.LogInformation("Unhandled event type: {Event}", payload.Event);
break;
}
await _dbContext.SaveChangesAsync(cancellationToken);
return Ok(new { status = "processed", eventType = payload.Event });
}
private static bool IsSignatureValid(string payload, string incomingSignatureHex, string secret)
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA256(secretBytes);
var computedHash = hmac.ComputeHash(payloadBytes);
var computedHex = Convert.ToHexString(computedHash).ToLowerInvariant();
var incomingBytes = Encoding.UTF8.GetBytes(incomingSignatureHex.Trim().ToLowerInvariant());
var expectedBytes = Encoding.UTF8.GetBytes(computedHex);
// Constant-time comparison protects against side-channel timing attacks
return CryptographicOperations.FixedTimeEquals(incomingBytes, expectedBytes);
}
}Step 6: Entity Framework Core Data Model and Audit Trails
For legal compliance under the US ESIGN Act, Uniform Electronic Transactions Act (UETA), and EU eIDAS Regulation, electronic agreements require an immutable audit trail with cryptographic tamper-evidence. Here is our EF Core database structure:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AspNetCoreEsignature.Data;
public enum ContractStatus
{
Draft = 0,
PendingSignature = 1,
Viewed = 2,
Signed = 3,
Declined = 4,
Expired = 5
}
[Table("contracts")]
public sealed class ContractEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(100)]
public string DocumentId { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Title { get; set; } = string.Empty;
[Required]
[MaxLength(150)]
public string RecipientName { get; set; } = string.Empty;
[Required]
[EmailAddress]
[MaxLength(255)]
public string RecipientEmail { get; set; } = string.Empty;
[Required]
public ContractStatus Status { get; set; } = ContractStatus.Draft;
[MaxLength(500)]
public string? SigningUrl { get; set; }
[MaxLength(500)]
public string? SignedPdfUrl { get; set; }
[MaxLength(64)]
public string? Sha256CertificateHash { get; set; }
[MaxLength(45)]
public string? SignerIpAddress { get; set; }
public string? AuditTrailJson { get; set; }
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? LastViewedAtUtc { get; set; }
public DateTime? SignedAtUtc { get; set; }
}And the AppDbContext configuration with indexes:
using Microsoft.EntityFrameworkCore;
namespace AspNetCoreEsignature.Data;
public sealed class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<ContractEntity> Contracts => Set<ContractEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ContractEntity>(entity =>
{
entity.HasIndex(e => e.DocumentId).IsUnique();
entity.HasIndex(e => e.RecipientEmail);
entity.HasIndex(e => e.Status);
entity.Property(e => e.Status).HasConversion<string>();
});
}
}Step 7: Putting It All Together in a Contract Controller
Now we build an ASP.NET Core API controller that assembles Markdown agreement terms from database data, calls ISignbeeClient, and returns the real-time signing link to the frontend:
using AspNetCoreEsignature.Data;
using AspNetCoreEsignature.Models;
using AspNetCoreEsignature.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace AspNetCoreEsignature.Controllers;
[ApiController]
[Route("api/contracts")]
public sealed class ContractsController : ControllerBase
{
private readonly ISignbeeClient _signbeeClient;
private readonly AppDbContext _dbContext;
private readonly ILogger<ContractsController> _logger;
public ContractsController(
ISignbeeClient signbeeClient,
AppDbContext dbContext,
ILogger<ContractsController> logger)
{
_signbeeClient = signbeeClient;
_dbContext = dbContext;
_logger = logger;
}
[HttpPost("send-nda")]
public async Task<IActionResult> SendNdaAsync(
[FromBody] CreateNdaRequest request,
CancellationToken cancellationToken)
{
// 1. Generate dynamic contract Markdown
var markdownContent = $"""
# Master Non-Disclosure Agreement
**Disclosing Party:** Acme Technologies Corp
**Receiving Party:** {request.RecipientName} ({request.CompanyName})
**Effective Date:** {DateTime.UtcNow:MMMM dd, yyyy}
## 1. Confidential Information
The Receiving Party agrees to preserve the confidentiality of all proprietary source code,
architecture diagrams, and strategic plans disclosed by Acme Technologies Corp.
## 2. Term & Governing Law
This Agreement shall remain binding for a period of two (2) years and is governed by
the laws of the State of Delaware.
""";
// 2. Dispatch document via Signbee API
var sendRequest = new SendDocumentRequest
{
Title = $"NDA - {request.CompanyName}",
RecipientName = request.RecipientName,
RecipientEmail = request.RecipientEmail,
Markdown = markdownContent,
WebhookUrl = "https://your-domain.com/webhooks/signbee",
Metadata = new Dictionary<string, string>
{
["company_id"] = request.CompanyId.ToString(),
["internal_ref"] = Guid.NewGuid().ToString("N")
}
};
var response = await _signbeeClient.SendDocumentAsync(sendRequest, cancellationToken);
// 3. Save initial record in EF Core
var contract = new ContractEntity
{
DocumentId = response.DocumentId,
Title = sendRequest.Title,
RecipientName = request.RecipientName,
RecipientEmail = request.RecipientEmail,
Status = ContractStatus.PendingSignature,
SigningUrl = response.SigningUrl,
CreatedAtUtc = DateTime.UtcNow
};
_dbContext.Contracts.Add(contract);
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Dispatched contract {DocumentId} to {RecipientEmail}",
response.DocumentId, request.RecipientEmail);
return Ok(new
{
contractId = contract.Id,
documentId = response.DocumentId,
signingUrl = response.SigningUrl,
status = contract.Status.ToString()
});
}
}
public sealed record CreateNdaRequest(
string RecipientName,
string RecipientEmail,
string CompanyName,
Guid CompanyId);Verifying Signed Document Cryptography
When a document is completed, Signbee calculates a SHA-256 hash across the final rendered PDF, recipient digital signature strokes, timestamp tokens, and IP geolocation logs. For deep dives into how digital signatures are verified, review our guide on Electronic Signature Audit Trails and How SHA-256 Signing Certificates Work.
In your C# application, you can independently verify that the signed PDF stored in your storage bucket matches the cryptographic certificate hash emitted in the webhook:
using System.Security.Cryptography;
using System.Text;
namespace AspNetCoreEsignature.Services;
public static class SignatureVerificationService
{
public static bool VerifyPdfHash(byte[] pdfBytes, string expectedSha256Hex)
{
var actualHash = SHA256.HashData(pdfBytes);
var actualHex = Convert.ToHexString(actualHash).ToLowerInvariant();
var actualSpan = Encoding.UTF8.GetBytes(actualHex);
var expectedSpan = Encoding.UTF8.GetBytes(expectedSha256Hex.ToLowerInvariant());
return CryptographicOperations.FixedTimeEquals(actualSpan, expectedSpan);
}
}Best Practices Checklist for .NET E-Signature Integrations
Prevents TCP socket exhaustion, automatically handles DNS refreshes, and standardizes bearer auth headers across all outbound requests.
Use CryptographicOperations.FixedTimeEquals to guard against microsecond timing vulnerabilities.
Store the raw webhook payload along with IP addresses and SHA-256 certificate digests to guarantee admissibility in court under ESIGN and eIDAS.
Return HTTP 200 OK within 2 seconds. Offload intensive operations like S3 file archival to background workers (e.g. BackgroundService or Hangfire).
Frequently Asked Questions
How does integrating e-signatures via REST API in ASP.NET Core compare to using vendor-specific .NET SDKs?
Integrating e-signatures into ASP.NET Core through a lightweight REST API avoids the heavy dependency footprint, complex OAuth 2.0 token management, and version lock-in associated with traditional vendor SDKs like DocuSign or Adobe Sign. Heavy vendor SDKs often drag dozens of transient NuGet dependencies, force multi-step envelope and tab coordinate calculations, and require hundreds of lines of boilerplate code. In contrast, an API-first REST approach with Signbee leverages native .NET 8 primitives—such asIHttpClientFactory, System.Text.Json source generation, and record types—allowing developers to dispatch signed documents in fewer than 20 lines of C# without third-party package dependencies. This pattern integrates seamlessly with standard ASP.NET Core dependency injection, health checks, Polly resilience policies, and distributed OpenTelemetry tracing.
Why is CryptographicOperations.FixedTimeEquals critical for ASP.NET Core webhook HMAC signature validation?
When verifying incoming HMAC-SHA256 webhook signatures from an e-signature provider, standard string equality comparisons (such as string.Equals or ==) evaluate characters sequentially and terminate at the first mismatching byte. This early-exit optimization creates measurable microsecond latency differences that attackers can exploit via side-channel timing attacks to reconstruct valid signatures byte-by-byte. By utilizing System.Security.Cryptography.CryptographicOperations.FixedTimeEquals, ASP.NET Core inspects byte spans in constant time regardless of where or whether differences exist. This guarantees that your webhook endpoint remains mathematically impervious to timing analysis while validating webhook payloads, protecting your contract state transitions and database records against spoofing and tampering.
How should Entity Framework Core and background workers manage e-signature state transitions and immutable audit trails?
In high-throughput ASP.NET Core enterprise architectures, contract state transitions should follow an asynchronous, event-driven model using Entity Framework Core (EF Core) combined with background processing (such as IHostedService, BackgroundService, or Wolverine/Hangfire). When an outbound document is generated, EF Core creates a ContractEntity in 'PendingSignature' state. When Signbee delivers a 'document.signed' webhook event, the controller validates the cryptographic HMAC signature, applies optimistic concurrency, updates the entity state to 'Signed', and stores the SHA-256 certificate hash, signer IP, and audit trail JSON payload in PostgreSQL or SQL Server. Offloading PDF downloads and external CRM syncs to background workers ensures the webhook endpoint responds with HTTP 200 OK within milliseconds, avoiding timeout retries and ensuring legal compliance under ESIGN and eIDAS.
Add production-grade e-signatures to your ASP.NET Core app in minutes — 5 free docs/month, no SDK required.
Last updated: August 4, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.