Saltar al contenido principal

Errores

Todas las respuestas siguen la misma forma:

Éxito
{ "ok": true, "result": { } }
Error
{ "ok": false, "error": "Mensaje explicativo", "code": "VALIDATION_ERROR" }

Chequeá ok, no el status HTTP. code acompaña a los errores de negocio; los de infraestructura (límite de peticiones, timeout) traen sólo error.

Algunos errores agregan details, y su forma la determina code. No es siempre la misma: un rechazo de ARCA lo manda como una lista de códigos, mientras que un problema de validación o de cuota lo manda como un objeto. Mirá el code antes de leer details; decidir por la forma —"¿es una lista?"— te va a confundir un pedido mal armado con un rechazo fiscal.

codedetails
VALIDATION_ERROR{ errors: [{ field, message }] } — un elemento por campo que falló
ARCA_ERROR[{ code, msg }] — los rechazos exactos de ARCA
SUBSCRIPTION_QUOTA_EXCEEDED{ quota: { … } } — el estado de tu cuota mensual
VOUCHER_LOCK_TIMEOUT{ retryAfterMs }
Los demásno traen details

details es opcional incluso dentro de un mismo code: un VALIDATION_ERROR que sale de una regla de negocio —una clave de idempotencia reusada, un PDF fuera del plazo de regeneración— trae sólo error y code. Preguntá si está antes de leerlo.

Tabla completa​

HTTPcodeCuándo aparece¿Reintentar?
400VALIDATION_ERRORPayload inválido, punto de venta inexistente, faltan fechas de servicioNo — corregí el pedido
401—Falta el Authorization, clave inválida o revocada, ruta no habilitadaNo
402SUBSCRIPTION_EXPIREDEl período contratado de la empresa vencióRecién al regularizar el pago
404NOT_FOUNDEl comprobante o el punto de venta no existe, o el documento no está en el padrón de ARCANo
409VALIDATION_ERRORClave de idempotencia reusada para otro comprobanteNo — generá una clave nueva
409PDF_NOT_READYEl PDF todavía se está generandoSí, en unos segundos
409ARCA_UNCONFIRMEDNo se pudo confirmar si el comprobante quedó autorizadoSí, con la misma idempotency
422ARCA_ERRORARCA rechazó el comprobanteNo — corregí el pedido
429—Límite de peticionesSí, con espera
429VOUCHER_LOCK_TIMEOUTOtra emisión del mismo punto de venta sigue en cursoSí, ver Retry-After
429QUOTA_EXCEEDEDSe agotó la cuota de una operación administrativaSí, pasada la ventana
429SUBSCRIPTION_QUOTA_EXCEEDEDSe agotó la cuota mensual de comprobantesRecién al ampliar el límite o al renovarse el período
500—Error internoSí, con backoff
502ARCA_CONNECTION_ERRORNo se pudo hablar con los servidores de ARCASí, con backoff — salvo que sea el certificado sin delegar
504—Se agotó el tiempo del pedidoSí, con la misma idempotency

Los que importan​

409 ARCA_UNCONFIRMED — no se pudo confirmar la emisión​

Se cortó la comunicación con ARCA justo al autorizar y no se pudo averiguar en qué quedó.

{
"ok": false,
"error": "No se pudo confirmar si el comprobante 2-11-13540 quedó autorizado en ARCA. Reintentá con la misma clave de idempotencia: si ya se había emitido, vas a recibir ese mismo comprobante y no uno nuevo.",
"code": "ARCA_UNCONFIRMED"
}

Reintentá con la misma idempotency, igual que con un 504. Si el comprobante ya existía lo recibís con idempotencyHit: true; si no, se emite ahora. Usar otra idempotency para el reintento sí podría duplicarlo.

422 ARCA_ERROR — ARCA rechazó el comprobante​

Es un rechazo de negocio: el comprobante no se guardó y no consumió numeración.

{
"ok": false,
"error": "(10015) Factura B con importe total mayor a $ … requiere identificación del comprador",
"code": "ARCA_ERROR",
"details": [{ "code": 10015, "msg": "Factura B con importe total mayor a …" }]
}

details trae el código y el mensaje exactos de ARCA. Registralos: son la única forma de diagnosticar qué regla se violó.

Reintentar sin cambiar nada va a fallar igual. Los motivos más comunes:

CódigoQué significaCómo se corrige
10015Factura B/C sobre el umbral sin identificar al compradorPedile DNI o CUIT al cliente
—Documento inválido para el tipo de comprobanteFactura A exige CUIT; ver qué comprobante emitir
—Fecha fuera del rango permitidoARCA acota cuánto podés retroceder o adelantar la fecha
—Punto de venta no habilitadoTiene que ser de tipo webservice; ver puntos de venta

400 VALIDATION_ERROR — el pedido está mal armado​

Falta un campo, sobra, o tiene un tipo que no corresponde. El comprobante no se envió a ARCA y no consumió numeración.

{
"ok": false,
"error": "Datos inválidos en \"items.0.price\": Entrada inválida: se esperaba number, recibido string (y 1 más)",
"code": "VALIDATION_ERROR",
"details": {
"errors": [
{ "field": "items.0.price", "message": "Entrada inválida: se esperaba number, recibido string" },
{ "field": "customer.docNumber", "message": "Entrada inválida: se esperaba string, recibido número" }
]
}
}

field es la ruta del dato dentro del cuerpo que mandaste, con los índices de los arreglos incluidos (items.0.price es el price del primer ítem). Va en null cuando el problema es del cuerpo entero y no de un campo. Es lo que te deja marcar el campo en un formulario sin tener que interpretar el texto.

error resume el primer problema y cuántos más hay; la lista completa está siempre en details.errors.

Reintentar sin corregir el pedido falla igual. Los dos más comunes: mandar docNumber como número cuando la API lo espera en texto (el padrón lo devuelve numérico, hay que convertirlo), y omitir las fechas de servicio cuando el concept las exige.

504 — se agotó el tiempo​

Un 504 no significa que el comprobante no se emitió

Puede haberse emitido y el corte haber ocurrido mientras volvía la respuesta. Reintentá con la misma idempotency: si ya existía, lo recuperás con idempotencyHit: true; si no, se emite ahora. Nunca se duplica.

Es exactamente el problema para el que existe la clave de idempotencia. Generar una clave nueva en el reintento sí puede duplicar el comprobante.

429 VOUCHER_LOCK_TIMEOUT — otra emisión en curso​

ARCA exige numeración consecutiva, así que se emite de a un comprobante por vez para cada combinación de punto de venta y tipo. Si llega un pedido mientras otro se autoriza, espera su turno solo. Este error aparece únicamente si pasados esos segundos la emisión anterior sigue en curso.

Es transitorio y seguro de reintentar: el comprobante no se emitió y el número quedó libre. Viene con el header Retry-After. Reintentá con la misma idempotency.

Si lo ves seguido, estás emitiendo en paralelo sobre el mismo punto de venta: secuenciá. Ver Límites.

409 — clave de idempotencia reusada​

{
"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"
}

La clave ya pertenece a un comprobante de otro punto de venta o de otro tipo. Es permanente: reintentar falla igual. Generá una clave nueva, y usá un UUID por comprobante.

402 SUBSCRIPTION_EXPIRED — período vencido​

{
"ok": false,
"error": "El período contratado de la empresa venció. Regularizá el pago para volver a emitir comprobantes.",
"code": "SUBSCRIPTION_EXPIRED"
}

No es un problema de credenciales —la clave sigue siendo válida—, por eso responde 402 y no 401. No consume numeración. Se resuelve abonando la renovación en la app, en Administrar → Pagos.

Con el período vencido, la empresa queda en sólo lectura: además de la emisión, este mismo 402 responden las escrituras de configuración —alta y edición de puntos de venta, su logo, los datos fiscales de la empresa, regenerar un PDF y programar el envío de un comprobante por email—. Las consultas (GET) siguen funcionando con normalidad, así que podés seguir leyendo tus comprobantes y su PDF. El mensaje cambia según el caso: hablará de emitir comprobantes o de modificar los datos de la empresa.

429 SUBSCRIPTION_QUOTA_EXCEEDED — cuota mensual agotada​

{
"ok": false,
"error": "Se agotó la cuota de 500 comprobantes de este período hasta el 2026-09-16. …",
"code": "SUBSCRIPTION_QUOTA_EXCEEDED",
"details": {
"quota": { "granted": 500, "used": 500, "remaining": 0, "periodEnd": "2026-09-16" }
}
}

La suscripción está al día: lo que se agotó es la capacidad del mes. Cede al ampliar el límite o al renovarse el período, así que no corresponde un backoff automático — reintentar antes de eso no cambia nada.

details.quota te deja distinguir los dos casos sin leer el mensaje:

Para no chocar de sorpresa: cada emisión devuelve quota.remaining, y al 90% del cupo la cuenta recibe un aviso por email.

502 ARCA_CONNECTION_ERROR — "Computador no autorizado a acceder al servicio"​

{
"ok": false,
"error": "WSAA Fault: Computador no autorizado a acceder al servicio",
"code": "ARCA_CONNECTION_ERROR"
}

El certificado está cargado y activo, pero su alias no está delegado al servicio que se está usando. Cada servicio de ARCA se autoriza por separado:

Qué estabas haciendoServicio que hay que delegar
Emitir un comprobanteFacturación Electrónica (wsfe)
Consultar el padrón de una persona físicaConsulta a Padrón A13
Consultar el padrón de una persona jurídicaConstancia de Inscripción

Se resuelve en el portal de ARCA, en el Administrador de Relaciones de Clave Fiscal (en homologación, desde WSASS), asociando el alias del certificado al servicio que falta: Vincular tu certificado de ARCA → Paso 4.

Es un 502, pero no cede reintentando

Llega como error de conexión porque falla la autenticación contra ARCA, no la emisión. Reintentar agota los intentos y termina fallando igual: hay que hacer la delegación. Es la causa número uno de "tengo el certificado cargado y no puedo facturar".

GET /:id-empresa/arca/status te dice cuál de los servicios está fallando —conexión, autenticación y padrones se prueban por separado— y devuelve la sugerencia correspondiente. Ver consultas a ARCA.

Cómo manejarlos​

Una regla simple que cubre todos los casos:

async function emitir(payload, intentos = 3) {
// La clave se genera UNA vez, fuera del bucle: es lo que hace que el
// reintento recupere el comprobante en vez de emitir otro.
const body = { ...payload, idempotency: payload.idempotency ?? crypto.randomUUID() }

for (let intento = 1; intento <= intentos; intento++) {
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) })
const data = await res.json()

if (data.ok) return data.result

// El comprobante puede haber quedado emitido: el reintento con la MISMA clave lo recupera
// en vez de emitir otro. Por eso va antes de la lista de permanentes, que también trae 409.
if (data.code === 'ARCA_UNCONFIRMED' && intento < intentos) {
await new Promise((r) => setTimeout(r, 2 ** intento * 1000))
continue
}
// Errores permanentes: reintentar no cambia nada.
if ([400, 401, 402, 403, 404, 409, 422].includes(res.status)) {
throw new Error(`${data.code}: ${data.error}`)
}
// La cuota mensual tampoco cede reintentando.
if (data.code === 'SUBSCRIPTION_QUOTA_EXCEEDED') {
throw new Error(data.error)
}
// El resto (429, 500, 502, 504) es transitorio.
if (intento === intentos) throw new Error(data.error ?? 'Error tras varios intentos')

const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** intento
await new Promise((r) => setTimeout(r, retryAfter * 1000))
}
}

Las tres decisiones que importan:

  1. La idempotency se genera una sola vez, fuera del bucle. Es lo único que hace seguro el reintento —y lo que convierte un 504 o un ARCA_UNCONFIRMED en una recuperación en vez de un duplicado.
  2. Los errores permanentes no se reintentan. Un 422 reintentado tres veces sigue siendo un 422, y sólo suma latencia.
  3. Respetá Retry-After cuando venga; si no, backoff exponencial.

Implementaciones completas con esta lógica en JavaScript, PHP y Python: Ejemplos.