Skip to content

Recetas

Casos que aparecen de verdad en un negocio, resueltos de principio a fin.


Restaurante

Propina sin cobrarle IVA

Una propina voluntaria no es una venta del restaurante: es dinero que pasa al mesero. Cobrarle IVA es cobrar un impuesto que no se debe, y hacerlo en cada cuenta de cada día convierte un detalle en una contingencia fiscal.

El esquema de Hacienda lo contempla con noGravado —«Cargos/Abonos que no afectan la base imponible»—. Se manda por línea:

json
{
  "tipoDte": "01",
  "items": [
    {"descripcion": "Almuerzo ejecutivo", "cantidad": 2, "uniMedida": 59,
     "precioUni": 8.50, "tipoItem": 2},
    {"descripcion": "Propina voluntaria", "cantidad": 1, "uniMedida": 59,
     "precioUni": 1.70, "tipoItem": 2, "noGravado": 1.70}
  ],
  "condicionOperacion": 1,
  "formaPago": "01"
}

noGravado es la parte del total de esa línea que no forma base imponible. Para una línea que es solo propina, iguala al total de la línea y su venta gravada queda en cero.

El efecto en el documento:

totalGravada     17.00     ← solo la comida
totalIva          1.96     ← IVA de la comida, no de la propina
totalNoGravado    1.70     ← la propina
totalPagar       18.70     ← lo que paga el cliente

Si la propina fuera al precio sin marcarla, totalIva saldría 2.15: veinte centavos de impuesto que nadie debe, en cada cuenta.

Antes de usarlo en producción, emite una prueba en el ambiente 00 y comprueba que sella. La aritmética está verificada contra los esquemas oficiales fe-f-v2.json y fe-ccf-v4.json, pero las reglas de negocio del validador de Hacienda son más estrictas que su esquema.

Una propina mayor que el importe de su línea se rechaza aquí, con 422: dejaría la venta gravada en negativo, que es algo que el esquema acepta y Hacienda no —con el correlativo ya gastado—.

Emitir sin hacer esperar al cliente

Un restaurante no puede decirle a la mesa «espere, le aviso por webhook». Usa esperar:

bash
curl -s -X POST "$API/api/v1/dte/documentos?esperar=8s" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Idempotency-Key: $CLAVE_IDEM" \
  -H "Content-Type: application/json" \
  -d "$CUENTA"

Si el sello llega dentro de los 8 segundos, la respuesta ya trae SELLADO y puedes imprimir el ticket con su QR. Si no llega, el documento sigue su curso igual: no se pierde nada.

Lo que tiene que hacer tu punto de venta en ese caso: imprimir el ticket de todos modos y quedarse con el codigoGeneracion. El QR se puede reimprimir después, y un webhook te avisará. No bloquees la caja esperando el sello, y sobre todo no bloquees la venta: en una contingencia de Hacienda dejarías de vender sin necesidad.

Dividir la cuenta

Cuatro personas piden cuentas separadas de una misma mesa. Fiscalmente son cuatro documentos independientes: no existe un DTE «parcial» ni una forma de enlazarlos.

Lo que hay que resolver de tu lado:

  1. Reparte las líneas en tu sistema, no en el DTE. Cada cuenta lleva los ítems de quien paga.
  2. Una Idempotency-Key distinta por cuenta. Es el error que más duele aquí: reutilizar la clave de la mesa haría que la segunda cuenta devolviera el documento de la primera, con 200 en vez de 202. Tu sistema creería que facturó cuatro veces y habría facturado una.
javascript
for (const cuenta of dividirMesa(mesa)) {
  await emitir(cuenta, { idempotencyKey: `mesa-${mesa.id}-cuenta-${cuenta.id}` });
}
  1. Si dividen a partes iguales, reparte los centavos explícitamente. Tres personas y 10.00 no da 3.333 tres veces: da 3.34, 3.33 y 3.33. Deja el sobrante en la primera cuenta y comprueba que la suma cuadra antes de emitir nada.

  2. Si alguien paga con tarjeta y otro en efectivo, cada documento lleva su propia formaPago (CAT-017).

Anular una orden ya facturada

Depende del tipo y del estado. Es el punto donde más se equivoca un POS.

SituaciónQué hacer
Documento TRANSMITIENDOEspera. No hay nada que anular todavía; y si lo intentas, 409.
Documento RECHAZADONada. No existe para Hacienda. Corrige y emite uno nuevo.
Factura (01) sellada, operación canceladaInvalidar. POST /dte/documentos/{cg}/invalidar.
Factura (01), FEX (11) o FSE (14) sellada, se devuelve mercancíaEvento de Retorno. POST /dte/documentos/{cg}/retorno. No invalida el original. Una nota de crédito no sirve: esos tipos no transfieren crédito fiscal.
CCF (03) selladoNota de Crédito (05), por el total, con el CCF en documentosRelacionados. Un CCF no admite retorno.
Se equivocaron en un ítem del CCFNota de Crédito por la diferencia, no por el total.

Invalidar una Factura:

bash
curl -s -X POST "$API/api/v1/dte/documentos/$CG/invalidar" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "tipoAnulacion": 2,
    "motivo": "El cliente canceló la orden antes de consumirla",
    "nombreResponsable": "MARIA LOPEZ",
    "tipoDocResponsable": "36",
    "numDocResponsable": "06140702191120",
    "nombreSolicita": "MARIA LOPEZ",
    "tipoDocSolicita": "36",
    "numDocSolicita": "06140702191120"
  }'

No uses tipoAnulacion: 3. Hacienda lo rechaza con el código 096 en una Factura; comprobado contra el ambiente de pruebas. Los tipos 1 («error en la información») y 2 («rescindir la operación») sellan sin problema.

Y en los dos casos: el correlativo sigue consumido. Anular no devuelve el número.

Un cliente pide factura a nombre de su empresa después de cobrar

Ya emitiste la Factura (01) a consumidor final y ahora quieren el CCF para deducir el IVA.

No se «convierte». Son dos operaciones:

  1. Invalida la Factura (tipoAnulacion: 1, error en la información).
  2. Emite el CCF con los datos de la empresa, recordando que precioUni ahora va sin IVA: el total al cliente es el mismo, el número del campo no.

Hazlo el mismo día. El plazo para invalidar está acotado por normativa y un documento del mes pasado ya no se toca.

Consumo interno y cortesías

Un plato que se regala o que consume el personal no genera documento tributario al cliente, pero sí sale del inventario. Regístralo en tu sistema como merma o consumo interno, sin emitir DTE.

Lo que no hay que hacer es emitir una factura de 0.00: cantidad tiene que ser mayor que cero y el documento quedaría en el libro con un total que no corresponde a ninguna venta.


Retail

Venta rápida con lector de códigos

Igual que un restaurante, con dos diferencias:

  • tipoItem: 1 (bienes) en vez de 2 (servicios).
  • codigo de cada línea con tu SKU. Viaja tal cual y aparece en el PDF, lo que hace mucho más fácil casar el documento con tu inventario después.
json
{"descripcion": "Camisa manga larga talla M", "codigo": "CAM-M-001",
 "cantidad": 2, "uniMedida": 59, "precioUni": 24.99, "tipoItem": 1}

Descuentos

Hay dos, y no se mezclan:

Por línea, con montoDescu en dinero, no en porcentaje. Resta de esa línea antes de sumar el documento:

json
{"descripcion": "Camisa", "cantidad": 2, "uniMedida": 59,
 "precioUni": 24.99, "montoDescu": 5.00, "tipoItem": 1}

Calcula el porcentaje de tu lado y manda el resultado redondeado a dos decimales. Un descuento mayor que el importe de la línea se rechaza con 422: dejaría la venta gravada en negativo, que Hacienda rechaza con el correlativo ya gastado.

Global, con descuentoGlobal en dinero, sobre el total gravado. MH lo declara en resumen.descuGravada (o resumen.descu en la FSE) y reduce el subTotal. En el CCF el IVA se calcula después de restarlo.

json
{
  "tipoDte": "01",
  "items": [
    {"descripcion": "Camisa", "cantidad": 2, "uniMedida": 59,
     "precioUni": 24.99, "tipoItem": 1}
  ],
  "descuentoGlobal": 5.00,
  "condicionOperacion": 1,
  "formaPago": "01"
}

Solo lo admiten 01, 03, 04, 11 y 14 —los tipos cuyo esquema tiene ese campo. GET /dte/tipos-documento lo dice en descuentoGlobal. En una nota de crédito el descuento es la nota: mandar descuentoGlobal se rechaza.

Si mandas los dos, se suman en resumen.totalDescu. El porcentaje del resumen lo calcula el sistema; no lo mandes.

Devolución de un producto

Si fue Factura (01), Factura de Exportación (11) o FSE (14), el Evento de Retorno: no invalida el original y no genera crédito fiscal. Si fue CCF, Nota de Crédito por las líneas devueltas.

bash
curl -s -X POST "$API/api/v1/dte/documentos/$CG/retorno" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"numItem":1,"cantidad":1}]}'

items vacío devuelve todas las líneas. Varios retornos sobre el mismo DTE mientras la suma no supere el valor original. Con un retorno vigente no se puede invalidar la factura.

Para una devolución parcial de un CCF, la nota lleva solo las líneas que vuelven, con su cantidad y su precio original:

json
{
  "tipoDte": "05",
  "receptor": { "..." },
  "documentosRelacionados": [{"numeroDocumento": "EL-CG-DEL-CCF"}],
  "items": [
    {"descripcion": "Camisa manga larga talla M", "cantidad": 1,
     "uniMedida": 59, "precioUni": 24.99, "tipoItem": 1}
  ]
}

Varias cajas

Manda siempre codEstable y codPuntoVenta. Cada caja lleva su propio correlativo; dejar que el sistema elija hace que dos cajas compartan serie y eso es un problema de auditoría, no de software.

json
{"tipoDte": "01", "codEstable": "M001", "codPuntoVenta": "P002", "items": [...]}

Servicios profesionales

Factura al crédito

condicionOperacion: 2. El documento es idéntico; lo que cambia es que tu sistema tiene que llevar la cuenta por cobrar.

json
{"tipoDte": "03", "condicionOperacion": 2, "formaPago": "05", "receptor": {...}, "items": [...]}

Cobrar a un proveedor informal: Factura de Sujeto Excluido

Cuando le compras a alguien que no es contribuyente inscrito —un albañil, un servicio ocasional—, el que emite el documento eres tú, con el tipo 14. → Tipos de documento

retencionRenta: 0.10 es el Art. 162 del Código Tributario, pero no siempre aplica: depende del monto y del tipo de servicio. Consúltalo con el contador del contribuyente antes de fijarlo en tu código; no lo pongas como constante.

Anticipos y saldos

Un anticipo es una operación con su propio documento; el saldo, otra. No hay una forma fiscal de «facturar el 50 %» de un documento anterior.

Si el trabajo se cancela después del anticipo, la devolución va por Nota de Crédito (si fue CCF) o por Evento de Retorno / invalidación (si fue Factura: retorno cuando se devuelve mercancía, invalidación cuando se cancela la operación entera).


Patrón general: qué guardar en tu base

Sea cual sea el negocio, guarda esto junto a tu propia venta:

CampoPor qué
codigoGeneracionEs la clave de todo: consultar, PDF, JSON, y el documento que ajusta una nota.
numeroControlLo que revisa un auditor y lo que busca el cliente cuando llama.
estadoActualízalo con el webhook.
La Idempotency-Key que usastePara poder reintentar sin duplicar aunque tu proceso se reinicie.
selloRecibidoPrueba de que Hacienda lo aceptó.

Y una regla que ahorra incidentes: no borres una venta que ya tiene codigoGeneracion. Márcala como anulada. El documento existe en Hacienda aunque desaparezca de tu base, y la diferencia aparece en la próxima conciliación.


Volumen mínimo de certificación

Hacienda exige un número de transmisiones satisfactorias en apitest antes de habilitar producción. Los constructores ya existen; lo que falta es el volumen:

TipoMínimo
01 Factura90
03 CCF75
04 Nota de Remisión50
05 Nota de Crédito50
06 Nota de Débito25
07 Comprobante de Retención50
08 Comprobante de Liquidación75
09 Documento Contable de Liquidación50
11 Factura de Exportación90
14 Sujeto Excluido25
15 Donación25
Cada evento (invalidación, retorno)5

Con el firmador y config/emisor.json apuntando a pruebas:

bash
go run ./cmd/probardte -tipo nr
go run ./cmd/probardte -tipo fex
go run ./cmd/probardte -tipo cr -relacionado out/<cg-del-ccf>.json
go run ./cmd/probardte -tipo cl -relacionado out/<cg-del-ccf>.json
go run ./cmd/probardte -tipo dcl
go run ./cmd/probardte -tipo cd
go run ./cmd/probardte -retorno out/<cg-de-la-factura>.json

Repetir hasta cubrir el mínimo de cada tipo. -dry-run construye y valida sin transmitir, para revisar el JSON antes de gastar una transmisión.


Carga masiva desde Excel

POST /api/v1/dte/lotes no emite documentos nuevos: retransmite a Hacienda DTE que ya existen. Para Factura (01), CCF (03) y FSE (14) el recurso es /api/v1/dte/cargas.

  1. Descarga la plantilla del tipo (GET /dte/cargas/plantilla?tipoDte=01).
  2. Una fila en la hoja Documentos es un DTE; las líneas van en Lineas, ligadas por ref. Instrucciones no se importa.
  3. Sube el libro. La validación es síncrona: limpia, completa huecos del catálogo, recalcula e informa. No firma ni gasta correlativo.
bash
curl -s -X POST "$API/api/v1/dte/cargas" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d "{\"tipoDte\":\"01\",\"nombreArchivo\":\"agosto.xlsx\",\"archivo\":\"$B64\"}"
  1. Revisa el resumen. Las filas con error no se emiten. Las de aviso solo si pides incluirAvisos.
  2. Procesa. Responde 202 y trabaja en segundo plano. Cada fila lleva Idempotency-Key = carga:{id}:fila:{n}: un reintento del worker no duplica.
bash
curl -s -X POST "$API/api/v1/dte/cargas/$ID/procesar" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"incluirAvisos":true}'
  1. Consulta GET /dte/cargas/{id} hasta COMPLETADA o COMPLETADA_CON_ERRORES. El Excel de resultado está en GET /dte/cargas/{id}/reporte. Los DTE nacidos se listan con GET /dte/documentos?origen=carga.

En la Factura el precioUni lleva IVA. En CCF y FSE, no. Los totales del Excel son opcionales: si vienen, se comparan con el recálculo y generan aviso; nunca se firman los números del archivo. La fecha de emisión es siempre hoy. Tope: 200 documentos y 2 000 líneas por archivo.


Registro de compras (Anexo 3)

Los JSON que te envían los proveedores no se emiten otra vez: ya están sellados. Se importan al registro de compras y de ahí sale el archivo que se carga en Hacienda.

Acepta CCF (03), notas de crédito (05), notas de débito (06) y facturas de exportación (11). Una Factura a consumidor final (01) o una FSE (14) se rechazan: no van en el Anexo 3.

bash
CONTENIDO=$(base64 < ccf-proveedor.json)
curl -s -X POST "$API/api/v1/gestion/compras/importar" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d "{\"archivos\":[{\"nombre\":\"ccf.json\",\"contenido\":\"$CONTENIDO\"}]}"

La respuesta dice por archivo si entró, si ya estaba (duplicado) o por qué falló. El receptor del DTE tiene que ser el NIT del contribuyente.

Las columnas Q–T (tipo de operación, costo/gasto, sector) no vienen en el JSON. Al importar quedan en Gravada / Gasto / Servicios / Gastos de administración. Se cambian después:

bash
curl -s -X PATCH "$API/api/v1/gestion/compras/$CG" \
  -H "Authorization: Bearer $CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"clasificacion":"1","sector":"2","tipoCostoGasto":"5"}'

El CSV del Anexo 3 no lleva encabezados. Es el archivo que se sube a Hacienda:

bash
curl -s "$API/api/v1/gestion/reportes/anexo-compras?desde=2026-08-01&hasta=2026-08-31&formato=csv" \
  -H "Authorization: Bearer $CLAVE" -o anexo3.csv