# Emitir un CFE

> POST /v1/companies/{rut}/sendCfes/{branchOffice} - el contrato completo

```
POST /v1/companies/{rut}/sendCfes/{branchOffice}
```

- `rut`: RUT emisor (12 dígitos)
- `branchOffice`: número de sucursal DGI

Un solo endpoint cubre todos los tipos de CFE (eTicket, eFactura, notas de
crédito/débito, exportación, contingencia): el tipo va como clave en el body.

## Antes de poder emitir

Tres cosas tienen que existir antes de emitir. Si falta la sucursal o el
certificado, falla **el request entero**, antes de crear ningún comprobante. Si
falta el CAE de un tipo, falla **por comprobante**, dentro del array de
respuesta.

| Requisito | Qué es | Si falta |
|---|---|---|
| La sucursal | La empresa tiene que tener la `branchOffice` que va en la URL | El request entero responde `412` con `UNEXISTENT_INSTANCE` |
| Un certificado activo | Un certificado de firma vigente de la empresa, uno solo activo a la vez | Sin certificado activo, el request entero responde `412` con `UNEXISTENT_INSTANCE`. Si el activo no se puede leer, cada comprobante falla con `KEYSTORE_GET_ERROR` |
| Un CAE activo **por cada tipo de CFE** | Rango de numeración autorizado por DGI para ese tipo | `DGI_MISSING_CAE` |

La sucursal la crea pymo en el alta de la empresa. El certificado y los CAE se
cargan por API, con `POST /v1/companies/{rut}/certs` y
`POST /v1/companies/{rut}/cfesActiveNumbers/{code}/upload-xml`. Cargar un CAE no
cambia nada en DGI: registra en pymo una numeración que DGI ya autorizó.

El CAE es por tipo, no por empresa: tener CAE de eFactura (`111`) no habilita
emitir eTickets (`101`). Sirve uno con `active: true` y `expireDate` en el
futuro, evaluado contra la hora de Uruguay.

Cada emisión consume un número del rango (`range.first` → `range.last`) de forma
atómica. Cuando el rango se agota el CAE se desactiva solo, y a partir de ahí ese
tipo responde `DGI_MISSING_CAE` hasta que se cargue uno nuevo. Hay un aviso
previo configurable por porcentaje de rango restante, así que no debería llegarse
al final por sorpresa.

`DGI_MISSING_CAE` es de los pocos errores **recuperables sin cambiar el request**:
cargado el CAE, reenviar el mismo `clientEmissionId` emite (ver
[Idempotencia](/idempotencia), sección del CAE faltante).

## Qué se manda

**No hay una clave `cfes`.** Cada clave del body es el código de tipo de CFE y su
valor es la lista de comprobantes de ese tipo, así que un mismo request puede
llevar eFacturas y eTickets a la vez. Las únicas claves que no son un tipo son
`emailsToNotify` y `phonesToNotify`; cualquier otra se ignora en silencio.

```json
{
  "111": [
    {
      "clientEmissionId": "pedido-2026-000123",
      "IdDoc": { "MntBruto": "1", "FmaPago": "1" },
      "Receptor": {
        "TipoDocRecep": "2",
        "CodPaisRecep": "UY",
        "DocRecep": "211234567005",
        "RznSocRecep": "EMPRESA DE EJEMPLO SA",
        "DirRecep": "Calle Falsa 1234",
        "CiudadRecep": "Montevideo",
        "DeptoRecep": "Montevideo"
      },
      "Totales": { "TpoMoneda": "UYU" },
      "Items": [
        { "NroLinDet": "1", "IndFact": "1", "NomItem": "Servicio de ejemplo",
          "Cantidad": 1, "UniMed": "N/A", "PrecioUnitario": 1000, "MontoItem": 1000 }
      ],
      "adenda": "Texto libre opcional"
    }
  ],
  "emailsToNotify": []
}
```

Obligatorios en cada comprobante: `clientEmissionId`, `IdDoc`, `Receptor`,
`Totales` e `Items` con al menos una línea. La estructura completa, campo por
campo, está en la [Referencia de la API](/api-reference) bajo el esquema
`SolicitudEmision`.

Los nombres de los campos son **los de DGI**, no los del gateway, y las reglas de
cada uno son las del documento [Formato CFE](https://www.efactura.dgi.gub.uy/principal/ampliacion_de_contenido/documento-de-formato-cfe-version-23-1) de DGI. Acá se documenta la estructura y
lo que el gateway hace con ella; lo que no está acá se resuelve contra ese
documento, no adivinando.

Dos que conviene tener claros:

- `IdDoc.Serie` y `IdDoc.Nro` sólo se mandan para los tipos que admiten serie y
  número propios. En el resto los asigna el gateway desde el CAE, y mandarlos no
  hace nada.
- `Receptor.DocRecep` con dígito verificador inválido **no falla en la emisión**.
  El gateway firma y responde `SUCCESS`; DGI rechaza el sobre después, de forma
  asíncrona. Es el caso más fácil de confundir con un éxito.

## Cómo responde

<Warning>
La respuesta del POST **no** significa "aceptado por DGI". El gateway **crea y
firma el CFE con su CAE** y responde de inmediato; el envío a DGI ocurre
**después, de forma asíncrona**.
Por eso un CFE puede volver `SUCCESS` en la
emisión y quedar `PROCESSED_REJECTED` más tarde cuando DGI valida el sobre.
</Warning>

El flujo:

1. `POST sendCfes` → el gateway asigna CAE, firma y guarda el CFE → responde
   `DGI_CFES_RECEIVE_SUCCESS` con los datos del comprobante.
2. El gateway envía el sobre a DGI, de forma asíncrona.
3. El estado final (`PROCESSED_ACCEPTED` / `PROCESSED_REJECTED`) se consulta por
   `GET .../sentCfes` o llega por el webhook `CFE_STATUS_CHANGE`.

## Respuesta de éxito

`message.code = DGI_CFES_RECEIVE_SUCCESS`. El payload trae `cfesIds`, un array
con un objeto por CFE creado. Campos **exactos** que devuelve el servicio:

```json
{
  "payload": {
    "cfesIds": [
      {
        "id": "6a7121a37698d7c0ab3f1b51",
        "clientEmissionId": "009993ca34f74e0de3106aba89f818b3",
        "serie": "A",
        "nro": 3503,
        "type": "111",
        "caeNumber": 90120000538,
        "caeSerie": "A",
        "caeRange": { "first": 1, "last": 999999 },
        "caeExpirationDate": "2050-01-01T00:00:00.000Z",
        "total": "3050.00",
        "emitionDate": "2026-08-03T20:17:46.000-03:00",
        "sentXmlHash": "mghK5nJZkGskjJUkRv8EnG+U8xa1vs1tROJsIqDXBMk=",
        "securityCode": "mghK5n",
        "qrUrl": "https://.../consultaQR/cfe?219999990008,111,A,3503,3050.00,...",
        "CAEEspecial": "4",
        "CausalCAEEsp": null
      }
    ]
  },
  "message": { "code": "DGI_CFES_RECEIVE_SUCCESS", "value": "Cfes recibidos correctamente." },
  "status": "SUCCESS"
}
```

### Campos a persistir

Todos los de arriba, y en particular:

| Campo | Para qué |
|---|---|
| `serie`, `nro` | Identificación fiscal del comprobante |
| `caeNumber`, `caeSerie`, `caeRange`, `caeExpirationDate` | Autorización DGI, para la representación impresa |
| `sentXmlHash` / `securityCode` | Hash del XML firmado; `securityCode` son sus primeros 6 caracteres |
| `qrUrl` | URL del QR - la arma el gateway, el integrador no la construye (ver [Salida](/salida-comprobante)) |
| `id` | Identificador interno para consultar estado y PDF |
| `clientEmissionId` | La clave de idempotencia - ver [Idempotencia](/idempotencia) |

## Lotes: éxito parcial

Un request puede llevar varios comprobantes y **cada uno se resuelve por
separado**. El array `cfesIds` es heterogéneo: los que salieron traen los datos
del comprobante, los que fallaron traen un error, en el mismo array y sin nada
que los separe.

<Warning>
**El estado de arriba no refleja lo de abajo.** Un envelope con
`status: "SUCCESS"` y `message.code: "DGI_CFES_RECEIVE_SUCCESS"` puede traer
**todos** los comprobantes del lote fallidos. No hay conteo, ni bandera de éxito
parcial, ni código distinto: hay que recorrer `cfesIds` y mirar cada entrada. Si
el envelope no es `SUCCESS`, falló el request entero y no hay `cfesIds`.
</Warning>

Una entrada fallida tiene esta forma:

```json
{
  "code": 412,
  "message": {
    "code": "RECEPTOR_DOC_EXT_REQUIRED",
    "value": "Es requerido especificar el documento extranjero del receptor: PASAPORTE (5), DNI (6), NIFE (7) o OTROS (4)"
  },
  "status": "FAIL",
  "receivedDataWithError": {
    "clientEmissionId": "pedido-2026-000123",
    "cfe": { "…": "el comprobante entero, como lo interpretó el gateway" }
  }
}
```

Cómo distinguirlas en código:

- Una entrada con `status: "FAIL"` es un error. Una con `serie` y `nro` es un
  comprobante emitido.
- **No te fíes del orden.** Para saber a cuál de tus comprobantes corresponde,
  usá `clientEmissionId`: está en `receivedDataWithError.clientEmissionId` en el
  error y en el nivel superior en el éxito.
- `receivedDataWithError.cfe` trae el comprobante **como lo interpretó el
  gateway**, con los totales ya calculados. Sirve para ver qué entendió de lo que
  mandaste.

## Errores

Códigos de error de la emisión:

| `message.code` | Significado | ¿Reintentable? |
|---|---|---|
| `UNEXISTENT_INSTANCE` (`412`) | Falta la sucursal o el certificado activo. Falla el request entero, sin `cfesIds` | No hasta cargarlo |
| `DGI_MISSING_CAE` | No hay CAE disponible para ese tipo de CFE | No hasta cargar CAEs |
| `DGI_MISSING_SPECIAL_CAE` / `DGI_MISSING_CUSTOM_SPECIAL_CAE` | Falta CAE especial / custom | No hasta cargar el CAE |
| `DGI_BAD_CUSTOM_SERIE_NUMBER` | Serie/número custom inválido | No (corregir el input) |
| `REQUIRED_PARAMETERS` / `RECEPTOR_REQUIRED` / `RECEPTOR_DOC_REQUIRED` | Falta un dato obligatorio | No (corregir el input) |
| `DUPLICATED_KEY` | `clientEmissionId` ya usado | No reemite; devuelve el CFE original (ver [Idempotencia](/idempotencia)) |
| `KEYSTORE_GET_ERROR` | El gateway no pudo leer el certificado activo | No hasta revisar el certificado |
| `DGI_COMPANY_NOT_READY_YET` | La empresa aún no está lista en DGI | Sí, más tarde |
| `DGI_SOAP_ERROR` | Error hablando con DGI (fase asíncrona) | Sí |

<Note>
El rechazo de DGI (`PROCESSED_REJECTED`) **no** viene en la respuesta del POST:
llega asíncrono. En `GET .../sentCfes`, el motivo viaja en
`cfeHistory[].data.digestAck`:

```json
{ "actualCfeStatus": "PROCESSED_REJECTED",
  "cfeHistory": [ { "cfeStatus": "PROCESSED_REJECTED",
    "data": { "digestAck": "DGI_ERR [SENDING SOBRE]: <Glosa>No cumple validaciones de Formato comprobantes - Dígito verificador RUT Receptor no es válido: 285267464895" } } ] }
```
</Note>

## Estados del CFE

El estado vive en `actualCfeStatus`, y el historial completo con sus fechas en
`cfeHistory[]`. Se consulta con `GET .../sentCfes` o llega por el webhook
`CFE_STATUS_CHANGE`.

Son diecisiete valores, no cuatro. La distinción que importa al escribir el
polling es **si el estado puede cambiar solo**: si es transitorio hay que seguir
consultando, si es final no.

### Transitorios: seguí consultando

| Estado | Qué pasó |
|---|---|
| `CREATED` | Firmado, pendiente de envío a DGI |
| `CREATED_WITHOUT_CAE_NRO` | Creado sin número de CAE asignado todavía |
| `BULK_CREATED_WITHOUT_CAE_NRO` | Igual, dentro de un envío masivo |
| `SCHEDULED` | Encolado para envío |
| `SCHEDULED_CONNECTION_ERR` | Encolado de nuevo porque falló la conexión con DGI. Reintenta solo |
| `SCHEDULED_WITHOUT_CAE_NRO` | Encolado, esperando numeración |
| `BULK_SCHEDULED_WITHOUT_CAE_NRO` | Igual, dentro de un envío masivo |
| `SENT` | El sobre salió hacia DGI; falta la respuesta |

### Finales: el comprobante ya no se mueve

| Estado | Qué pasó | ¿Comprobante fiscal válido? |
|---|---|---|
| `PROCESSED_ACCEPTED` | DGI lo aceptó | Sí |
| `PROCESSED_REJECTED` | DGI lo rechazó. El motivo viaja en `cfeHistory[].data.digestAck` | No |
| `PROCESSED_RELIQUIDATED` | Reliquidado (sólo CFC) | Sí, con ajuste |
| `FORMAT_REJECTED` | No pasó las validaciones de formato de DGI | No |
| `SOBRE_DUPLICATED` | El sobre ya había sido recibido por DGI | Se asume enviado |
| `DUPLICATED_AT_DGI` | DGI ya tenía ese comprobante. El original es el que vale | No, el original sí |
| `BAD_CUSTOM_SERIE_NUMBER` | La serie o el número propios eran inválidos | No |
| `DELETED_MISSING_CAE` | No había CAE al recibirlo. Ver [Idempotencia](/idempotencia): tu `clientEmissionId` queda libre para reintentar | No |
| `REPORTED_DAILY_REPORT` | Incluido en el reporte diario a DGI | Sí, es posterior a la aceptación |
| `CFE_UNKNOWN_ERROR` | Error no clasificado | No |

`FAKE_CFES_HOMOLOGATION` existe para el proceso de homologación y no aparece en
operación normal.

<Warning>
`PROCESSED_REJECTED` es el mismo valor para dos cosas distintas: un CFE rechazado
y un CFC observado. Si trabajás con comprobantes de contingencia, el estado solo
no alcanza para distinguirlos.
</Warning>

## Corregir o anular un comprobante ya emitido

**No se puede borrar ni editar un CFE emitido.** Se emite otro comprobante que lo
referencia: una nota de crédito para anular o descontar, una nota de débito para
agregar. Ambas son comprobantes fiscales por derecho propio, consumen su propia
numeración y también se informan a DGI.

```
POST /v1/companies/{rut}/createCreditNote
POST /v1/companies/{rut}/createDebitNote
```

```json
{
  "referencedDocuments": [
    {
      "clientEmissionId": "nc-2026-000045",
      "cfeId": "6a7121a37698d7c0ab3f1b51",
      "pctToAffect": 100
    }
  ],
  "emailsToNotify": []
}
```

Cómo se referencia el original, y sus reglas:

| Campo | Regla |
|---|---|
| `clientEmissionId` | **Obligatorio por documento**, igual que en una emisión: es la misma clave de idempotencia |
| `cfeId` **o** (`cfeType`, `serie`, `nro`) | Uno de los dos, nunca los dos. Mandar ambos es un error explícito |
| `pctToAffect` | Porcentaje del original a afectar, entre 0 y 100 |
| `mntToAffect` | Monto a afectar; tiene que ser mayor a 0 |
| `items` | Líneas explícitas. **Si se mandan, `pctToAffect` y `mntToAffect` se ignoran** |

La sucursal no se manda: se infiere del comprobante referenciado. El tipo de la
nota tampoco: sale del tipo del original (una NC de una eFactura `111` es `112`),
y si ese tipo no admite notas el gateway responde que no las admite en lugar de
inventar una.

Cuando no se mandan `items`, el gateway construye la línea a partir de la primera
del original, heredando `IndFact`, `UniMed` y `NomItem`. Si no puede inferirlos,
falla en lugar de adivinar.
