Emitir un comprobante
POST /:id-empresa/invoices
La emisión es síncrona: la API le pide el CAE a ARCA y responde recién con la autorización en mano. Una emisión sana tarda entre 1 y 3 segundos.
Los importes no se envían: mandás cantidad, precio y alícuota por ítem, y la API calcula netos, IVA y totales con el redondeo que exige ARCA. Ver cálculo de totales.
priceIncludesVat decide cómo se lee items[].price, y vale para todo el comprobante:
true(por defecto): el precio ya trae el IVA adentro y la API lo extrae.false: el precio es neto y la API le suma el IVA por encima.
Un ítem de $121 al 21% da un comprobante de $121 con el default y de $146,41 con false. No hay forma de adivinar cuál querías: es el parámetro más importante del pedido después del importe mismo.
El default sigue la convención de ARCA para los comprobantes B —en sus ejemplos oficiales la columna de la tabla de ítems se titula "Precio Unitario (incluye IVA)"— y es el número que casi cualquier vendedor tiene a mano: el que ve el cliente. Si tu sistema guarda precios netos, mandá false.
En comprobantes C (11, 12, 13) el parámetro no hace nada: el emisor no traslada IVA, así que el precio es el precio.
Campos principales
| Campo | Obligatorio | Tipo | En ARCA | Descripción |
|---|---|---|---|---|
idempotency | Sí | String | — | Clave única de este comprobante. Máx. 128 caracteres: letras, números, - y _. Ver idempotencia. |
posNumber | Sí | Integer | PtoVta | Punto de venta. Tiene que estar dado de alta en NanoFactura y habilitado en ARCA. |
voucherType | Sí | Integer | CbteTipo | Tipo de comprobante (tabla). |
concept | Sí | Integer | Concepto | 1 productos · 2 servicios · 3 productos y servicios. |
voucherDate | Sí | String | CbteFch | Fecha del comprobante, YYYYMMDD. |
customer | Sí | Object | — | Datos del receptor (ver tabla). |
items | Sí | Array | — | Líneas de detalle, mínimo 1 (ver tabla). |
serviceFrom | Condicional | String | FchServDesde | Inicio del servicio, YYYYMMDD. Obligatorio si concept es 2 o 3. |
serviceTo | Condicional | String | FchServHasta | Fin del servicio, YYYYMMDD. Obligatorio si concept es 2 o 3. |
serviceExpiration | Condicional | String | FchVtoPago | Vencimiento del pago, YYYYMMDD. Obligatorio si concept es 2 o 3. |
associatedVouchers | Condicional | Array | CbtesAsoc | Comprobante que se modifica. Necesario en notas de crédito y débito (ver tabla). |
currency | No | Object | — | Moneda. Por defecto, pesos argentinos (ver tabla). |
priceIncludesVat | No | Boolean | — | Si los items[].price traen el IVA incluido. Por defecto true. Ver cálculo de totales. |
tributes | No | Array | Tributos | Impuestos y percepciones que no son IVA (ver tabla). |
emails | No | Array | — | Destinatarios del envío automático del PDF (ver tabla). |
settings | No | Object | — | Precisión decimal de cantidades y precios (ver tabla). |
legend | No | String | — | Leyenda impresa en el comprobante. Máx. 200 caracteres. |
comments | No | String | — | Comentario interno del emisor, no se imprime. Máx. 512 caracteres. |
externalReference | No | String | — | Identificador de tu sistema. Máx. 128 caracteres. Filtrable en el listado. |
additionalData | No | Object | — | Objeto clave-valor libre que se guarda junto al comprobante. Las claves se definen en el punto de venta (ver datos adicionales) y se pueden imprimir en la plantilla del PDF. |
cancelsInSameForeignCurrency | No | String | CanMisMonExt | 'S' o 'N'. Sólo aplica en moneda extranjera. |
receiverTaxConditionId | No | Integer | CondicionIVAReceptorId | Si se envía, pisa a customer.taxType. Normalmente no hace falta. |
externalReferenceEs el campo que ata el comprobante a tu operación (número de pedido, id de venta). Es el único que después podés usar para buscarlo desde tu sistema sin llevar una tabla de equivalencias propia.
Qué campos son obligatorios según tu condición fiscal y el tipo de comprobante: ver Equivalencia de campos con ARCA.
Cliente (customer)
| Campo | Obligatorio | Tipo | En ARCA | Descripción |
|---|---|---|---|---|
docType | Sí | Integer | DocTipo | Tipo de documento (tabla). |
docNumber | Sí | String | DocNro | Número sin guiones ni puntos. |
name | Sí | String | — | Nombre, apellido o razón social. |
taxType | Sí | Integer | CondicionIVAReceptorId | Condición frente al IVA (tabla). |
address | No | String | — | Domicilio. Se imprime en el comprobante. |
stateId | Recomendado | Integer | null | — | Provincia (tabla). |
stateIdEs opcional a nivel técnico, pero es el único dato estructurado de ubicación del receptor: address es texto libre y no se puede agrupar. Con la provincia cargada podés después responder por jurisdicción —dónde está tu facturación, qué te corresponde para Ingresos Brutos, cómo se reparte tu cartera— y armar reportes de impuestos sin trabajo manual. Sin ella, esa pregunta no tiene respuesta y no hay forma de reconstruirla hacia atrás.
Si genuinamente no sabés de dónde es el receptor, mandá null: significa "no informada" y es mejor que inventar 0, que es CABA.
Podés reenviar tal cual el stateId que devuelve la consulta de padrón.
Ítems (items)
Mínimo uno. Cada objeto es una línea de detalle.
| Campo | Obligatorio | Tipo | Descripción |
|---|---|---|---|
description | Sí | String | Descripción del producto o servicio. |
quantity | Sí | Number | Cantidad. Mínimo 0.0001. |
price | Sí | Number | Precio unitario antes del descuento. Con el IVA incluido, salvo que mandes priceIncludesVat: false. Mínimo 0. |
taxType | Sí, salvo en C | Integer | Alícuota de IVA de la línea (tabla). Los comprobantes C no la llevan: no discriminan IVA. |
code | No | String | Tu código interno del producto. |
unitType | No | Integer | Unidad de medida (tabla). Por defecto 7 (unidades). |
discount | No | Number | Bonificación del renglón entero, no por unidad (detalle). Por defecto 0. |
discountType | No | Enum | 'percentage' (defecto) o 'amount'. |
Moneda (currency)
| Campo | Obligatorio | Tipo | En ARCA | Descripción |
|---|---|---|---|---|
id | No | String | MonId | Código de moneda (tabla). Por defecto 'PES'. |
quotation | No | Number | MonCotiz | Cotización contra el peso. Por defecto 1. |
Si usás una moneda distinta de PES, mandá la cotización del día: no se consulta sola.
Tributos (tributes)
Impuestos y percepciones que no son IVA: Ingresos Brutos, tasas municipales, impuestos internos, percepciones.
| Campo | Obligatorio | Tipo | En ARCA | Descripción |
|---|---|---|---|---|
type | Sí | Integer | Id | Tipo de tributo (tabla). |
baseAmount | Sí | Number | BaseImp | Base imponible. |
aliquot | Sí | Number | Alic | Alícuota aplicada. |
subtotal | Sí | Number | Importe | Importe del tributo. |
description | No | String | Desc | Descripción. |
A diferencia de los ítems, acá el importe sí lo calculás vos: el subtotal se informa tal como lo mandás.
Comprobantes asociados
Vincula la factura original que una nota de crédito o nota de débito modifica.
| Campo | Obligatorio | Tipo | En ARCA | Descripción |
|---|---|---|---|---|
voucherType | Sí | Integer | Tipo | Tipo del comprobante asociado. |
posNumber | Sí | Integer | PtoVta | Punto de venta del asociado. |
voucherNumber | Sí | Integer | Nro | Número del asociado. |
cuit | No | String | Cuit | CUIT del emisor, sólo si es distinto al tuyo. |
{
"voucherType": 13,
"associatedVouchers": [{ "voucherType": 11, "posNumber": 1, "voucherNumber": 45 }]
}
La nota de crédito debe ser de la misma letra que la factura que anula. Ver tipos de comprobante.
Correos (emails)
Despacha el PDF por correo en segundo plano, sin bloquear la respuesta.
| Campo | Obligatorio | Tipo | Descripción |
|---|---|---|---|
email | Sí | String | Dirección del destinatario. |
name | No | String | Nombre del destinatario. |
comments | No | String | Texto a incluir en el cuerpo del correo. |
pdfVersions | No | Array | Copias a adjuntar: 1 original, 2 duplicado, 3 triplicado. Por defecto [1]. |
Máximo 3 destinatarios por comprobante, contando los que agregues después.
Precisión (settings)
| Campo | Tipo | Descripción |
|---|---|---|
quantityPrecision | Integer | Decimales de la cantidad: 2 (defecto), 4 o 6. |
pricePrecision | Integer | Decimales mínimos del precio unitario: 2 (defecto), 4 o 6. Ver el comprobante impreso cierra solo. |
Subilos si vendés por unidades fraccionadas o con precios de más de dos decimales. Los importes resultantes se redondean siempre a 2 decimales, que es lo que exige ARCA.
Cálculo de totales
Por cada ítem, en este orden y redondeando a 2 decimales en cada paso:
subtotal = quantity × price
descuento = discountType === 'percentage' ? subtotal × discount / 100 : discount
importe = subtotal − descuento
Bonificaciones
El descuento se aplica sobre el renglón, ya multiplicado por la cantidad, y nunca sobre el precio unitario:
| 10 unidades a $100 | Descuenta | Importe | |
|---|---|---|---|
discount: 10, discountType: 'percentage' | subtotal $1.000 | $100 | $900 |
discount: 10, discountType: 'amount' | subtotal $1.000 | $10 | $990 |
amount es del renglón, no por unidaddiscount: 10 con discountType: 'amount' son $10 en todo el renglón, sin importar la cantidad. Si querés $10 por unidad, el descuento es 10 × cantidad.
El descuento va en la misma base que el precio: con el default, discount: 10 de tipo amount son $10 que el cliente no paga; con priceIncludesVat: false, son $10 menos de neto. En porcentaje da lo mismo en las dos bases, porque un porcentaje no depende de la base.
La bonificación no viaja a ARCA: FECAESolicitar no tiene campo para ella, así que se declara el neto ya descontado. Se imprime en el comprobante, donde la fila cierra: precio unitario × cantidad − descuento = importe.
A dónde va el importe
El destino del importe depende del taxType de la línea:
taxType | Tratamiento | Suma a | ¿Entra en el desglose de IVA? |
|---|---|---|---|
4, 5, 6, 8, 9 | Gravado | Neto gravado | Sí, con su IVA |
3 | Gravado al 0% | Neto gravado | Sí, con IVA 0 |
2 | Exento | Importe exento | No |
1 u otro | No gravado | Importe no gravado | No |
En las líneas gravadas, de dónde sale el IVA depende de priceIncludesVat:
true (defecto) → base = importe / (1 + alícuota) IVA = importe − base
false → base = importe IVA = base × alícuota
La resta del primer caso es deliberada: garantiza que base + IVA dé exactamente el importe del renglón, sin el centavo que dejaría redondear las dos puntas por separado.
Y el total del comprobante es la suma de los cinco importes:
neto gravado + IVA + importe exento + importe no gravado + otros tributos
Con el default eso equivale a la suma de los importes de los renglones más los tributos: los precios que mandaste son finales y cierran contra el total.
Redondeo
Todo se redondea a 2 decimales con medio hacia arriba, y en este orden: primero cada renglón, después las sumas. Como los renglones ya vienen redondeados, los totales son sumas exactas de números de dos decimales, y de ahí salen las tres igualdades que sostienen el comprobante:
- El neto gravado es la suma exacta de las bases del desglose por alícuota.
- El IVA es la suma exacta de los impuestos de ese mismo desglose.
- El total es la suma de los cinco importes de arriba.
Cuando el IVA se extrae de un precio final, la base es la que queda redondeada y el impuesto es la diferencia. Un ítem de $100 al 21% da base 82,64 e IVA 17,36, aunque 82,64 × 0,21 sea 17,3544, que redondearía a 17,35.
El centavo está bien puesto: lo que tiene que cerrar es 82,64 + 17,36 = 100,00, el importe que el cliente paga. Es el mismo criterio de los ejemplos oficiales de ARCA, que declaran una base de 9.917,36 con un IVA de 2.082,64.
El comprobante impreso cierra solo
Lo que sale impreso es exactamente lo que se declara: la suma de los subtotales de los renglones es el neto gravado, el desglose de IVA impreso es el que se informa, y el total es el mismo. Y cada fila cierra por sí misma:
precio unitario × cantidad − descuento = importe del renglón
Cuando la letra imprime una base distinta de la que mandaste, el precio unitario es un número derivado y la API le da los decimales que esa igualdad necesite. Una Factura A de 50 unidades a $100 finales imprime el unitario neto como 82,6446, no como 82,64: con dos decimales la fila mostraría 4.132,00 contra un subtotal de 4.132,23.
settings.pricePrecision es el mínimo de decimales de esa columna, no el máximo: si mandás los precios en la base que el comprobante imprime, se usa tal cual el número que pusiste.
Qué imprime cada letra
El neto y el IVA se calculan y se informan siempre, en las tres letras. Lo que cambia es el comprobante impreso, y de eso se ocupa la API sola:
| Precio unitario y subtotal del renglón | Columna "Subtotal c/IVA" | Al pie | |
|---|---|---|---|
| A | Netos | Sí | Neto gravado y una línea por alícuota |
| B | Finales, con IVA | No | Subtotal y el "IVA Contenido" de transparencia fiscal |
| C | Finales | No | Subtotal |
Mandes los precios en la base que los mandes, el PDF sale igual: es el mismo comprobante declarado de dos formas.
En Factura C, Nota de Débito C y Nota de Crédito C (11, 12, 13) todo el importe va al neto, el IVA queda en 0 y no hay desglose por alícuota. Por eso los ítems de un comprobante C no llevan taxType: no hay alícuota que declarar. Si llega igual, se acepta y se ignora.
Es correcto y es lo que corresponde: un monotributista no discrimina IVA.
Lo que la API completa sola
| Qué | De dónde sale |
|---|---|
| Número del comprobante | Correlativo por punto de venta y tipo, que reserva la API. |
| Neto gravado | Suma de las bases gravadas (y de todas las líneas en comprobantes C). |
| IVA | Suma del IVA de cada línea. |
| Importe exento | Suma de las líneas exentas. |
| Importe no gravado | Suma de las líneas no gravadas. |
| Otros tributos | Suma de los tributes[].subtotal. |
| Total | La suma de todo lo anterior. |
| Desglose por alícuota | Una entrada por cada alícuota gravada presente en los ítems, el 0% incluido. |
Cómo se llama cada uno de estos importes del lado de ARCA, si venís de una integración directa contra el webservice: Equivalencia de campos.
Idempotencia
idempotency identifica un comprobante. Es lo que hace segura la red: si se corta la conexión y no sabés si el comprobante se emitió, reintentás con la misma clave y la API te devuelve el original en vez de emitir uno nuevo.
{ "ok": true, "result": { "id": 1485, "cae": "76251489623547", "idempotencyHit": true } }
idempotencyHit: true significa "esto ya existía, no se emitió nada nuevo".
Cómo armar la clave
Una clave identifica exactamente un comprobante. No se reparte por tipo ni por punto de venta: si mandás una clave que ya pertenece a un comprobante de otro punto de venta o de otro tipo, el pedido se rechaza con 409, y es un error permanente.
{
"ok": false,
"error": "La clave de idempotencia ya corresponde al comprobante 1-6-45, no al que se está pidiendo (2-6). Usá una clave distinta para cada comprobante.",
"code": "VALIDATION_ERROR"
}
Lo más robusto no es un UUID al azar: es una clave determinística, que podés recalcular sin haberla guardado. Si tu proceso se cae después de emitir pero antes de registrar el resultado, con un UUID perdiste la clave y el reintento duplica el comprobante; con una clave derivada la volvés a calcular igual y recuperás el original.
La condición es que incluya todo lo que hace único al comprobante, no sólo la operación:
{tuIdDeVenta}-{voucherType}-{posNumber}
venta-8842-11-1 Factura C de la venta 8842
venta-8842-13-1 su nota de crédito
Así el mismo pedido puede tener su factura y su nota de crédito sin chocar. Sólo venta-8842 para las dos daría 409 en la segunda.
Si preferís un UUID, es igual de válido —pero guardalo junto a la venta antes de emitir, no después.
Sólo se admiten letras, números, - y _, hasta 128 caracteres. Las claves son independientes por entorno: la misma puede existir en homologación y en producción sin pisarse, así que podés ensayar con las claves reales.
Respuesta (201 Created)
{
"ok": true,
"result": {
"id": 1485,
"voucherNumber": 45,
"cae": "76251489623547",
"totalAmount": 142500,
"arcaObservations": null,
"idempotencyHit": false
},
"quota": {
"type": "invoices",
"granted": 500,
"used": 138,
"remaining": 362,
"overage": 0,
"periodEnd": "2026-09-16",
"breakdown": { "production": 112, "homologation": 26 }
}
}
| Campo | Descripción |
|---|---|
id | Identificador interno. Es el que se usa en las rutas de PDF y email. |
voucherNumber | Número correlativo dentro del punto de venta y tipo. |
cae | CAE otorgado por ARCA. |
totalAmount | Importe total calculado. |
arcaObservations | null, o un array [{ code, msg }] con observaciones que ARCA devolvió aprobando igual el comprobante. Conviene registrarlas. |
idempotencyHit | true si la clave ya correspondía a un comprobante existente. |
id, cae, voucherNumber, posNumber y voucherType. Con eso alcanza para todo lo demás.El PDF no está listo en este momento: se genera en segundo plano. Ver PDF y envío por email.
El objeto quota
Va al lado de result, no adentro: es el estado del cupo mensual de tu plan, no un dato del comprobante. Sirve para frenar antes de chocar sin consultar nada aparte.
| Campo | Descripción |
|---|---|
granted | Cupo total del período (los adicionales contratados ya están sumados). |
used | Consumido, incluyendo este comprobante. |
remaining | granted − used, con piso en 0. Nunca es negativo. |
periodEnd | Día en que se renueva el cupo. |
breakdown | Consumo por entorno. |
Tres cosas a tener en cuenta: el cupo es mensual y no se acumula; homologación consume el mismo cupo; y quota puede venir en null (en reintentos idempotentes, que no consumen) — tu cliente tiene que tolerarlo. null no significa "cero disponible", significa "no hay nada que informar".
quota no es parte del contrato estableA diferencia de result, la forma de este objeto puede cambiar: pueden aparecer campos nuevos, y los actuales pueden dejar de estar. Usalo para mostrar o alertar, no como algo de lo que dependa tu emisión.
En concreto: leelo siempre de forma defensiva (quota?.remaining), tolerá que sea null y no lo uses como condición para decidir si emitís. La única fuente confiable de que el cupo se agotó es el error 429 SUBSCRIPTION_QUOTA_EXCEEDED.
Errores
Los cuatro que vas a ver en la práctica:
| HTTP | code | Qué pasó | ¿Reintentar? |
|---|---|---|---|
400 | VALIDATION_ERROR | Payload inválido o punto de venta inexistente | No, corregí el pedido |
422 | ARCA_ERROR | ARCA rechazó el comprobante | No, corregí el pedido |
429 | VOUCHER_LOCK_TIMEOUT | Otra emisión del mismo punto de venta sigue en curso | Sí, con la misma idempotency |
504 | — | Se agotó el tiempo del pedido | Sí, con la misma idempotency |
Un 504 no significa que el comprobante no se haya emitido. Reintentá con la misma idempotency: si se había emitido, lo recuperás.
La lista completa, con qué hacer en cada caso: Errores.
Emitir en volumen
ARCA exige numeración consecutiva por punto de venta y tipo, así que la API emite de a un comprobante por vez para cada combinación posNumber + voucherType. Si llega un pedido mientras otro se está autorizando, espera su turno solo (hasta 20 segundos).
Secuenciá los pedidos de un mismo punto de venta y tipo. Lanzarlos en paralelo no acelera nada —igual se serializan— y sólo produce errores 429. Distintos puntos de venta sí pueden emitir en paralelo. No intentes paralelizar llamados a la API, solo generarás errores.
Ver Límites y buenas prácticas.
Ejemplo completo
{
"idempotency": "9f1c3a5e-7b2d-4c81-9e6f-0a2b4c6d8e10",
"posNumber": 1,
"voucherType": 11,
"concept": 2,
"voucherDate": "20260613",
"serviceFrom": "20260601",
"serviceTo": "20260613",
"serviceExpiration": "20260630",
"customer": {
"docType": 96,
"docNumber": "35888999",
"name": "Pedro López",
"address": "Av. de Mayo 500, CABA",
"stateId": 0,
"taxType": 5
},
"items": [
{
"code": "SRV-DEV",
"description": "Servicios de desarrollo de software — Junio 2026",
"quantity": 1,
"unitType": 7,
"price": 150000,
"discount": 5,
"discountType": "percentage"
}
],
"emails": [
],
"externalReference": "EXT-ORD-99882",
"legend": "Gracias por su compra"
}
Implementaciones completas en JavaScript, PHP y Python: Ejemplos.