A clean, modern .NET SDK for the Egyptian Tax Authority (ETA) e-invoicing APIs — submit signed e-invoices, track their validation status, cancel/reject documents, and search your document history.
Disclaimer: community SDK, not affiliated with or endorsed by the Egyptian Tax Authority. API shapes follow the official ETA SDK documentation. Always verify against the official docs before production use.
dotnet add package EtaInvoice.NetTargets netstandard2.0 and net8.0. Only dependency: System.Text.Json (+ DI abstractions).
using EtaInvoice;
using EtaInvoice.Models;
var client = new EtaClient(new EtaClientOptions
{
ClientId = "<your-client-id>", // from the ETA portal (register your ERP)
ClientSecret = "<your-client-secret>",
// BaseUrl/AuthUrl default to the preprod environment
});
var document = new EtaDocument
{
Issuer = new Taxpayer
{
Type = TaxpayerTypes.Business,
Id = "123456789", // tax registration number
Name = "My Company",
Address = new TaxpayerAddress
{
BranchId = "0",
Country = "EG",
Governate = "Cairo",
RegionCity = "Nasr City",
Street = "Main St",
BuildingNumber = "10"
}
},
Receiver = new Taxpayer
{
Type = TaxpayerTypes.Business,
Id = "987654321",
Name = "Buyer Co",
Address = new TaxpayerAddress
{
Country = "EG", Governate = "Giza", RegionCity = "Dokki",
Street = "Side St", BuildingNumber = "5"
}
},
DocumentType = DocumentTypes.Invoice, // "i"
DocumentTypeVersion = "1.0",
DateTimeIssued = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
TaxpayerActivityCode = "1234",
InternalId = "INV-2026-0001",
InvoiceLines = new List<InvoiceLine>
{
new InvoiceLine
{
Description = "Consulting service",
ItemType = "EGS",
ItemCode = "EG-1234",
UnitType = "EA",
Quantity = 1,
UnitValue = new UnitValue { CurrencySold = "EGP", AmountEGP = 1000m },
SalesTotal = 1000m,
Total = 1140m,
TaxableItems = new List<TaxableItem>
{
new TaxableItem { TaxType = "T1", SubType = "V009", Amount = 140m, Rate = 14m }
}
}
},
TotalSalesAmount = 1000m,
NetAmount = 1000m,
TaxTotals = new List<TaxTotal> { new TaxTotal { TaxType = "T1", Amount = 140m } },
TotalAmount = 1140m
};
// Optional: catch structural mistakes before the network call.
var problems = EtaInvoice.Validation.EtaDocumentValidator.Validate(document);
// Submit. Pass an IDocumentSigner to attach a CAdES-BES issuer signature
// (required in production — see "Signing" below).
var result = await client.SubmitDocumentsAsync(new[] { document }, signer: mySigner);
Console.WriteLine(result.SubmissionUuid);
foreach (var accepted in result.AcceptedDocuments)
Console.WriteLine($"accepted: {accepted.InternalId} -> {accepted.Uuid}");
foreach (var rejected in result.RejectedDocuments)
Console.WriteLine($"rejected: {rejected.InternalId}: {rejected.Error.Code}");Cancel/reject requests between issuer and receiver go through a request/decline cycle on ETA. The SDK covers both sides:
// Issuer cancels a document (within the ETA cancellation window).
await client.CancelDocumentAsync(documentUuid, "Wrong buyer details");
// Receiver rejects a received document (within the ETA rejection window).
await client.RejectDocumentAsync(documentUuid, "Received incorrect invoice");
// Receiver objects to the issuer's cancellation request.
await client.DeclineCancellationAsync(documentUuid);
// Issuer objects to the receiver's rejection — the document returns to `valid`.
await client.DeclineRejectionAsync(documentUuid);// Quick paged listing of recently sent/received documents.
var recent = await client.GetRecentDocumentsAsync(new RecentDocumentsQuery
{
PageNo = 1,
PageSize = 20,
Direction = "Sent", // "Sent" or "Received"
Status = "Valid"
});
foreach (var doc in recent.Result)
Console.WriteLine($"{doc.InternalId} -> {doc.Status} ({doc.Total} EGP)");
Console.WriteLine($"page 1 of {recent.Metadata.TotalPages}, {recent.Metadata.TotalCount} total");Prefer
SearchDocumentsAsyncfor new integrations — ETA documents it as the better-performing replacement with more filters and no record-count limits.
services.AddEtaInvoice(options =>
{
options.ClientId = builder.Configuration["Eta:ClientId"];
options.ClientSecret = builder.Configuration["Eta:ClientSecret"];
});
// then inject EtaClient anywhereAll methods target the /api/v1.0/… endpoints documented in the official ETA SDK.
| Method | Endpoint |
|---|---|
GetAccessTokenAsync() |
OAuth2 client-credentials (/connect/token, scope InvoicingAPI); token cached until expiry |
SubmitDocumentsAsync(documents, signer?) |
POST /api/v1.0/documentsubmissions/ → 202 {submissionUUID, acceptedDocuments, rejectedDocuments} |
GetSubmissionAsync(uuid, pageNo?, pageSize?) |
GET /api/v1.0/documentsubmissions/{uuid} |
GetDocumentDetailsAsync(uuid) |
GET /api/v1.0/documents/{uuid}/details (status + validation results) |
GetDocumentRawAsync(uuid) |
GET /api/v1.0/documents/{uuid}/raw (source JSON + ETA metadata) |
GetDocumentPdfAsync(uuid) |
GET /api/v1.0/documents/{uuid}/pdf → byte[] |
CancelDocumentAsync(uuid, reason) |
PUT /api/v1.0/documents/state/{uuid}/state (cancelled) |
RejectDocumentAsync(uuid, reason) |
PUT /api/v1.0/documents/state/{uuid}/state (rejected) |
DeclineCancellationAsync(uuid) |
PUT /api/v1.0/documents/state/{uuid}/decline/cancelation |
DeclineRejectionAsync(uuid) |
PUT /api/v1.0/documents/state/{uuid}/decline/rejection (issuer objects to a receiver's rejection) |
SearchDocumentsAsync(query) |
GET /api/v1.0/documents/search (paged via metadata.continuationToken) |
GetRecentDocumentsAsync(query?) |
GET /api/v1.0/documents/recent (paged via metadata.totalPages/totalCount; ETA recommends SearchDocumentsAsync for new integrations) |
Document types: DocumentTypes.Invoice (i), CreditNote (c), DebitNote (d), ImportInvoice (ii), ExportInvoice (ei), ExportCreditNote (ec), ExportDebitNote (ed).
Errors surface as EtaApiException with StatusCode, ErrorCode (e.g. DuplicateSubmission, IncorrectSubmitter) and the raw response body.
Real ETA signing requires your eSeal certificate (USB token / HSM / key vault) producing a CAdES-BES signature over the canonical document JSON. Private key material must never leave your infrastructure, so this SDK ships no built-in signer — instead:
- Implement
IDocumentSignerwith your signing service (USB token middleware, HSM, cloud vault). - Pass it to
SubmitDocumentsAsync(documents, signer)— the SDK canonicalizes each document (deterministic JSON: sorted keys, no whitespace,signaturessection excluded, per ETA's "signed content is the entire document except the signature section" rule) and embeds the returned Base64 value as the issuer (I) signature. - Verify the canonical string against the official ETA "Signature Creation" documentation (or the official EInvoicingSigner tracing tool) before going live — canonicalization details are defined by ETA.
Submitting without a signer is fine for preprod experimentation; production ETA rejects unsigned B2B documents.
Defaults point at preprod (api.preprod.invoicing.eta.gov.eg, id.preprod.eta.gov.eg). For production use EtaClientOptions.Production(clientId, clientSecret).
- ✅ Verified against the official ETA SDK docs (Oct 2026): auth, submit, submission status, document details/raw/PDF, cancel, reject, decline-cancellation, decline-rejection, search, recent documents.
⚠️ IDocumentSigneris an interface — you bring the real CAdES-BES signing (USB token/HSM). No fake crypto anywhere.⚠️ Client-side validation (EtaDocumentValidator) catches structural mistakes only; ETA server-side validation is authoritative.- Not covered (yet): EGS/GS1 code management APIs, document packages, notification callbacks, e-receipt (POS) APIs. Issues and PRs welcome.
| Type | What it does |
|---|---|
EtaDocumentValidator.Validate(document) |
Client-side structural checks (required fields, taxpayer formats, tax uniqueness, totals) — returns a list of problems, empty when OK |
EtaCanonicalJson.ToCanonicalJson(document) |
Deterministic JSON (sorted keys, no whitespace, signatures excluded) — the exact input your IDocumentSigner receives |
IDocumentSigner |
Interface you implement with your eSeal/HSM/USB-token signing to produce the Base64 CAdES-BES issuer signature |
SearchDocumentsQuery / RecentDocumentsQuery |
Filter/paging models for the two list APIs |
EtaClientOptions.Production(clientId, clientSecret) |
Pre-built options pointing at the production environment |
MIT — see LICENSE.
