> ## Documentation Index
> Fetch the complete documentation index at: https://docs.emite.do/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Python

> Cliente oficial ecfservice. Emisión E31–E47, portal, inbox, ACECF y ANECF.

Paquete: [`ecfservice`](https://pypi.org/project/ecfservice/) · Código: [ecf-service-python](https://github.com/yasmanycastillo/ecf-service-python)

```bash theme={null}
pip install ecfservice
```

Requiere Python 3.10+. Host: `https://api.emite.do/api/v1`.

## Cliente

```python theme={null}
from ecfservice import ECFClient

with ECFClient(api_key="ecf_...") as client:
    print(client.health().status)
```

`api_key` es opcional si solo usas `health()` o `client.dgii.*`.

Constructor:

| Parámetro  | Default                       | Uso                                 |
| ---------- | ----------------------------- | ----------------------------------- |
| `api_key`  | `None`                        | Header `X-API-Key`                  |
| `base_url` | `https://api.emite.do/api/v1` | Staging propio                      |
| `timeout`  | `30` s                        | Las descargas de PDF/ZIP usan 120 s |

## Primer e-CF

El e-NCF lo pones tú. `FechaEmision` va en `Emisor` como **DD-MM-YYYY**. El builder escribe `TipoeCF` (no `TipoEcf`).

```python theme={null}
from datetime import date
from ecfservice import ECFClient, ECFPayloadBuilder

payload = (
    ECFPayloadBuilder(ecf_type="31")
    .id_doc(
        eNCF="E310000000001",
        FechaVencimientoSecuencia="31-12-2028",
        IndicadorMontoGravado=0,
        TipoIngresos="01",
        TipoPago="1",
    )
    .emisor(
        RNCEmisor="130478031",
        RazonSocialEmisor="Mi Empresa SRL",
        DireccionEmisor="AV. DEMO 1",
        FechaEmision=date.today().strftime("%d-%m-%Y"),
    )
    .comprador(RNCComprador="131098193", RazonSocialComprador="Cliente SRL")
    .totales(MontoTotal=11800.0, TotalITBIS=1800.0)
    .add_item({
        "NumeroLinea": 1,
        "NombreItem": "SERVICIO DEMO",
        "IndicadorFacturacion": 1,
        "IndicadorBienoServicio": 2,
        "CantidadItem": 1.0,
        "MontoItem": 10000.0,
    })
    .build()
)

with ECFClient(api_key="ecf_...") as client:
    doc = client.ecf.create(
        idempotency_key="INV-2026-0001",
        ecf_type="31",
        environment="TesteCF",
        payload=payload,
    )
    print(doc.public_id, doc.status)  # received
    doc = client.ecf.get(doc.public_id)
    xml = client.ecf.download_xml(doc.public_id)
```

Misma `idempotency_key` → `200` del original. El payload no se compara. Ver [idempotencia](/guides/idempotency).

`ecf_type` acepta `31` `32` `33` `34` `41` `43` `44` `45` `46` `47`. Cambia `TipoeCF` y el e-NCF (`E32…`, `E47…`).

Alias cortos en el builder: `RNC` / `RazonSocial` → `RNCEmisor` o `RNCComprador`. `Descripcion` en un ítem → `NombreItem`.

Para rellenar Emisor desde el perfil:

```python theme={null}
from ecfservice import build_emisor

profile = client.client.company()
emisor = build_emisor(profile, FechaEmision="15-08-2026")
```

## Después del 201

1. Persiste `public_id`.
2. Configura [webhooks](/guides/webhooks) o haz poll a `client.ecf.get`.
3. Descarga artefactos cuando el estado lo permita (`accepted` / `conditionally_accepted`).

Estados habituales: `received` → `signed` → `submitted` → `accepted` | `conditionally_accepted` | `rejected`.

## Empresa, rangos, inbox

```python theme={null}
profile = client.client.company()
seqs = client.client.sequences(active_only=True)
inbox = client.client.inbox(acked=False)
client.client.ack_inbox(inbox.items[0].public_id)
xml = client.client.download_inbox_xml(inbox.items[0].public_id)
```

Los rangos se listan; no se crean por API. Inbox = e-CF **recibidos** como comprador, no tus emisiones.

## ACECF y ANECF

```python theme={null}
client.ecf.submit_acecf(
    idempotency_key="ac-001",
    encf="E310000000001",
    rnc_emisor="130478031",
    rnc_comprador="131098193",
    fecha_emision="15-08-2026",
    monto_total="11800.00",
    estado="1",  # 1 acepta, 2 rechaza
)
client.ecf.cancel_sequences(
    idempotency_key="an-001",
    cancellations=[{
        "ecf_type": "31",
        "sequence_from": "E310000000010",
        "sequence_to": "E310000000012",
        "quantity": 3,
    }],
)
```

## Webhooks

Hoy el service notifica `accepted`, `conditionally_accepted` y `rejected`.

```python theme={null}
from ecfservice.webhook import verify_webhook_signature

wh = client.client.create_webhook(
    url="https://mi-app.example/webhooks/ecf",
    events=["accepted", "conditionally_accepted", "rejected"],
)
ok = verify_webhook_signature(request_body, request.headers["X-ECF-Signature"], wh.secret)
```

`wh.secret` solo se muestra al crear o rotar.

## Errores

El service responde `{ "detail": "..." }`.

| HTTP         | Excepción                                 |
| ------------ | ----------------------------------------- |
| 401          | `ECFAuthError`                            |
| 404          | `ECFNotFoundError`                        |
| 409          | `ECFConflictError` (e-NCF o idempotencia) |
| 422          | `ECFValidationError`                      |
| otro 4xx/5xx | `ECFError`                                |

## Mapa rápido

| Método                                     | Endpoint                         |
| ------------------------------------------ | -------------------------------- |
| `client.ecf.create`                        | `POST /api/v1/ecf`               |
| `client.ecf.get`                           | `GET /api/v1/ecf/{id}`           |
| `client.ecf.download_xml` / `pdf` / `rfce` | artefactos de emisión            |
| `client.ecf.submit_acecf`                  | `POST /api/v1/ecf/acecf/submit`  |
| `client.ecf.cancel_sequences`              | `POST /api/v1/ecf/cancellations` |
| `client.client.company`                    | `GET /api/v1/client/company`     |
| `client.client.inbox`                      | `GET /api/v1/client/inbox`       |
| `client.dgii.rnc`                          | `GET /api/v1/dgii/rnc/{rnc}`     |
| `client.health`                            | `GET /api/v1/health`             |

Siguiente: [primer documento](/guides/first-document) o [webhooks](/guides/webhooks).
