efrisapi.com

Docs / Quickstart

Quickstart

From nothing to a fiscalised invoice. Work against the URA sandbox until the output is right — documents submitted to production are real tax records.

What you need from URA

You do not need to apply for an appId. It names the integration channel, not you — AP04 for system-to-system, the same for everyone. This trips people up because it looks like a credential.

Install

pip install -r requirements.txt

Python 3.9 or newer. The client itself needs only requests and cryptography.

Connect

import os, logging
from efris import EfrisClient, EfrisConfig, EfrisError

logging.basicConfig(level=logging.INFO)

client = EfrisClient(EfrisConfig(
    tin=os.environ["EFRIS_TIN"],
    cert_path=os.environ["EFRIS_CERT_PATH"],
    cert_password=os.environ["EFRIS_CERT_PASSWORD"],
    legal_name="Your Company Ltd",     # appears on the invoice
    test_mode=True,                    # URA sandbox
))

print(client.get_server_time())

Always start with get_server_time(). It is unsigned and unencrypted, so if it succeeds and everything else fails, the problem is the certificate rather than connectivity or your TIN. That one check separates the two most common first-day failures.

Fiscalise an invoice

Give the library plain line data and it computes the tax-inclusive breakdown:

document = client.build_invoice_payload(
    "INV-2026-0042",
    [
        {"item_code": "CHAIR-001", "item_name": "Office Chair",
         "quantity": 2, "unit_price": 150000, "tax_rate": 18},
        {"item_code": "DESK-004", "item_name": "Desk 120cm",
         "quantity": 1, "unit_price": 420000, "tax_rate": 18},
    ],
    customer_name="Acme Wholesalers Ltd",
    customer_tin="2000000000",
    buyer_type="0",                    # 0 B2B, 1 B2C, 2 foreigner, 3 B2G
)

result = client.upload_invoice(document)

That produces a summary of 720000.00 gross, 610169.49 net and 109830.51 tax — VAT-inclusive, reconciling the way URA recomputes it.

Two line shapes, and they behave differently. Plain shape (quantity, unit_price, tax_rate) is calculated for you. EFRIS shape (qty, unitPrice, total, tax) is taken as authoritative and is not recalculated — so supplying qty and unitPrice without total would otherwise fiscalise a zero-value invoice. The library refuses that rather than filing a wrong record.

Handle rejections

try:
    client.upload_invoice(document)
except EfrisError as exc:
    print(exc.code, exc.message)       # branch on the CODE, never the text
    if exc.code == "1345":
        ...                            # summary.grossAmount includes excise

URA rewords messages between releases, so the number is the stable contract. See the error reference for what each one means.

Serving several taxpayers

Nothing is global. Build one config per taxpayer from wherever you keep credentials, and cache the client — a cold one performs a key exchange on its first call:

clients = {}

def for_tenant(tenant):
    if tenant.id not in clients:
        clients[tenant.id] = EfrisClient(EfrisConfig(
            tin=tenant.tin,
            device_no=tenant.device_no,
            cert_path=tenant.cert_path,
            cert_password=tenant.cert_password,
            legal_name=tenant.legal_name,
            test_mode=tenant.sandbox,
        ))
    return clients[tenant.id]

Each holds its own session key, so tenants never share cryptographic state. Certificates are per-taxpayer and must be stored per-taxpayer.

Going to production

Next


The EFRIS API Kit handles this case already — it is one of the rejections the library was calibrated against. This page stays free either way.