Saltar al contenido principal

POST /v1/invoices

Emite una factura legal contra el rango CAI de tu empresa.

peligro

Lee esto antes de escribir tu lógica de reintento

Este endpoint exige Idempotency-Key. Genera la key por factura lógica, persístela antes de la primera llamada y manda la misma key con el mismo body en cada reintento. Detalles en Idempotencia.

consejo

Para probar, usa POST /v1/invoices/preview

Mismo body, mismos cálculos, mismo PDF — pero no consume CAI ni correlativo y no necesita Idempotency-Key. Cada llamada a este endpoint, en cambio, emite una factura legal.

Solicitud

POST /v1/invoices
Authorization: Bearer ctd_sk_...
Content-Type: application/json
Idempotency-Key: order-2026-0917

Headers

HeaderReq.Notas
AuthorizationBearer <tu api key>.
Idempotency-KeyÚnico por factura lógica. Tu propio id de orden sirve; no tiene que ser un UUID. Se conserva ~24 h.

Body

{
"clientId": 1042,
"invoiceDate": "2026-08-10T15:04:05.000Z",
"isCredit": false,
"items": [
{ "externalProductId": "SKU-WEB-HOSTING-01", "quantity": 2 },
{ "externalProductId": "SKU-SOPORTE-MES", "quantity": 1 }
]
}
CampoTipoReq.Notas
itemsarrayAl menos una línea.
clientIdnumberNoId devuelto por POST /v1/clients. Omítelo para una venta sin cliente registrado. Obligatorio si la venta es exonerada.
invoiceDatestringNoISO 8601. Por defecto, la fecha y hora actuales. Facturar con fecha retroactiva está fuera del alcance de v1.
isCreditbooleanNoReservado para v2. true se rechaza con 422. Por defecto false.
peligro

purchaseOrderNumber no es una referencia de compra del cliente

Aunque su nombre lo sugiere, este campo no guarda el número de compra interno de tu cliente. Es la Orden de Compra Exenta — un documento legal de las ventas exoneradas. Enviar uno (con cualquier contenido) exonera toda la factura del ISV y exige que el comprador tenga RTN válido. Es un campo de uso reservado para ventas exoneradas; en una venta ordinaria no lo mandes. Se explica en Conceptos de facturación.

items[]

CampoTipoReq.Notas
externalProductIdstringTu propio identificador de producto. El personal de Contadito lo mapea una sola vez a un producto interno. Un id sin mapear se rechaza con PRODUCT_NOT_FOUND.
quantitynumberMayor que cero.
pricenumberNoPrecio unitario negociado para esta línea. Omítelo para facturar al precio del catálogo. Mayor que cero. Ver abajo.

Fíjate en lo que no mandas: impuesto, descuento, correlativo, CAI. Todo eso se resuelve con el catálogo y la configuración de tu empresa. Ver Conceptos de facturación.

Precio negociado (price)

Por defecto el unitPrice sale de la lista de precios de tu empresa. Si esta venta se cerró a otro precio, mándalo en la línea:

{
"items": [
{ "externalProductId": "SKU-WEB-HOSTING-01", "quantity": 2, "price": 1500 },
{ "externalProductId": "SKU-SOPORTE-MES", "quantity": 1 }
]
}

La primera línea se factura a 1500 por unidad; la segunda, al precio del catálogo. Reglas:

  • Va en la moneda del producto, no necesariamente en lempiras. Un producto en dólares recibe un precio en dólares y se convierte con la misma tasa de cambio que se le habría aplicado al precio de catálogo.
  • Debe ser mayor que cero. No sirve para regalar una línea.
  • No crea ni actualiza nada en el catálogo. Aplica solo a esta factura.
  • El ISV lo sigue poniendo el producto: mandar price cambia la base gravable, no la tasa ni la condición de exento.
aviso

Productos parcialmente gravados

En un producto marcado como parcialmente gravado, el monto exento por unidad sigue viniendo del catálogo: no se reescala con tu precio. Si bajas el precio de una línea así, la porción exenta se mantiene y la gravada absorbe toda la diferencia. Confírmalo con POST /v1/invoices/preview antes de emitir.

Respuesta 201

{
"id": 8801,
"invoiceNumber": 1573,
"cai": "A1B2C3-D4E5F6-A1B2C3-D4E5F6-A1B2C3-12",
"invoiceDate": "2026-08-10T15:04:05.000Z",
"clientId": 1042,
"importe": 2000,
"discountTotal": 0,
"exemptedAmount": 0,
"exoneratedAmount": 0,
"totalTaxes": 300,
"total": 2300,
"totalInLetters": "DOS MIL TRESCIENTOS LEMPIRAS EXACTOS",
"isPaid": true,
"isAnnulled": false,
"taxBreakdown": [{ "rate": 15, "taxAmount": 300, "taxedAmount": 2000 }],
"lines": [
{
"externalProductId": "SKU-WEB-HOSTING-01",
"quantity": 2,
"unitPrice": 1000,
"importe": 2000,
"discountAmount": 0,
"subTotal": 2000,
"exemptAmount": null
}
],
"replayed": false,
"pdfBase64": "JVBERi0xLjQK..."
}
CampoTipoDescripción
idnumberÚsalo en GET /v1/invoices/{id}. Guárdalo.
invoiceNumbernumberCorrelativo del rango CAI.
caistring | nullCAI del SAR bajo el cual se emitió.
invoiceDatestringISO 8601.
clientIdnumber | null
importenumberSuma del importe de las líneas, antes de descuentos.
discountTotalnumberDescuentos aplicados.
exemptedAmountnumberMonto no sujeto a ISV por naturaleza del bien.
exoneratedAmountnumberMonto cubierto por una exoneración del cliente.
totalTaxesnumberISV total.
totalnumberLo que paga el cliente.
totalInLettersstringTotal en letras, como exige el comprobante.
isPaidbooleanSiempre true en v1 (solo contado).
isAnnulledboolean
taxBreakdownarrayUna entrada por tasa de ISV.
linesarrayLas líneas facturadas.
replayedbooleantrue si esta respuesta es la repetición de una solicitud anterior con la misma Idempotency-Key.
pdfBase64string | nullEl PDF A4 de la factura en base64. null si la factura se emitió pero el PDF no pudo generarse: la factura es válida de todos modos y el PDF sigue disponible en GET /v1/invoices/{id}/pdf.

taxBreakdown[]

CampoTipoDescripción
ratenumberTasa de ISV en porcentaje (p. ej. 15).
taxAmountnumberImpuesto cobrado a esa tasa.
taxedAmountnumberBase gravable a la que se aplicó.

lines[]

CampoTipoDescripción
externalProductIdstring | nullTu id, tal como lo mandaste.
quantitynumber
unitPricenumberPrecio unitario facturado, sin impuestos.
importenumberunitPrice × quantity, antes de descuento.
discountAmountnumber
subTotalnumberTotal de la línea tras el descuento, antes de impuestos.
exemptAmountnumber | nullSiempre null en lecturas: no se persiste el exento por línea. Usa exemptedAmount del encabezado.
consejo

replayed: true no es un error

Significa que tu reintento no emitió una segunda factura y que estás viendo la original. Es un éxito.

Errores

HTTPcodeCausa
400MISSING_IDEMPOTENCY_KEYFalta el header.
400VALIDATION_FAILEDBody inválido o con campos desconocidos.
400PRODUCT_NOT_FOUNDUn externalProductId no está mapeado.
400EXTERNAL_INVOICE_NO_ITEMSitems vacío.
400EXTERNAL_INVOICE_CLIENT_NOT_FOUNDEl clientId no existe en tu empresa.
400EXTERNAL_INVOICE_EXONERATION_REQUIRES_RTNEl cliente exonerado no tiene RTN.
401MISSING_API_KEY / INVALID_API_KEYCredencial.
403INTEGRATION_NOT_ENABLEDIntegración no habilitada.
409IDEMPOTENCY_KEY_REQUEST_MISMATCHMisma key, body distinto.
409INTEGRATION_MISSING_*, PRICE_NOT_FOUNDConfiguración o catálogo incompletos.
422EXTERNAL_INVOICE_CREDIT_NOT_SUPPORTEDisCredit: true.
500INTERNAL_ERRORFalla nuestra: reintenta con la misma key.

Ver el catálogo completo de errores.