Errores
Todas las respuestas siguen la misma forma:
{ "ok": true, "result": { } }
{ "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.
code | details |
|---|---|
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ás | no 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
| HTTP | code | Cuándo aparece | ¿Reintentar? |
|---|---|---|---|
400 | VALIDATION_ERROR | Payload inválido, punto de venta inexistente, faltan fechas de servicio | No — corregí el pedido |
401 | — | Falta el Authorization, clave inválida o revocada, ruta no habilitada | No |
402 | SUBSCRIPTION_EXPIRED | El período contratado de la empresa venció | Recién al regularizar el pago |
404 | NOT_FOUND | El comprobante o el punto de venta no existe, o el documento no está en el padrón de ARCA | No |
409 | VALIDATION_ERROR | Clave de idempotencia reusada para otro comprobante | No — generá una clave nueva |
409 | PDF_NOT_READY | El PDF todavía se está generando | Sí, en unos segundos |
409 | ARCA_UNCONFIRMED | No se pudo confirmar si el comprobante quedó autorizado | Sí, con la misma idempotency |
422 | ARCA_ERROR | ARCA rechazó el comprobante | No — corregí el pedido |
429 | — | Límite de peticiones | Sí, con espera |
429 | VOUCHER_LOCK_TIMEOUT | Otra emisión del mismo punto de venta sigue en curso | Sí, ver Retry-After |
429 | QUOTA_EXCEEDED | Se agotó la cuota de una operación administrativa | Sí, pasada la ventana |
429 | SUBSCRIPTION_QUOTA_EXCEEDED | Se agotó la cuota mensual de comprobantes | Recién al ampliar el límite o al renovarse el período |
500 | — | Error interno | Sí, con backoff |
502 | ARCA_CONNECTION_ERROR | No se pudo hablar con los servidores de ARCA | Sí, con backoff — salvo que sea el certificado sin delegar |
504 | — | Se agotó el tiempo del pedido | Sí, 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ódigo | Qué significa | Cómo se corrige |
|---|---|---|
10015 | Factura B/C sobre el umbral sin identificar al comprador | Pedile DNI o CUIT al cliente |
| — | Documento inválido para el tipo de comprobante | Factura A exige CUIT; ver qué comprobante emitir |
| — | Fecha fuera del rango permitido | ARCA acota cuánto podés retroceder o adelantar la fecha |
| — | Punto de venta no habilitado | Tiene 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
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 haciendo | Servicio que hay que delegar |
|---|---|
| Emitir un comprobante | Facturación Electrónica (wsfe) |
| Consultar el padrón de una persona física | Consulta a Padrón A13 |
| Consultar el padrón de una persona jurídica | Constancia 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.
502, pero no cede reintentandoLlega 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:
- La
idempotencyse genera una sola vez, fuera del bucle. Es lo único que hace seguro el reintento —y lo que convierte un504o unARCA_UNCONFIRMEDen una recuperación en vez de un duplicado. - Los errores permanentes no se reintentan. Un
422reintentado tres veces sigue siendo un422, y sólo suma latencia. - Respetá
Retry-Aftercuando venga; si no, backoff exponencial.
Implementaciones completas con esta lógica en JavaScript, PHP y Python: Ejemplos.