Emitir comprobantes por API
Una sola API para emitir facturas, notas de crédito y notas de débito electrónicas en Argentina (ARCA) y Paraguay (SIFEN / e-Kuatia). La emisión es siempre asíncrona: la API responde al instante y el comprobante se autoriza en segundos; te enterás por webhook firmado, por polling o por el link público.
1. Autenticación
Todas las llamadas a /api/v1/* llevan el header X-Api-Key.
Las keys se emiten por integración y tienen una o dos audiencias:
| Audiencia | Permite |
|---|---|
facturauno.issue | Emitir, cancelar, suscribir webhooks — y también todo lo de lectura. |
facturauno.read | Solo consultar (detalle, listado, XML, catálogo). |
Auth inválida o ausente responde 401/403 con cuerpo genérico.
Los errores de negocio tienen siempre la forma
{ "error": "<código>", "detail": "…" }.
curl -s https://api.facturauno.com/api/v1/invoices \
-H "X-Api-Key: $FACTURAUNO_KEY"
2. Emitir un comprobante issue
POST /api/v1/invoices encola la emisión y responde
202 de inmediato. Por defecto el modelo es NETO: los
unitPrice van sin IVA; el impuesto se calcula por ítem según su
taxRateCode. Si vendés a precio final (IVA incluido) y tenés
que facturar exactamente lo cobrado, usá el modo
pricesIncludeTax.
curl -s -X POST https://api.facturauno.com/api/v1/invoices \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{
"issuerId": "0b0e8a1e-1111-2222-3333-444455556666",
"docClass": "INVOICE",
"idempotencyKey": "pedido-84512",
"currency": "PES",
"recipient": {
"docType": "CUIT",
"docNumber": "30123456789",
"name": "Cliente SRL",
"email": "facturas@cliente.com",
"fiscalProfile": { "condicionIVAReceptorId": 1 }
},
"items": [
{ "description": "Servicio de plataforma — julio", "qty": 1,
"unitPrice": 100000.00, "taxRateCode": "IVA_21" }
],
"ar": { "concepto": 2, "servicioDesde": "2026-07-01",
"servicioHasta": "2026-07-31", "vtoPago": "2026-08-15" },
"notifyEmail": "facturas@cliente.com",
"webhookUrl": "https://miapp.com/hooks/facturauno"
}'
Campos del body
| Campo | Detalle |
|---|---|
issuerId | Obligatorio. El emisor (empresa) por el que se factura. |
emissionPointId | Opcional; si el emisor tiene un solo punto de emisión activo, se resuelve solo. |
docClass | INVOICE · CREDIT_NOTE · DEBIT_NOTE. |
docTypeLocal | Opcional. Override del tipo local (AR: CbteTipo "1"/"6"/"11"…). Sin él, el tipo se deriva de la condición fiscal emisor × receptor. |
relatedInvoiceId | Obligatorio en NC/ND: el comprobante que se ajusta (ver §5). |
idempotencyKey | Obligatorio, único por emisor (máx. 80). Reintentá con la misma key sin miedo (ver Idempotencia). |
externalRef | Opcional. Tu referencia (id de pedido/cobro); vuelve en consultas y webhooks. |
currency | Código de moneda del WS: PES (pesos AR) o DOL (dólares; requiere exchangeRate). |
recipient | Por referencia (recipientId ya validado) o inline: docType (AR: CUIT|CUIL|DNI|CF · PY: RUC con DV|CI (cédula paraguaya)|PASAPORTE|CIE (cédula extranjera: documento de identidad de otro país — DNI argentino, RG/CPF brasileño, CI uruguaya…; nunca como CI)), docNumber, name, email, address y fiscalProfile.condicionIVAReceptorId (RG 5616 — obligatoria en AR; ids en el catálogo). En PY el número de CI/PASAPORTE/CIE se normaliza (sin puntos ni espacios; letras en mayúscula en PASAPORTE/CIE) y tiene que quedar con 1 a 20 letras, dígitos o guiones — si no, 422. recipient: null = consumidor final innominado (en PY solo facturas, y por menos de 7.000.000 guaraníes — en USD, total × exchangeRate: desde ese monto SIFEN exige identificar al receptor (D208c, rechazo 1321; no rige con py.tipoTransaccion 13, muestras médicas) y la API responde 422; las NC/ND exigen receptor identificado — ver §5). |
pricesIncludeTax | Opcional, default false. Con true, items[].unitPrice es el precio FINAL (IVA incluido) y se factura exactamente lo cobrado (ver Precios con IVA incluido). |
expectedTotal | Opcional. El total que cobraste. Si el total calculado no es exactamente ese, responde 422 y no se crea nada. En AR vale con o sin pricesIncludeTax; en PY exige pricesIncludeTax: true (ver expectedTotal). |
supersedesInvoiceId | Opcional. El comprobante REJECTED (mismo emisor, misma docClass, mismo ambiente) que este viene a reemplazar. Declararlo evita la doble factura: nadie podrá re-encolar el rechazado mientras este siga vivo (ver Reemplazar un comprobante rechazado). |
items[] | description, qty, unitPrice NETO (o FINAL con pricesIncludeTax), taxRateCode (AR: IVA_21|IVA_10_5|IVA_27|IVA_5|IVA_2_5|IVA_0|EXENTO|NO_GRAVADO · PY: IVA_10|IVA_5|EXENTA|EXONERADO), discountAmount opcional. |
py | Opciones PY: tipoTransaccion (iTipTra 1-13), condicionVenta CONTADO|CREDITO (+plazoCredito), indicadorPresencia (iIndPres) y motivoEmision (iMotEmi de NC/ND). Moneda PYG (entera) o USD (exchangeRate obligatorio). El comprobante autorizado devuelve el CDC de 44 dígitos en authCode y el PDF es el KuDE con QR verificable en e-Kuatia. |
ar | Opciones AR: concepto (1=Productos, 2=Servicios, 3=Ambos — 2/3 exigen servicioDesde/servicioHasta), vtoPago, y condicionIVAReceptorId que pisa la del perfil si viene. |
notifyEmail | Opcional. Al autorizarse, el receptor recibe un mail con el link público. |
webhookUrl | Opcional. URL https adicional para los webhooks de ESTE comprobante (además de tus suscripciones). |
Respuesta
HTTP/1.1 202 Accepted
{ "invoiceId": "6f2b…", "status": "QUEUED", "docTypeLocal": "1",
"publicUrl": "https://facturauno.com/c/<token>",
"totalAmount": 121000.00, "pricesIncludeTax": false }
totalAmount y pricesIncludeTax salen del comprobante
persistido: en la misma respuesta confirmás que se va a facturar lo que cobraste
(en un reintento son los del comprobante original). Un canónico inválido responde
422 validation_failed con el detalle y no crea nada.
Una salvedad en Paraguay con el modelo neto (sin
pricesIncludeTax): ese totalAmount es el total
persistido, calculado por alícuota, y el dTotGralOpe que se firma
ante SIFEN se calcula por ítem — pueden diferir en guaraníes. Si necesitás que el número
de la respuesta sea exactamente el que se firma, usá
pricesIncludeTax (ahí son el mismo por
construcción). En Argentina el persistido es siempre el ImpTotal que va a
ARCA, con o sin el flag.
Idempotencia
idempotencyKey del mismo emisor devuelve
200 con el comprobante existente (mismo body que el 202, con los
status y totales actuales) — no se re-emite ni se re-encola.
El reintento se resuelve ANTES de validar el pedido: si la key ya
existe, responde el original aunque el pedido hoy no validaría (emisor pausado, punto de
emisión dado de baja, receptor editado). Un reintento nunca se convierte en un 422.
Lo que sí se compara es que sea el mismo pedido: si la key ya existe y el
expectedTotal que mandás no coincide con el total del comprobante existente,
el docClass es otro (p.ej. una nota de crédito con la key de la factura), el
pricesIncludeTax es distinto del que tiene el comprobante existente (ausente
cuenta como false: los mismos números leídos como precios finales o como netos
son otro comprobante), o el supersedesInvoiceId es otro (ausente
cuenta como ninguno), responde 409
idempotency_conflict con el detalle — no se crea nada y no se
devuelve el original como si fuera la respuesta a ese pedido. Por eso conviene mandar
expectedTotal siempre, y repetir el pedido tal cual (flag incluido):
es lo que permite detectar una key reutilizada por error.
HTTP/1.1 409 Conflict
{ "error": "idempotency_conflict",
"detail": "la idempotencyKey ya se usó para un comprobante con otro total: expectedTotal=120, total del comprobante existente=115 (invoiceId 6f2b…)" }
Precios con IVA incluido — pricesIncludeTax
Para quien vende a precio final (entradas, retail): con el modelo neto tendrías que
des-ivar antes de llamar, y des-ivar y re-ivar no son operaciones inversas — una
entrada de $ 100,00 da neto 82,64 + IVA 17,35 = 99,99; una de
₲ 50.000 da 50.001. Con pricesIncludeTax: true mandás
lo que cobraste y el IVA se calcula por adentro: el total del comprobante es
exactamente la suma de qty × unitPrice. Es opt-in: sin el flag nada cambia.
Consumidor final innominado en Argentina, precio final (Factura B):
curl -s -X POST https://api.facturauno.com/api/v1/invoices \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{
"issuerId": "0b0e8a1e-1111-2222-3333-444455556666",
"docClass": "INVOICE",
"idempotencyKey": "venta-991204",
"externalRef": "orden-991204",
"currency": "PES",
"pricesIncludeTax": true,
"expectedTotal": 115.00,
"recipient": null,
"items": [
{ "description": "Entrada general", "qty": 1,
"unitPrice": 100.00, "taxRateCode": "IVA_21" },
{ "description": "Cargo por servicio", "qty": 1,
"unitPrice": 15.00, "taxRateCode": "IVA_21" }
]
}'
HTTP/1.1 202 Accepted
{ "invoiceId": "9c41…", "status": "QUEUED", "docTypeLocal": "6",
"publicUrl": "https://facturauno.com/c/<token>",
"totalAmount": 115.00, "pricesIncludeTax": true }
El comprobante sale por 115,00: neto gravado 95,04 + IVA 19,96.
| País | Cómo se calcula |
|---|---|
| AR | Por alícuota (así valida ARCA): se suman los brutos de cada taxRateCode, base = bruto ÷ (1 + tasa) redondeada a 2 decimales e IVA por diferencia (bruto − base) → el total es exacto. IVA_0/EXENTO/NO_GRAVADO: base = bruto, IVA 0. El neto por renglón (items[].lineTotal) es informativo: cada línea lleva bruto ÷ (1 + tasa) redondeado, y el residuo contra la base de la alícuota se reparte de a 0,01 por línea (primero las líneas que más perdieron al redondear), así que un renglón puede diferir 1 centavo de su propio bruto ÷ (1 + tasa) — los totales, el IVA por alícuota y lo que va a ARCA no se mueven. |
| PY | Por ítem (así liquida SIFEN): total del ítem = qty × unitPrice, IVA = total × tasa ÷ (100 + tasa), redondeado a la moneda (PYG entero, USD 2 decimales). Una entrada de ₲ 50.000 al 10%: IVA 4.545, base 45.455, total 50.000 — el mismo número en el XML firmado y en el KuDE. |
Restricciones del modo (todas 422 validation_failed):
discountAmounttiene que ser 0 (o no venir): mandá el precio ya descontado. Un descuento neto sobre un precio final no tiene lectura única.unitPriceno puede traer más decimales que la moneda:PYGentero;PES/DOL/USDhasta 2. Un precio final de 100,005 no se puede cobrar.qtyadmite hasta 4 decimales.
GET /api/v1/invoices/{id}, items[].unitPrice,
discountAmount y lineTotal son netos en todos los
comprobantes, con o sin el flag (con el flag, unitPrice es
lineTotal ÷ qty y puede traer hasta 6 decimales: no es un precio para
mostrar). Los importes IVA incluido — tal cual los mandaste — van en
items[].grossUnitPrice y items[].grossLineTotal, que son
null en los comprobantes del modelo neto.
invoice.pricesIncludeTax dice con qué modo nació el comprobante. El PDF y la
página pública muestran los importes brutos persistidos, sin re-derivarlos.
expectedTotal
Declarás cuánto cobraste; si la cuenta no da exactamente eso, el pedido rebota con los dos
valores en el detalle (con toda la precisión que mandaste: un 115.00004 se muestra tal
cual, no como "115") y no se crea nada. En AR sirve con y sin
pricesIncludeTax (en el modelo neto es la red que atrapa la diferencia de
1 centavo antes de facturarla):
HTTP/1.1 422 Unprocessable Entity
{ "error": "validation_failed",
"detail": "expectedTotal no coincide con el total calculado: expectedTotal=50001, total calculado=50000" }
En PY expectedTotal exige pricesIncludeTax: true.
En el modelo neto el total que se guarda se calcula por alícuota y el que se firma ante
SIFEN (dTotGralOpe) por ítem, y pueden diferir en guaraníes: un
expectedTotal que cierra contra uno no garantiza el otro, así que en vez de
prometer de más responde 422
("expectedTotal en PY requiere pricesIncludeTax=true: …"). Con el flag, el
total guardado y el firmado son el mismo número por construcción.
Reemplazar un comprobante rechazado — supersedesInvoiceId
Un REJECTED (la autoridad fiscal rechazó el comprobante) se corrige emitiendo
uno nuevo, con otra idempotencyKey. Declarale a quién
reemplaza con supersedesInvoiceId: así la venta queda con un solo
comprobante vivo — mientras el nuevo esté en vuelo o autorizado, nadie puede re-encolar
el rechazado desde el back office (responde 409), ni emitir un segundo
reemplazo del mismo rechazado. Es lo que evita facturar dos veces la misma venta.
curl -s -X POST https://api.facturauno.com/api/v1/invoices \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{
"issuerId": "0b0e8a1e-1111-2222-3333-444455556666",
"docClass": "INVOICE",
"idempotencyKey": "venta-991204-r2",
"externalRef": "orden-991204",
"supersedesInvoiceId": "6f2b…",
"currency": "PES", "pricesIncludeTax": true, "expectedTotal": 115.00,
"recipient": { "docType": "DNI", "docNumber": "30111222", "name": "Ana Pérez",
"fiscalProfile": { "condicionIVAReceptorId": 5 } },
"items": [ { "description": "Entrada general", "qty": 1,
"unitPrice": 115.00, "taxRateCode": "IVA_21" } ]
}'
- Solo se reemplaza un comprobante
REJECTEDdel mismo emisor, mismadocClassy mismo ambiente fiscal. Cualquier otro caso responde 422validation_failedcon el motivo — incluidoDEAD: un comprobante que agotó reintentos puede tener un CAE/CDC otorgado que no llegamos a leer, así que se resuelve desde el back office (verificación contra la autoridad y requeue), no con un reemplazo. - Si la venta de ese rechazado ya tiene otro comprobante vivo (un reemplazo
anterior en vuelo o autorizado, o el rechazado re-encolado): 409
conflictcon el id y el estado del que ya la cubre. No se crea nada. - El comprobante nuevo vuelve con
supersedesInvoiceIden el detalle y en el estado por lote, así podés seguir la cadena. Un reintento del mismo pedido (misma key) tiene que traer el mismosupersedesInvoiceId; otro distinto es 409idempotency_conflict.
HTTP/1.1 422 Unprocessable Entity
{ "error": "validation_failed",
"detail": "solo se puede reemplazar un comprobante REJECTED (supersedesInvoiceId está DEAD); para DEAD usá el requeue admin tras verificar contra la autoridad" }
HTTP/1.1 409 Conflict
{ "error": "conflict",
"detail": "supersedesInvoiceId ya fue reemplazado por 9c41… (AUTHORIZED): esa venta ya tiene un comprobante vivo" }
Estados del ciclo de vida: QUEUED → SENT → AUTHORIZED |
REJECTED (rechazo de la autoridad) · VERIFYING (timeout en vuelo,
se verifica antes de reintentar) · DEAD (agotó reintentos) ·
CANCELED.
3. Consultar issue · read
Detalle (con items y eventos)
curl -s https://api.facturauno.com/api/v1/invoices/{invoiceId} \
-H "X-Api-Key: $FACTURAUNO_KEY"
Devuelve { invoice, items, events }: estado, número, authCode
(CAE/CDC) y su vencimiento (authCodeExpiresAt, fecha sin zona:
"2026-09-27T00:00:00"), totales, pricesIncludeTax,
supersedesInvoiceId (el rechazado que este
reemplaza, o null), pdfStatus,
publicUrl y el historial de transiciones (cada evento con
fromStatus, toStatus, detail, actor,
at — ahí queda el motivo de un rechazo aunque el comprobante se haya
re-encolado después). El number aparece antes de la autorización
(tentativo en AR, al firmar en PY) y es definitivo solo en AUTHORIZED. En
items[],
unitPrice/discountAmount/lineTotal son
siempre netos; los comprobantes emitidos con
pricesIncludeTax traen además
grossUnitPrice y grossLineTotal (IVA incluido), que en el resto
son null. Para conciliar muchos comprobantes de una vez usá el
estado por lote.
Listado con filtros
curl -s "https://api.facturauno.com/api/v1/invoices?issuerId=…&status=AUTHORIZED&from=2026-07-01&page=1&pageSize=30" \
-H "X-Api-Key: $FACTURAUNO_KEY"
Filtros: issuerId, country, status,
docClass, search, from/to. Respuesta
{ total, items }.
XML / payload fiscal
curl -s https://api.facturauno.com/api/v1/invoices/{invoiceId}/xml \
-H "X-Api-Key: $FACTURAUNO_KEY"
Solo con el comprobante AUTHORIZED; antes responde 409
conflict. El PDF no se descarga por la API: se sirve desde el
link público cuando está materializado.
4. Estado por lote issue · read
POST /api/v1/invoices/status devuelve el estado de hasta
200 comprobantes en una sola llamada — por invoiceId, por
idempotencyKey o mezclando ambos. Es la consulta para conciliar una tanda
de emisiones sin hacer un GET por comprobante (y sin depender de haber guardado el
invoiceId: con tu propia key de idempotencia alcanza).
curl -s -X POST https://api.facturauno.com/api/v1/invoices/status \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{
"issuerId": "0b0e8a1e-1111-2222-3333-444455556666",
"invoiceIds": [ "6f2b…", "9c41…" ],
"idempotencyKeys": [ "venta-991204", "venta-991205" ]
}'
| Campo | Detalle |
|---|---|
issuerId | Opcional si solo consultás por invoiceIds; obligatorio con idempotencyKeys (la key es única por emisor). |
invoiceIds | Opcional. Lista de GUIDs. |
idempotencyKeys | Opcional. Las keys con las que emitiste, del emisor issuerId. |
- Máximo 200 referencias sumando las dos listas (cuenta lo que
mandaste, antes de deduplicar); más que eso, o
idempotencyKeyssinissuerId: 422validation_failed. - Sin body, sin listas o con las dos vacías: 200
{ "items": [] }. - Lo que no existe, lo ilegible (un GUID mal formado) y lo que está fuera del alcance
de tu key se omite en silencio — no hay 404 por elemento ni se
confirma la existencia de comprobantes ajenos: compará lo que pediste contra lo que
volvió. Una
idempotencyKeyque no vuelve no se emitió — reintentá el POST de emisión con esa misma key. (UnissuerIdexplícito que tu key no puede operar sí responde 403, igual que al emitir.) - Un comprobante pedido por id y por key vuelve una sola vez.
HTTP/1.1 200 OK
{ "items": [
{ "invoiceId": "9c41…", "issuerId": "0b0e…", "issuerTaxId": "30111111111",
"idempotencyKey": "venta-991204", "externalRef": "orden-991204",
"supersedesInvoiceId": null,
"status": "AUTHORIZED", "environment": "Prod", "number": 16615,
"docClass": "INVOICE", "docTypeLocal": "6",
"authCode": "75123456789012", "authCodeExpiresAt": "2026-09-27T00:00:00",
"netAmount": 95.04, "taxAmount": 19.96, "totalAmount": 115.00,
"pricesIncludeTax": true,
"publicUrl": "https://facturauno.com/c/<token>",
"authorizedAt": "2026-09-17T14:03:22Z", "updatedAt": "2026-09-17T14:03:22Z",
"lastDetail": null }
] }
lastDetail es el detalle del último evento hacia el estado
actual del comprobante — en un REJECTED, el motivo que dio la
autoridad fiscal (el vigente, si fue rechazado más de una vez); en un DEAD,
el último error de la cola; null en un AUTHORIZED. Es el
detalle del estado en que está hoy, no el último que hubo: si un
REJECTED se re-encola desde el back office pasa a QUEUED y
lastDetail pasa a "requeue manual" — el motivo del rechazo
deja de verse ahí (sigue en events del detalle). supersedesInvoiceId
es el rechazado que este comprobante reemplaza (null
si no reemplaza a nadie).
authCode, authCodeExpiresAt y authorizedAt son
null hasta la autorización. number no: en
Argentina se asigna tentativo al armar el pedido, antes de enviarlo a ARCA, y
en Paraguay al firmar — así que un comprobante SENT/VERIFYING
ya trae número, y hasta un REJECTED puede traerlo. El número es
definitivo solo en AUTHORIZED: no lo uses antes.
authCodeExpiresAt es una fecha sin zona, con hora en cero
("2026-09-27T00:00:00", igual que en GET /invoices/{id});
authorizedAt/updatedAt son instantes UTC con offset.
publicUrl es null si el link público fue revocado. Los items
vuelven ordenados por fecha de creación — no en el orden en que los pediste: indexalos
por invoiceId o idempotencyKey.
5. Notas de crédito / débito y anulación
Una NC o ND es un POST /api/v1/invoices más, con
docClass: "CREDIT_NOTE" (o DEBIT_NOTE) y el
relatedInvoiceId del comprobante que ajusta — obligatorio.
En Paraguay la nota además exige recipient
identificado (RUC, CI, PASAPORTE o CIE): SIFEN rechaza NC/ND innominadas
(código 1331) y la API lo corta local con 422. La Factura referenciada
también tiene que haberse emitido con receptor identificado — sobre una FE innominada
SIFEN no asocia notas (haría falta el evento de nominación, hoy no soportado), así que
si el comprador puede llegar a necesitar una NC, identificalo desde la factura.
curl -s -X POST https://api.facturauno.com/api/v1/invoices \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{ "issuerId": "…", "docClass": "CREDIT_NOTE",
"relatedInvoiceId": "6f2b…", "idempotencyKey": "nc-pedido-84512",
"currency": "PES", "recipient": { … }, "items": [ … ] }'
POST /api/v1/invoices/{id}/cancel sobre un comprobante AR responde
409 use_credit_note a propósito.
En Paraguay sí existe: el mismo endpoint (body opcional
{"reason": "…"}) registra el evento de cancelación ante SIFEN — solo sobre
un DTE AUTHORIZED y dentro de la ventana (48 h la Factura electrónica,
168 h NC/ND, desde la aprobación). Responde 202 con el evento
PENDING; cuando SIFEN lo acepta, el comprobante pasa a
CANCELED y te llega el webhook invoice.canceled. Vencida la
ventana, anulá con nota de crédito como en AR.
6. Webhooks firmados issue
Suscribirse
curl -s -X POST https://api.facturauno.com/api/v1/webhooks/subscriptions \
-H "X-Api-Key: $FACTURAUNO_KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://miapp.com/hooks/facturauno", "events": "*" }'
HTTP/1.1 200 OK
{ "subscriptionId": "…", "url": "https://miapp.com/hooks/facturauno", "events": "*",
"secret": "<base64 — SE MUESTRA UNA SOLA VEZ, guardalo>",
"signature": { "header": "X-FacturaUno-Signature",
"scheme": "t=<unix>,v1=hex(HMACSHA256(secret, '<unix>.' + body))",
"toleranceSeconds": 300 } }
El secret viaja en claro una única vez en esta respuesta y
queda cifrado de nuestro lado. events acepta * o una lista
separada por comas. GET /api/v1/webhooks/subscriptions lista las tuyas
(sin secretos).
Eventos
| Evento | Cuándo |
|---|---|
invoice.authorized | La autoridad fiscal autorizó (hay CAE y link público descargable). |
invoice.rejected | La autoridad rechazó el comprobante. |
invoice.dead | La emisión agotó los reintentos (interviene soporte/admin). |
Entrega y verificación de firma
Cada entrega es un POST JSON con headers
X-FacturaUno-Event y X-FacturaUno-Signature: t=<unixSeconds>,v1=<hex>.
Para verificar: reconstruí "<t>." + body crudo, calculá
HMAC-SHA256 con tu secret, comparalo en tiempo constante con v1 y rechazá si
|ahora − t| > 300 s.
{ "eventType": "invoice.authorized", "invoiceId": "6f2b…", "issuerId": "…",
"country": "AR", "docClass": "INVOICE", "docTypeLocal": "1", "status": "AUTHORIZED",
"number": 1234, "authCode": "75123456789012", "totalAmount": 121000.00,
"currency": "PES", "externalRef": "pedido-84512", "idempotencyKey": "pedido-84512",
"publicUrl": "https://facturauno.com/c/<token>", "at": "2026-08-07T14:03:22Z" }
Respondé 2xx; ante otra cosa reintentamos con backoff (1m, 5m, 15m, 1h, 4h…) hasta 24 h y
después la entrega queda DEAD.
7. Link público del comprobante
Todo comprobante tiene un link https://facturauno.com/c/{token} (viene como
publicUrl en la respuesta de emisión, el detalle, el webhook y el mail al
receptor). Es una página sin login que informa el estado — "en proceso",
autorizada o cancelada — con los datos del comprobante, el CAE y su vencimiento, y los
botones Descargar PDF (/c/{token}/pdf — se habilita cuando el
PDF está materializado, pdfStatus = READY) y XML
(/c/{token}/xml).
- Token opaco criptográficamente aleatorio (anti-enumeración) y revocable por el emisor.
- Rate limit por IP; token inválido o revocado → 404 genérico.
- Con white-label habilitado, el receptor ve la marca del emisor.
8. Catálogo por país issue · read
curl -s https://api.facturauno.com/api/v1/catalog/AR/doctypes \
-H "X-Api-Key: $FACTURAUNO_KEY"
Devuelve los tipos locales soportados, los taxRateCodes válidos y las
tablas de códigos por país — todo lo que necesitás para armar el
IssueRequest sin hardcodear. /catalog/AR/doctypes:
Factura/NC/ND A, B y C + condicionesIvaReceptor (RG 5616).
/catalog/PY/doctypes: Factura (iTiDE 1), NC (5) y ND (6) + las opciones
py (tipoTransaccion, condicionVenta, indicadorPresencia, motivoEmision).
9. Emisores de tu key issue · read
curl -s https://api.facturauno.com/api/v1/issuers \
-H "X-Api-Key: $FACTURAUNO_KEY"
Devuelve los emisores visibles para tu API key, cada uno con sus puntos
de emisión: de acá salen el issuerId (y, si el emisor tiene más de un punto
activo, el emissionPointId) que pide el POST /api/v1/invoices —
sin hardcodear GUIDs por ambiente. Una key acotada a emisores concretos ve solo esos; lo
que no figura en esta respuesta, esa key no lo puede emitir ni consultar (emitir para
otro emisor responde 403).
HTTP/1.1 200 OK
{ "items": [
{ "issuerId": "0b0e8a1e-1111-2222-3333-444455556666", "taxId": "30111111111",
"country": "AR", "legalName": "Mi Empresa S.A.", "status": "ACTIVE",
"emissionPoints": [
{ "emissionPointId": "78fd…", "status": "ACTIVE",
"countryKey": { "posNumber": 4 } } ] },
{ "issuerId": "8bf0…", "taxId": "80012345-6",
"country": "PY", "legalName": "Mi Empresa Paraguay S.A.", "status": "ACTIVE",
"emissionPoints": [
{ "emissionPointId": "b5f5…", "status": "ACTIVE",
"countryKey": { "establishment": "001", "expeditionPoint": "003",
"timbradoNumber": "12345678", "timbradoValidFrom": "2026-01-01" } } ] }
] }
Sin paginado. Se listan todos los emisores de la key, no solo los operativos:
mirá status (ACTIVE | SUSPENDED |
ARCHIVED; puntos: ACTIVE | DISABLED) antes de
emitir. countryKey es el identificador fiscal del punto: en AR el punto de
venta del WS (posNumber), en PY establecimiento + punto de expedición +
timbrado. No expone domicilio, configuración fiscal, branding ni credenciales.
10. Códigos de respuesta
Los errores tienen siempre la forma
{ "error": "<código>", "detail": "…" } (detail es
opcional; los de autenticación son genéricos a propósito).
| HTTP | error | Cuándo |
|---|---|---|
| 202 | — | Emisión aceptada y encolada (POST /invoices); cancelación PY registrada (/cancel). |
| 200 | — | Consultas. En POST /invoices: la idempotencyKey ya existía y se devuelve el comprobante original, sin re-emitir. |
| 400 | invalid_request | Pedido mal formado. Un JSON ilegible o con tipos que no corresponden (un string donde va un número, un GUID inválido) responde 400 sin cuerpo. |
| 401 | unauthorized | X-Api-Key ausente, inválida o sin la audiencia que pide el endpoint. |
| 403 | forbidden | La key está acotada a otros emisores y mandaste el issuerId de uno que no es suyo (al emitir, listar o consultar por lote). |
| 404 | not_found | El comprobante no existe o es de un emisor fuera del alcance de tu key (la respuesta es idéntica: no se confirma existencia). |
| 409 | idempotency_conflict | La idempotencyKey ya se usó para otro pedido: el expectedTotal no coincide con el total del comprobante existente, el docClass es distinto, el pricesIncludeTax no es el del comprobante existente, o el supersedesInvoiceId es otro. No se crea nada (ver Idempotencia). |
| 409 | conflict | El estado no admite la operación: XML de un comprobante todavía no autorizado, cancelación PY fuera de ventana o repetida, o supersedesInvoiceId sobre una venta que ya tiene otro comprobante vivo (ver Reemplazar un comprobante rechazado). |
| 409 | use_credit_note | /cancel sobre un comprobante AR: se anula con nota de crédito (ver §5). |
| 422 | validation_failed | El pedido no valida: canónico inválido, expectedTotal que no coincide con el total calculado, restricciones de pricesIncludeTax (descuento, decimales), supersedesInvoiceId que no es un REJECTED del mismo emisor/clase/ambiente, más de 200 elementos o idempotencyKeys sin issuerId en el estado por lote. El detail dice qué. No se crea nada. |
| 429 | too_many_requests | Rate limit (hoy, el link público /c/* por IP). |
| 500 | — | Error nuestro. Sale sin cuerpo (no hay un código error para esto: no asumas JSON en un 500). En la emisión reintentá con la misma idempotencyKey: si el comprobante llegó a crearse, vuelve el original con 200; si no, se crea recién ahora. |
REJECTED y te
enterás por el webhook invoice.rejected, por el
estado por lote (lastDetail trae el motivo) o por
el detalle.