Turkish e-Invoicing with .NET: Integrating Hızlı Teknoloji e-Connect
e-Fatura vs e-Arşiv, SOAP clients that behave in modern .NET, UBL-TR mapping without tears, the status lifecycle, cancellation rules and a go-live checklist from real ERP integrations.
If you build software that issues invoices in Turkey, you don't get to choose whether to care about e-Fatura. Above the revenue thresholds, electronic invoicing through the Revenue Administration (GİB) is mandatory, the format is UBL-TR, and the practical route for almost everyone is a special integrator — a licensed provider whose API sits between your app and the government. Hızlı Teknoloji's e-Connect is one of the established players there, and this post is the integration guide I wish existed when I first wired an ERP into it: the .NET version, SOAP warts and all.
The lay of the land in five minutes
Two document families matter for a typical business app:
- e-Fatura — for customers who are themselves registered e-invoice users. Delivery happens over GİB's network, mailbox-to-mailbox; the invoice is "sent" when the system routes it, and B2B scenarios can involve accept/reject responses.
- e-Arşiv — for everyone else (consumers, unregistered companies). The invoice is issued electronically, reported to GİB, and delivered to the customer by e-mail or a link.
Which one applies is per recipient, decided at issue time by checking the recipient's tax number against the registered-user list. Every integrator, e-Connect included, exposes a taxpayer-check operation for exactly this. Your invoice flow therefore always starts with a lookup — bake that into your design, not as an afterthought.
Talking SOAP from modern .NET
e-Connect's integration surface is SOAP web services with credential-based authentication (the ecosystem even has an open-source community client — a PHP/Laravel package on GitHub — which is a decent map of the operation names even if you live in C#). In .NET, resist the urge to hand-craft envelopes; generate a typed client:
# One-time: generate a WCF client from the service WSDL
dotnet tool install --global dotnet-svcutil
dotnet-svcutil https://efatura.example-endpoint.com.tr/Service?wsdl \
--outputDir ./Generated --namespace "*,EConnect.Generated"
Then wrap the generated client behind your own interface. This is the single highest-value decision in the whole integration — your app should depend on IEInvoiceGateway, not on a WSDL's opinion of the world:
public interface IEInvoiceGateway
{
Task<RecipientType> CheckTaxpayerAsync(string vknTckn, CancellationToken ct);
Task<SendResult> SendInvoiceAsync(InvoiceDraft draft, CancellationToken ct);
Task<InvoiceStatus> GetStatusAsync(string uuid, CancellationToken ct);
Task<byte[]> DownloadPdfAsync(string uuid, CancellationToken ct);
}
public sealed class EConnectGateway(EConnectClientFactory factory, IOptions<EConnectOptions> opt)
: IEInvoiceGateway
{
public async Task<SendResult> SendInvoiceAsync(InvoiceDraft draft, CancellationToken ct)
{
using var client = factory.Create(); // fresh channel; WCF channels fault
var request = InvoiceMapper.ToUblTr(draft, opt.Value.SenderAlias);
var response = await client.SendInvoiceAsync(
opt.Value.Username, opt.Value.Password, request);
return response.Code == "0"
? SendResult.Accepted(response.Uuid, response.InvoiceNumber)
: SendResult.Rejected(response.Code, response.Message);
}
// ... remaining operations follow the same shape
}
WCF-specific advice earned the annoying way: treat channels as disposable (a faulted channel poisons every later call), set explicit binding timeouts (the defaults are optimistic), and log the raw request/response XML on failures via an IClientMessageInspector — when the integrator's support asks "can you send us the envelope?", you want to say yes in one minute, not reproduce the bug in three days.
The invoice itself: UBL-TR without tears
UBL-TR is XML with strong opinions — mandatory scenario codes, profile IDs (TICARIFATURA, EARSIVFATURA…), line-level tax breakdowns that must sum perfectly. The integrator validates before GİB does, and rejection messages reference UBL paths. Two habits keep this civilized:
- One mapper, heavily unit-tested. Money and VAT rounding live in exactly one place. Test the ugly cases: discounts, withholding (tevkifat), multiple VAT rates on one invoice, currency invoices with exchange rates.
- Store what you sent. Persist the full UBL you submitted, the returned UUID and invoice number, and every status transition. e-invoices are legal documents; "we can regenerate it from the order" does not satisfy an auditor whose question is about the document that went out.
Status is a lifecycle, not a boolean
DRAFT → QUEUED → SENT ─┬→ DELIVERED (e-Fatura, routed via GİB)
│ └→ ACCEPTED / REJECTED (commercial profile)
└→ REPORTED (e-Arşiv) → EMAILED
any state ──────→ ERROR (schema, mailbox, GİB outage…)
Poll status asynchronously (a Quartz.NET job every few minutes is a natural fit) and surface the state on your invoice screen. The failure modes worth explicit handling: recipient mailbox not found (their alias changed — re-check the taxpayer list), schema rejection (your mapper has a bug — alert loudly), and GİB maintenance windows (retry with patience; they happen, usually at month-end when everyone is invoicing).
Cancellation and its cousins
Here the two document types diverge sharply, and mixing them up creates accounting incidents: an e-Arşiv invoice can be cancelled through the integrator within its legal window; a basic-profile e-Fatura, once delivered, is effectively immutable — correction happens with a return invoice issued by the counterparty or a new corrective document. Encode these rules in your domain layer so the UI never offers a cancel button that the law doesn't.
A go-live checklist from the trenches
- Full rehearsal on the integrator's test environment — including PDF retrieval and cancellation, not just the happy send.
- Taxpayer check cached with a short TTL; the list changes, but not per-second.
- Idempotency on send: key on your own invoice id so a retry after timeout can't create a duplicate legal document.
- Credit/contour monitoring if your plan meters documents — running out of credits on the last day of the month is a classic.
- Every envelope logged (masked where needed) to central logging with the invoice UUID as correlation id.
I've integrated Turkish e-invoice flows into ERPs and SaaS products enough times to have opinions about UBL rounding. If e-Fatura is between you and a launch, bring it over — this is solved-problem territory.