Skip to main content

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 to cancel() instead of a raw string.
  • InvoiceStatus (PENDING, REGISTERED, FAILED, CANCELLED, UNKNOWN) with InvoiceStatus.from_string() and predicates is_registered() / is_cancelled(). Read it from StatusResult.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 of camelCase; the JSON wire format sent to EIMS is unchanged.
  • Invoice.for(...) is Invoice.for_buyer(...) — for is 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 + SellerConfigResolver the 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)

Source distribution for ethiopia-eims 0.1.0
File Size Uploaded
ethiopia_eims-0.1.0.tar.gz 26.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ethiopia-eims 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page