Skip to content

Python

Package: py-dgii-ecf · import py_dgii_ecf

Sync httpx2, tenacity + pybreaker, and result-pattern (result). Type-checked with mypy --strict.

Authenticate

from py_dgii_ecf import ECF, Environment, P12Reader

credentials = P12Reader(password).get_key_from_file("/secure/identity.p12")
if credentials.is_err():
    raise SystemExit(credentials.unwrap_err())

ecf = ECF(credentials.unwrap(), Environment.CERT)
auth = ecf.authenticate()
if auth.is_err():
    raise SystemExit(auth.unwrap_err())

print(auth.unwrap().token)

Sign a document

from py_dgii_ecf import Signature

data = credentials.unwrap()
signed = Signature(data.key, data.cert).sign_xml(xml)
if signed.is_err():
    raise SystemExit(signed.unwrap_err())

To build the signer directly from a .p12 file, use try_from_p12 — it returns a Result instead of raising:

signer = Signature.try_from_p12("identity.p12", password)
if signer.is_err():
    raise SystemExit(signer.unwrap_err())
signed = signer.unwrap().sign_xml(xml)

Submit

response = ecf.send_electronic_document(
    signed.unwrap(), "130862346E310000000001.xml"
)
if response.is_err():
    raise SystemExit(response.unwrap_err())
print(response.unwrap())

Result pattern

result = ecf.status_track_id("track-id")
if result.is_ok():
    print(result.unwrap().estado)
else:
    error = result.unwrap_err()
    print(error.code, error.message)

API

Every method returns a typed model, not a dict.

Method Returns
authenticate(buyer_host=None) Result[AuthToken]
send_electronic_document(xml, file_name, buyer_host=None) Result[InvoiceResponse]
send_summary(xml, file_name) Result[InvoiceSummaryResponse]
send_document(xml, file_name, buyer_host=None) Result[InvoiceResponse \| InvoiceSummaryResponse]
status_track_id(track_id) Result[TrackingStatusResponse]
inquiry_status(rnc, ncf, comprador=None, codigo=None) Result[InquiryStatusResponse]
track_statuses(rnc, encf) Result[list[SummaryTrackingStatusResponse]]
get_customer_directory(rnc) Result[list[ServiceDirectoryResponse] \| None]
get_directory_listing() Result[list[ServiceDirectoryResponse]]
get_summary_invoice_inquiry(rnc, encf, codigo) Result[InquiryInvoiceSummary]
send_commercial_approval(xml, file_name, buyer_host=None) Result[CommercialApprovalResponse \| B2BCommercialApprovalResponse]
void_encf(xml, file_name) Result[VoidNCFResponse]
reconcile_submission(xml) Result[ReconciliationResult]
get_service_status() Result[list[ServiceStatusEntry]]
get_maintenance_windows() Result[list[MaintenanceWindow]]
check_service_availability(ambiente) Result[ServiceAvailabilityResponse]

Routing with send_document

send_document reads the root element, TipoeCF and MontoTotal from the signed XML and picks the endpoint for you: an RFCE goes to the summary reception, an ECF to the general reception. An E32 below RD$250,000 is rejected with VALIDATION — build the RFCE first — and an RFCE cannot be delivered directly to a buyer_host.

Use resolve_reception_route(signed_xml) when you need the decision without sending it.

Service status

get_service_status, get_maintenance_windows and check_service_availability talk to the statusecf.dgii.gov.do host. It authenticates with an API key, not the bearer token, so pass status_api_key to the constructor and skip authenticate():

import os

ecf = ECF(credentials.unwrap(), Environment.PROD, status_api_key=os.environ["DGII_STATUS_API_KEY"])
availability = ecf.check_service_availability(StatusEnvironment.PRODUCTION)
if availability.is_ok() and availability.unwrap().estado == "No Disponible":
    ...  # Inside a maintenance window — defer the batch.

B2B applicability

A direct Emisor↔Receptor delivery (buyerHost / buyer_host) is not allowed for every document type. DGII excludes E32 below RD$250,000 (only the RFCE summary applies) plus E41, E43, E45, E46 and E47. E31, E33, E34, E44 and an E32 at or above the threshold can be delivered peer-to-peer.

The client checks this before issuing the request and fails with VALIDATION without calling the buyer host. Disable the check with enforce_b2b_rules=False when you need to reproduce a certification scenario:

result = ecf.send_electronic_document(signed_xml, "f.xml", buyer_host)
if result.is_err() and result.unwrap_err().code is DgiiErrorCode.VALIDATION:
    ...  # The type or amount is not eligible for direct delivery.

Production-only endpoints

get_summary_invoice_inquiry (RFCE inquiry, fc.dgii.gov.do/consultarfce) is only exposed by DGII in production (Environment.PROD / eCF). In TesteCF or CerteCF it returns a VALIDATION error without issuing a request:

inquiry = ecf.get_summary_invoice_inquiry(rnc, encf, codigo_seguridad)
if inquiry.is_err():
    print(inquiry.unwrap_err().code)   # VALIDATION outside production

A direct ACECF delivery goes to the buyer's fe/aprobacioncomercial/api/ecf endpoint and returns B2BCommercialApprovalResponse instead of DGII's {codigo, estado} payload. DGII documents HTTP 200 as satisfactory and HTTP 400 as unsatisfactory, so a 400 is reported as accepted: false rather than as a transport failure:

ack = ecf.send_commercial_approval(acecf, "acecf.xml", buyer_host)
if ack.is_ok() and not ack.unwrap().accepted:
    ...  # The receiver rejected the approval; the issuer must answer with an E34.

Schema validation

validate_document checks a document against the official DGII XML Schema and reports each violation with its element path. The schemas are bundled, so validation works offline.

from py_dgii_ecf import validate_document

result = validate_document(signed_xml)
if result.is_ok():
    for violation in result.unwrap():
        print(violation.path, violation.message)

validate_before_send=True runs the same check before a general e-CF submission and fails with VALIDATION without calling DGII:

ecf = ECF(credentials.unwrap(), Environment.PROD, validate_before_send=True)

Twelve schemas are bundled: e-CF 32, 33, 34, 41, 43, 44, 45, 46, 47, ANECF, ARECF and Semilla. Three published schemas do not compile — e-CF 31, RFCE 32 and ACECF — so those document types are reported as schema unavailable rather than passing silently. See examples/fixtures/schemas/README.md for the exact reasons.

Monetary rounding

Transformer.json_to_xml(..., round_monetary=True) rounds decimal values to two places with ROUND_HALF_UP (2.665 → 2.67, -2.675 → -2.68), matching the TypeScript SDK. Version and non-monetary text are never rounded.

CLI

py-dgii-ecf sign invoice.xml identity.p12 --output signed.xml
py-dgii-ecf authenticate identity.p12 --environment CerteCF
py-dgii-ecf status <track-id> identity.p12 --environment CerteCF

Each command prompts for the .p12 password (--password, hidden input). --environment accepts TesteCF / CerteCF / eCF, or the aliases dev, cert and prod.

See Signing, Resilience, and the package README.