.NET (ASP.NET Core)
Integración con ASP.NET Core 8+. Usa HttpClient con IHttpClientFactory para
las llamadas a Emitia y un ApiController para recibir webhooks.
Emitia-Version: 2026-01-01
Instalación
Section titled “Instalación”# No hay NuGet extra — usa System.Net.Http y System.Security.Cryptography incluidosdotnet add package Microsoft.Extensions.HttpVariables en appsettings.json:
{ "Emitia": { "ApiKey": "sk_test_TU_CLAVE", "WebhookSecret": "whsec_...", "BaseUrl": "https://api.emitia.co/v1", "Version": "2026-01-01" }}Modelos
Section titled “Modelos”namespace MyApp.Models;
public record EmitiaCustomer(string Id, string LegalName);public record EmitiaInvoice(string Id, string Status, string? Cufe);
public record CreateCustomerRequest( string OrganizationType, CustomerIdentification Identification, string LegalName, string Email);
public record CustomerIdentification(string Type, string Number, string? CheckDigit);
public record CreateInvoiceRequest( string CustomerId, string Currency, string OperationType, PaymentMeans PaymentMeans, List<InvoiceLine> Lines, Dictionary<string, string>? Metadata = null);
public record PaymentMeans(string Code, string Type, string? DueDate = null);
public record InvoiceLine( string Description, string Quantity, string UnitCode, string UnitPrice, string LineExtensionAmount, List<TaxLine> Taxes);
public record TaxLine(string Code, string Rate, string Base, string Value);Servicio Emitia
Section titled “Servicio Emitia”using System.Net.Http.Json;using System.Text.Json;using System.Text.Json.Serialization;using MyApp.Models;
namespace MyApp.Services;
public class EmitiaService(HttpClient client){ private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, };
/// <summary>Verifica autenticación (funciona hoy).</summary> public async Task<object> GetAccountAsync() { var res = await client.GetAsync("/account"); res.EnsureSuccessStatusCode(); return await res.Content.ReadFromJsonAsync<object>() ?? throw new Exception("Empty response"); }
/// <summary>Crea un cliente. Disponible con api-resources.</summary> public async Task<EmitiaCustomer> CreateCustomerAsync(CreateCustomerRequest request) { var idempotencyKey = Guid.NewGuid().ToString(); using var req = new HttpRequestMessage(HttpMethod.Post, "/customers") { Content = JsonContent.Create(request, options: JsonOptions), Headers = { { "Idempotency-Key", idempotencyKey } }, }; var res = await client.SendAsync(req); res.EnsureSuccessStatusCode(); return await res.Content.ReadFromJsonAsync<EmitiaCustomer>(JsonOptions) ?? throw new Exception("Empty response"); }
/// <summary> /// Crea una factura. Disponible con api-resources + test-mode-sandbox. /// El caller debe generar y persistir idempotencyKey ANTES de llamar. /// </summary> public async Task<EmitiaInvoice> CreateInvoiceAsync( CreateInvoiceRequest request, string idempotencyKey) { using var req = new HttpRequestMessage(HttpMethod.Post, "/invoices") { Content = JsonContent.Create(request, options: JsonOptions), Headers = { { "Idempotency-Key", idempotencyKey } }, }; var res = await client.SendAsync(req); res.EnsureSuccessStatusCode(); return await res.Content.ReadFromJsonAsync<EmitiaInvoice>(JsonOptions) ?? throw new Exception("Empty response"); }}Registro en DI
Section titled “Registro en DI”using MyApp.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<EmitiaService>(client =>{ client.BaseAddress = new Uri( builder.Configuration["Emitia:BaseUrl"] ?? "https://api.emitia.co/v1"); client.DefaultRequestHeaders.Add( "Authorization", $"Bearer {builder.Configuration["Emitia:ApiKey"]}"); client.DefaultRequestHeaders.Add( "Emitia-Version", builder.Configuration["Emitia:Version"] ?? "2026-01-01");});
builder.Services.AddControllers();var app = builder.Build();app.MapControllers();app.Run();Controlador de webhook
Section titled “Controlador de webhook”using System.Security.Cryptography;using System.Text;using System.Text.Json;using Microsoft.AspNetCore.Mvc;
namespace MyApp.Controllers;
[ApiController][Route("webhooks")]public class WebhookController(IConfiguration config) : ControllerBase{ [HttpPost("emitia")] public async Task<IActionResult> HandleEmitia() { // Leer el body como texto crudo ANTES de deserializar Request.EnableBuffering(); using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true); var rawBody = await reader.ReadToEndAsync();
var signature = Request.Headers["Emitia-Signature"].FirstOrDefault() ?? string.Empty; var secret = config["Emitia:WebhookSecret"] ?? string.Empty;
if (!VerifySignature(rawBody, signature, secret)) return Unauthorized(new { error = "Firma inválida" });
var evt = JsonSerializer.Deserialize<JsonElement>(rawBody); var eventType = evt.GetProperty("type").GetString();
// Procesar de forma asincrónica _ = ProcessEventAsync(eventType, evt);
return Ok(new { received = true }); }
private static bool VerifySignature(string rawBody, string signature, string secret, int toleranceSec = 300) { var parts = signature.Split(',') .Select(p => p.Split('=', 2)) .Where(p => p.Length == 2) .ToDictionary(p => p[0], p => p[1]);
if (!parts.TryGetValue("t", out var tStr) || !parts.TryGetValue("v1", out var v1)) return false;
if (!long.TryParse(tStr, out var t)) return false;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(now - t) > toleranceSec) return false;
var payload = $"{t}.{rawBody}"; var keyBytes = Encoding.UTF8.GetBytes(secret); var msgBytes = Encoding.UTF8.GetBytes(payload); var expected = Convert.ToHexString(HMACSHA256.HashData(keyBytes, msgBytes)).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(v1)); }
private static Task ProcessEventAsync(string? eventType, JsonElement evt) { return Task.Run(() => { var data = evt.GetProperty("data").GetProperty("object"); var invoiceId = data.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
switch (eventType) { case "invoice.accepted_by_dian": var cufe = data.TryGetProperty("cufe", out var cufeEl) ? cufeEl.GetString() : null; Console.WriteLine($"Factura aceptada: {invoiceId} CUFE: {cufe}"); break; case "invoice.rejected_by_dian": Console.Error.WriteLine($"Factura rechazada: {invoiceId}"); break; } }); }}Qué sigue
Section titled “Qué sigue”- Conceptos: Webhooks — detalles de la verificación HMAC
- Conceptos: Idempotencia — el patrón de retry seguro
- Guía de mapeo POS — códigos DIAN para tu sistema