Ethiopia EIMS Python Client
A framework-agnostic Python client for the Ethiopian Ministry of Revenue (MoR) Electronic Invoice Management System (EIMS).
It lets any ERP, POS, or backend register invoices, receipts, and
query/verify/cancel documents against the EIMS API. The package speaks
plain dataclasses and returns result objects — it does not touch your
database or ORM. Mapping your domain (sales, customers, products) to an
InvoiceRequest and persisting results is your application's job.
Credit: this is a Python port of, and was entirely inspired by,
mattdi/ethiopia-eims — the
original PHP client. All credit for the API design, the EIMS request/response
shapes, and the groundwork of figuring out how to talk to EIMS at all belongs
to that project; this port exists to bring the same client to Python
services, with the wire format (JSON shapes, field names, enum values) kept
identical between the two so the two clients can be mixed across a system
without surprises.
Status:
0.x, API may change. Ethiopian e-invoicing is pre-mandate; the request/response shapes reflect the current EIMS API as reverse engineered from the PHP package and may evolve. There is no official EIMS API specification backing this client — treat the shapes here as a best-effort draft, and validate against a real sandbox before relying on this in production.
Requirements
- Python 3.10+
httpx(installed automatically)
Installation
pip install ethiopia-eims
For RSA signing / AES encryption support (experimental, see below):
pip install "ethiopia-eims[signing]"
Usage
import os
from eims import (
EimsClient,
EimsCredentials,
SellerConfig,
BuyerDetails,
InvoiceItem,
InvoiceRequest,
ValueDetails,
DocumentDetails,
SourceSystem,
)
from eims.enums import TransactionType
client = EimsClient(
credentials=EimsCredentials(
client_id=os.environ["EIMS_CLIENT_ID"],
client_secret=os.environ["EIMS_CLIENT_SECRET"],
api_key=os.environ["EIMS_API_KEY"],
base_url="http://core.mor.gov.et",
),
seller=SellerConfig(
tin="0001234567",
legal_name="Acme Pharmacy PLC",
region_code="14",
city="Addis Ababa",
vat_number="VAT-123",
),
# cache=my_cache, # optional, see "Token caching" below
# cache_namespace="store_42", # optional; defaults to the TIN
)
result = client.register(
InvoiceRequest(
buyer=BuyerDetails.walk_in(),
items=[
InvoiceItem(
product_description="Paracetamol 500mg",
quantity=2,
unit_price=25.00,
pre_tax_value=50.00,
tax_amount=0.00,
total_line_amount=50.00,
),
],
values=ValueDetails(total_value=50.00, tax_value=0.00),
document=DocumentDetails(document_number=1001, date="02-10-2026T10:00:00"),
source_system=SourceSystem(invoice_counter=1001, system_number="SYS-1"),
transaction_type=TransactionType.B2C,
)
)
print(result.irn, result.qr_code, result.signed_invoice)
Building invoices the easy way
Invoice + InvoiceItem.make() compute line totals and the ValueDetails
for you:
from eims.data import Invoice, InvoiceItem, BuyerDetails
from eims.enums import TransactionType, PaymentMode, DocumentType
invoice = (
Invoice.for_buyer(BuyerDetails.walk_in())
.document(1001, "02-10-2026T10:00:00", DocumentType.INVOICE)
.source(1001, system_number="SYS-1")
.transaction_type(TransactionType.B2C)
.payment(PaymentMode.CASH)
.add_item(InvoiceItem.make("Paracetamol 500mg", quantity=2, unit_price=25.00, tax_rate=0.15))
.add_item(InvoiceItem.make("Vitamin C", quantity=1, unit_price=20.00, tax_rate=0.15, discount=5.00))
.build() # raises EimsConfigurationException if missing items / document / source
)
result = client.register(invoice)
InvoiceItem.make() returns an immutable InvoiceItem where pre_tax_value,
tax_amount, and total_line_amount are derived from the inputs.
Token caching
Pass anything implementing get(key, default=None) / set(key, value, ttl=None)
as cache to persist OAuth tokens across processes (the default is an
in-memory, per-process cache). This matches the shape of Django's cache
framework and most third-party caches (diskcache, a thin redis wrapper,
etc.) — adapt yours with a few lines if it doesn't already match.
Resilience: logging & retries
Pass a standard library logging.Logger and EimsCredentials(max_retries=..., retry_delay_ms=...) to see every request/response and retry transient
connection failures / 5xx responses with exponential backoff:
import logging
from eims import EimsCredentials
from eims.support.http_client_factory import make as make_http_client
credentials = EimsCredentials(
client_id="...", client_secret="...", api_key="...",
base_url="http://core.mor.gov.et",
max_retries=2, retry_delay_ms=500,
)
http = make_http_client(credentials, logging.getLogger("eims"))
client = EimsClient(credentials=credentials, seller=seller, http=http)
EimsCredentials.validate() fails fast if any required value is missing.
Testing your integration
eims.testing.FakeEimsClient records calls and lets you assert on them
(assertions use plain assert, so they work with pytest or any other
runner):
from eims.testing import FakeEimsClient
from eims.data.responses import RegisterResult
fake = FakeEimsClient()
fake.push_register(RegisterResult("FAKE-IRN", None, None, None))
result = fake.register(invoice)
fake.assert_registered()
fake.assert_registered_count(1)
Operations
| Method | Endpoint | Returns |
|---|---|---|
register(InvoiceRequest) |
POST /v1/register |
RegisterResult |
bulk_register(list[InvoiceRequest]) |
POST /v1/bulkRegister |
BulkRegisterResult |
cancel(irn, reason_code, remark) |
POST /v1/cancel |
CancelResult |
bulk_cancel(list[dict]) |
POST /v1/bulkCancel |
BulkCancelResult |
verify(irn) |
POST /v1/verify |
VerifyResult |
bulk_verify(irns) |
loops verify |
dict[irn, VerifyResult | EimsException] |
register_receipt(ReceiptRequest) |
POST /v1/receipt/sales |
ReceiptResult |
register_withholding_receipt(WithholdingReceiptRequest) |
POST /v1/receipt/withholding |
ReceiptResult |
status(irn) |
GET /v1/invoices/status/{irn} |
StatusResult |
Enums
CancellationReason(DUPLICATE,DATA_ENTRY_ERROR,ORDER_CANCELLED,CALCULATION_ERROR) can be passed tocancel()instead of a raw string.InvoiceStatus(PENDING,REGISTERED,FAILED,CANCELLED,UNKNOWN) withInvoiceStatus.from_string()and predicatesis_registered()/is_cancelled(). Read it fromStatusResult.status_enum().
Error handling
from eims.exceptions import EimsApiException, EimsAuthenticationException, EimsConnectionException
try:
result = client.register(invoice)
except EimsAuthenticationException:
... # bad/expired credentials
except EimsApiException as e:
... # EIMS rejected the request: e.args[0], e.body, e.status_code
except EimsConnectionException:
... # network/transport failure or non-JSON response
Multi-tenant note
The client is stateless per instance and represents a single taxpayer. To
serve many taxpayers (e.g. a chain of stores), create one client per taxpayer
and pass a unique cache_namespace so their auth tokens never collide. The
package never needs to know about your "store"/"branch"/tenant concepts —
eims.contracts.SellerConfigResolver is a Protocol you can implement in a
multi-tenant service to resolve a SellerConfig from a request-scoped key.
Bring your own persistence
The client returns result objects only. Store the IRN, QR code, signed invoice, and raw responses in whatever table/model your app uses.
Signing / encryption (experimental)
The current EIMS register flow accepts plain JSON. eims.support.Canonicalizer
and eims.support.Signer are provided for integrations issued signing keys,
but are not wired into the request flow. Signer requires the optional
cryptography dependency (pip install "ethiopia-eims[signing]").
Differences from the PHP package
- Identifiers are
snake_case(idiomatic Python) instead ofcamelCase; the JSON wire format sent to EIMS is unchanged. Invoice.for(...)isInvoice.for_buyer(...)—foris a reserved word in Python.- No Laravel bridge. If you need a multi-tenant HTTP service wrapping this
client (e.g. a FastAPI gateway), build it on top of
EimsClient+SellerConfigResolverthe same way the PHP package's Laravel bridge does. - Test assertions use plain
assert(pytest-compatible) instead of a PHPUnit bridge.
License
MIT
Metadata
Release files for ethiopia-eims 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ethiopia_eims-0.1.0.tar.gz | 26.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ethiopia_eims-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.6 kB
Release files / ethiopia_eims-0.1.0.tar.gz
| Download URL | ethiopia_eims-0.1.0.tar.gz |
|---|---|
| Size | 26.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
829d416f6479bec3cff8760431515e3b905f6d58c21791f18843325f524f1c16
|
|
BLAKE2b-256 checksum How to use checksums |
32860802bff48061bc854cd11ca8d463f6d25f2a9fb939a0b36a848c2a4794b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / ethiopia_eims-0.1.0-py3-none-any.whl
| Download URL | ethiopia_eims-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a77eca0d51257064aa7266fd8bc3aa48c368b2a1239ea963a1bc25705d7415d1
|
|
BLAKE2b-256 checksum How to use checksums |
2f082a0eabd3d62dc99e16f52fb8847b4f58e4f19a06352fc20aa79cfb755fb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log