Skip to content

.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


Terminal window
# No hay NuGet extra — usa System.Net.Http y System.Security.Cryptography incluidos
dotnet add package Microsoft.Extensions.Http

Variables en appsettings.json:

{
"Emitia": {
"ApiKey": "sk_test_TU_CLAVE",
"WebhookSecret": "whsec_...",
"BaseUrl": "https://api.emitia.co/v1",
"Version": "2026-01-01"
}
}

Models/EmitiaModels.cs
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);

Services/EmitiaService.cs
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");
}
}

Program.cs
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();

Controllers/WebhookController.cs
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;
}
});
}
}