Cómo integrar la API de Facturación Electrónica de ARCA (ex-AFIP) en Node.js y Python en 2026

Guía técnica paso a paso para integrar los Web Services de ARCA (WSAA y WSFE): certificados digitales, obtención de Token y Sign (TRA), solicitud de CAE y manejo de errores.

Damián Oliva··11 min de lectura

Integrar la facturación electrónica en Argentina sigue siendo uno de los mayores dolores de cabeza para los desarrolladores de software y equipos de producto. Tras la disolución de la AFIP y la creación de la Agencia de Recaudación y Control Aduanero (ARCA) mediante el Decreto 955/2024, las reglas tributarias de fondo se mantuvieron, pero los estándares de infraestructura técnica se endurecieron: endpoints bajo nuevas directivas de seguridad, requerimiento estricto de TLS 1.2 y 1.3, renovación de certificados intermedios y deprecación de paquetes de código desactualizados en npm y PyPI que no soportan OpenSSL 3.x.

Para emitir una factura electrónica legal en Argentina mediante la API de ARCA se deben coordinar dos Web Services SOAP independientes:

  1. WSAA (Web Service de Autenticación y Autorización): Valida tu certificado digital X.509 mediante un XML firmado con estándar CMS/PKCS#7 y te devuelve un Token y un Sign válidos por 12 horas.
  2. WSFE (Web Service de Factura Electrónica v1): Recibe los datos fiscales del comprobante junto con el Token, Sign y CUIT, y devuelve el CAE (Código de Autorización Electrónico) con su fecha de vencimiento.

A continuación, revisaremos paso a paso la arquitectura, la configuración de certificados, el firmado del Ticket de Acceso (TRA), el código funcional en Node.js (TypeScript) y Python, y las mejores prácticas de contingencia en producción.


Arquitectura del Web Service de ARCA: WSAA vs WSFE

La arquitectura de ARCA no utiliza tokens Bearer tipo JWT ni autenticación por API Keys estáticas. Todo se basa en criptografía asimétrica de clave pública y privada.

[ Tu Aplicación ]
       │
       │  1. Genera XML TRA (Ticket de Requerimiento de Acceso)
       │  2. Firma TRA con Clave Privada y Certificado (CMS/PKCS#7)
       ▼
[ WSAA de ARCA ] ─────────► Valida firma y vigencia del certificado
       │
       │  3. Devuelve Access Ticket: <token> y <sign> (Válido por 12 hs)
       ▼
[ Tu Caché (Redis / Memoria) ] ── (Reutiliza Token y Sign por 11.5 horas)
       │
       │  4. Invoca FECAESolicitar(Token, Sign, CUIT, DatosFactura)
       ▼
[ WSFE de ARCA ] ─────────► Valida importes, IVA, CUIT receptor y correlatividad
       │
       │  5. Devuelve CAE + Fecha de Vencimiento + Resultado (Aprobado/Rechazado)
       ▼
[ Tu Base de Datos / PDF ] ── Almacena CAE y genera QR oficial

El error más común de los desarrolladores novatos es acoplar la autenticación con la facturación: si hacés una petición al WSAA cada vez que un cliente compra en tu plataforma, tu servidor tardará 3 segundos por venta y ARCA te bloqueará la IP por saturación. El WSAA se consulta únicamente cuando el token en caché expiró o está próximo a expirar.


Paso 1: Generación de clave privada (.key) y CSR con OpenSSL

Para comunicarte con ARCA necesitás un par de claves criptográficas RSA de 2048 bits. Ejecutá los siguientes comandos en tu terminal:

# 1. Generar la clave privada en formato PEM (sin passphrase para uso desatendido en servidores)
openssl genrsa -out arca_privada.key 2048

# 2. Generar el Certificate Signing Request (CSR)
# Reemplazá 'MiEmpresa' y el CUIT correspondiente (sin guiones)
openssl req -new -key arca_privada.key -subj "/C=AR/O=MiEmpresa/CN=facturacion_arca/serialNumber=CUIT 30712345678" -out arca_pedido.csr

Importante: El campo CN (Common Name) identifica al computador fiscal y serialNumber debe llevar el formato exacto CUIT <numero> con espacio. Guardá la clave privada arca_privada.key en un secreto seguro (AWS Secrets Manager, Doppler, HashiCorp Vault o variable de entorno codificada en Base64). Nunca la subas a un repositorio de Git.


Paso 2: Creación del certificado en ARCA y delegación de servicios

El proceso administrativo varía si trabajás en el entorno de desarrollo o en producción:

Entorno de Homologación (Testing)

  1. Ingresá con Clave Fiscal nivel 3 al portal de ARCA.
  2. Buscá el servicio interactivo "WSASS - Autogestión de Certificados de Homologación".
  3. Creá un nuevo alias de computador fiscal (por ejemplo, dev-backend).
  4. Pegá el contenido del archivo arca_pedido.csr que generaste en el Paso 1.
  5. El sistema te descargará un certificado digital con extensión .crt. Guardalo como arca_homo.crt.
  6. En el mismo portal WSASS, asociá tu computador fiscal al servicio wsfe (Facturación Electrónica).

Entorno de Producción

  1. Ingresá al servicio "Administración de Certificados Digitales".
  2. Creá el alias del computador y subí el CSR generado para producción. Descargá el .crt oficial firmado por la CA de ARCA.
  3. Ingresá al servicio "Administrador de Relaciones de Clave Fiscal".
  4. Hacé clic en Nueva Relación -> Buscar Servicio -> ARCA -> WebServices -> Facturación Electrónica.
  5. En el campo "Representante", seleccioná el computador fiscal creado en el paso 2. Sin este paso de delegación, el WSAA devolverá el error Computador no autorizado a acceder al servicio.

Paso 3: El Ticket de Requerimiento de Acceso (TRA) firmado

El WSAA exige que construyas un XML con una estructura temporal muy estricta, expresada en formato ISO 8601 con zona horaria de Buenos Aires (-03:00):

<?xml version="1.0" encoding="UTF-8"?>
<loginTicketRequest version="1.0">
  <header>
    <uniqueId>1726394820</uniqueId>
    <generationTime>2026-09-15T10:30:00-03:00</generationTime>
    <expirationTime>2026-09-15T22:30:00-03:00</expirationTime>
  </header>
  <service>wsfe</service>
</loginTicketRequest>

Reglas críticas del TRA:

  • generationTime: Debe tener la hora actual menos unos minutos (por ejemplo, -10 minutos) para evitar rechazos por desincronización de reloj (clock skew) entre tu servidor y los clusters de ARCA.
  • expirationTime: Máximo 12 horas posteriores a generationTime.
  • uniqueId: Un entero de 32 bits secuencial o timestamp UNIX único.
  • service: wsfe para facturación nacional (o wsfex para exportación, wsfce para crédito electrónica).

Este XML se debe envolver en una estructura criptográfica CMS / PKCS#7 firmada con tu clave privada y tu certificado X.509, y luego codificarse en Base64.


Paso 4: Implementación de la autenticación con WSAA

A continuación, vemos cómo firmar el XML y llamar al WSAA usando Node.js con la librería estándar crypto y utilidades nativas, evitando dependencias de terceros obsoletas.

Implementación en Node.js / TypeScript

import fs from "fs";
import { execSync } from "child_process";
import axios from "axios";
import { parseStringPromise } from "xml2js";

interface AuthTicket {
  token: string;
  sign: string;
  expiration: Date;
}

const WSAA_HOMO_URL = "https://wsaahomo.afip.gov.ar/ws/services/LoginCms";
const WSAA_PROD_URL = "https://wsaa.afip.gov.ar/ws/services/LoginCms";

export class ArcaAuthService {
  private cachedTicket: AuthTicket | null = null;

  constructor(
    private certPath: string,
    private keyPath: string,
    private isProduction: boolean = false
  ) {}

  public async getAuthTicket(): Promise<{ token: string; sign: string }> {
    // 1. Revisar si tenemos un ticket en caché con al menos 15 minutos de margen
    if (this.cachedTicket && this.cachedTicket.expiration > new Date(Date.now() + 15 * 60 * 1000)) {
      return {
        token: this.cachedTicket.token,
        sign: this.cachedTicket.sign,
      };
    }

    // 2. Generar el XML del TRA
    const now = new Date();
    const generationTime = new Date(now.getTime() - 10 * 60 * 1000).toISOString();
    const expirationTime = new Date(now.getTime() + 11.5 * 60 * 60 * 1000).toISOString();
    const uniqueId = Math.floor(now.getTime() / 1000);

    const traXml = `<?xml version="1.0" encoding="UTF-8"?>
<loginTicketRequest version="1.0">
  <header>
    <uniqueId>${uniqueId}</uniqueId>
    <generationTime>${generationTime}</generationTime>
    <expirationTime>${expirationTime}</expirationTime>
  </header>
  <service>wsfe</service>
</loginTicketRequest>`;

    // 3. Firmar el XML con OpenSSL en formato CMS (compatible con OpenSSL 3.x)
    const traPath = `/tmp/tra_${uniqueId}.xml`;
    const cmsPath = `/tmp/tra_${uniqueId}.cms`;
    fs.writeFileSync(traPath, traXml);

    try {
      execSync(
        `openssl cms -sign -in ${traPath} -out ${cmsPath} -signer ${this.certPath} -inkey ${this.keyPath} -outform DER -nodetach`
      );

      const cmsDer = fs.readFileSync(cmsPath);
      const cmsBase64 = cmsDer.toString("base64");

      // 4. Invocar el Web Service SOAP de WSAA
      const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">
  <soapenv:Header/>
  <soapenv:Body>
    <wsaa:loginCms>
      <wsaa:in0>${cmsBase64}</wsaa:in0>
    </wsaa:loginCms>
  </soapenv:Body>
</soapenv:Envelope>`;

      const url = this.isProduction ? WSAA_PROD_URL : WSAA_HOMO_URL;
      const response = await axios.post(url, soapEnvelope, {
        headers: {
          "Content-Type": "text/xml;charset=UTF-8",
          SOAPAction: "",
        },
      });

      // 5. Parsear la respuesta XML
      const parsedSoap = await parseStringPromise(response.data, { explicitArray: false });
      const loginCmsReturn = parsedSoap["soapenv:Envelope"]["soapenv:Body"]["loginCmsResponse"]["loginCmsReturn"];
      const credentials = await parseStringPromise(loginCmsReturn, { explicitArray: false });

      const token = credentials.loginTicketResponse.credentials.token;
      const sign = credentials.loginTicketResponse.credentials.sign;
      const expDate = new Date(credentials.loginTicketResponse.header.expirationTime);

      this.cachedTicket = { token, sign, expiration: expDate };
      return { token, sign };
    } finally {
      // Limpieza de archivos temporales
      if (fs.existsSync(traPath)) fs.unlinkSync(traPath);
      if (fs.existsSync(cmsPath)) fs.unlinkSync(cmsPath);
    }
  }
}

Paso 5: Llamada a WSFE para obtener el CAE (FECAESolicitar)

Una vez que tenés el token y el sign, podés comunicarte con el Web Service de Facturación Electrónica (WSFE).

Tipos de comprobantes habituales

En ARCA, cada tipo de comprobante responde a un código numérico:

  • 001: Factura A (Responsable Inscripto a Responsable Inscripto).
  • 002: Nota de Débito A.
  • 003: Nota de Crédito A.
  • 006: Factura B (Responsable Inscripto a Consumidor Final, Exento o Monotributista).
  • 007: Nota de Débito B.
  • 008: Nota de Crédito B.
  • 011: Factura C (Monotributista o Exento a cualquier receptor).
  • 012: Nota de Débito C.
  • 013: Nota de Crédito C.

La ecuación contable obligatoria

Para comprobantes con discriminación de IVA (Facturas A y B emitidas por Responsables Inscriptos), ARCA valida que se cumpla la igualdad al centavo:

$$\text{ImpTotal} = \text{ImpNeto} + \text{ImpTotConc} + \text{ImpOpEx} + \text{ImpTrib} + \text{ImpIVA}$$

Donde:

  • ImpNeto: Subtotal gravado sujeto a alícuotas de IVA.
  • ImpTotConc: Importes no gravados.
  • ImpOpEx: Operaciones exentas.
  • ImpTrib: Percepciones provinciales de IIBB, tasas municipales o impuestos internos.
  • ImpIVA: Suma exacta de los montos de IVA del array AlicIva.
  • ImpTotal: Importe final a cobrar.

Si esta igualdad no cuadra exactamente al centavo, ARCA devuelve un rechazo inmediato con código de error contable.

Implementación de emisión en Python

A continuación vemos la implementación en Python 3.11+ utilizando la librería zeep para el cliente SOAP y requests.

import os
import datetime
import pytz
from zeep import Client
from zeep.transports import Transport
from requests import Session

class ArcaBillingClient:
    WSFE_HOMO_WSDL = "https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL"
    WSFE_PROD_WSDL = "https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL"

    def __init__(self, cuit: int, token: str, sign: str, is_production: bool = False):
        self.cuit = cuit
        self.token = token
        self.sign = sign
        wsdl = self.WSFE_PROD_WSDL if is_production else self.WSFE_HOMO_WSDL
        
        session = Session()
        session.headers.update({"Accept-Encoding": "gzip,deflate"})
        self.client = Client(wsdl=wsdl, transport=Transport(session=session))

    def get_last_authorized_voucher(self, pto_vta: int, cbte_tipo: int) -> int:
        """Consulta el último comprobante emitido para evitar el error 10016."""
        auth = {
            "Token": self.token,
            "Sign": self.sign,
            "Cuit": self.cuit
        }
        res = self.client.service.FECompUltimoAutorizado(
            Auth=auth,
            PtoVta=pto_vta,
            CbteTipo=cbte_tipo
        )
        if res.Errors:
            raise Exception(f"Error consultando último comprobante: {res.Errors}")
        return res.CbteNro

    def emit_factura_b(self, pto_vta: int, doc_nro: int, imp_neto: float, imp_iva: float) -> dict:
        """
        Emite una Factura B (código 6) a Consumidor Final (DocTipo 96 o 99).
        Alícuota 21% -> Id 5 en ARCA.
        """
        ultimo_nro = self.get_last_authorized_voucher(pto_vta, 6)
        proximo_nro = ultimo_nro + 1

        fecha_hoy = datetime.datetime.now(pytz.timezone("America/Argentina/Buenos_Aires")).strftime("%Y%m%d")
        imp_total = round(imp_neto + imp_iva, 2)

        payload = {
            "Auth": {
                "Token": self.token,
                "Sign": self.sign,
                "Cuit": self.cuit
            },
            "FeCAEReq": {
                "FeCabReq": {
                    "CantReg": 1,
                    "PtoVta": pto_vta,
                    "CbteTipo": 6 # Factura B
                },
                "FeDetReq": {
                    "FECAEDetRequest": [{
                        "Concepto": 1, # 1: Productos, 2: Servicios, 3: Productos y Servicios
                        "DocTipo": 96 if doc_nro > 0 else 99, # 96: DNI, 99: Consumidor Final sin identificar
                        "DocNro": doc_nro,
                        "CbteDesde": proximo_nro,
                        "CbteHasta": proximo_nro,
                        "CbteFch": fecha_hoy,
                        "ImpTotal": imp_total,
                        "ImpTotConc": 0.0,
                        "ImpNeto": imp_neto,
                        "ImpOpEx": 0.0,
                        "ImpTrib": 0.0,
                        "ImpIVA": imp_iva,
                        "MonId": "PES",
                        "MonCotiz": 1.0,
                        "Iva": {
                            "AlicIva": [{
                                "Id": 5, # 21% (Id 4: 10.5%, Id 6: 27%)
                                "BaseImp": imp_neto,
                                "Importe": imp_iva
                            }]
                        }
                    }]
                }
            }
        }

        response = self.client.service.FECAESolicitar(**payload)
        
        # Validar el resultado devuelto por ARCA
        det_response = response.FeDetResp.FECAEDetResponse[0]
        if det_response.Resultado == "R": # R: Rechazado, A: Aprobado
            observaciones = det_response.Observaciones.Obs if det_response.Observaciones else []
            obs_msgs = [f"[{o.Code}] {o.Msg}" for o in observaciones]
            raise ValueError(f"Factura rechazada por ARCA: {', '.join(obs_msgs)}")

        return {
            "cae": det_response.CAE,
            "vto_cae": det_response.CAEFchVto,
            "comprobante_nro": proximo_nro,
            "resultado": det_response.Resultado
        }

Manejo de contingencia y códigos de error comunes de ARCA

En producción, los Web Services de ARCA sufren caídas intermitentes, picos de latencia en días de vencimiento impositivo y validaciones estrictas. Diseñar un sistema robusto requiere contemplar estos escenarios:

Código de Error Descripción en ARCA Causa Raíz Solución en Código
10016 El número de comprobante no se corresponde con el próximo a autorizar Se envió un número ya emitido o se salteó un correlativo Consultar siempre FECompUltimoAutorizado antes del envío o implementar locks optimistas/pesimistas en la base de datos
10017 La suma de los importes no coincide con el total Descuadre de redondeo o mala asignación de IVA/Trib Forzar redondeo a 2 decimales (Number.toFixed(2) / round(x, 2)) y chequear la ecuación contable antes del POST
10004 El campo CbteFch no puede diferir en más de X días La fecha enviada es muy antigua o futura Para bienes (Concepto 1), no más de 5 días de diferencia; para servicios (Concepto 2 o 3), no más de 10 días
500 / 502 HTTP Fallo interno de ARCA / Timeout de red Caída de servidores fiscales No reenviar a ciegas con el mismo número sin chequear antes si el comprobante ya se autorizó con FECompConsultar
wsaa:500 Certificado no emitido por AC de confianza Certificado vencido o de ambiente cruzado Verificar si se está usando el certificado de homologación contra la URL de producción o viceversa

Idempotencia y recuperación ante Timeouts

Si tu llamada a FECAESolicitar devuelve un error de timeout de red (HTTP 504) o conexión abortada:

  1. NUNCA descartes el pedido ni generes una nueva factura. Es muy probable que ARCA haya procesado el comprobante pero tu servidor no haya recibido la respuesta.
  2. Ejecutá una llamada a FECompConsultar(PtoVta, CbteTipo, CbteNro).
  3. Si la respuesta contiene un CAE válido, guardalo en tu base de datos.
  4. Si la consulta responde que el comprobante no existe, recién ahí podés reintentar la llamada a FECAESolicitar.

Generación del código QR fiscal obligatorio

Por normativa de ARCA (Resolución General 4291/2018 y complementarias), toda factura electrónica impresa o enviada por PDF debe incluir un código QR que apunta a la verificación pública en el sitio oficial.

La URL que debe codificarse dentro del QR tiene la siguiente estructura:

https://www.afip.gob.ar/fe/qr/?p= + JSON codificado en Base64

El objeto JSON debe contener estos campos exactos:

{
  "ver": 1,
  "fecha": "2026-09-15",
  "cuit": 30712345678,
  "ptoVta": 2,
  "tipoCmp": 6,
  "nroCmp": 1042,
  "importe": 150000.00,
  "moneda": "PES",
  "ctz": 1.0,
  "tipoDocRec": 96,
  "nroDocRec": 35123456,
  "tipoCodAut": "E",
  "codAut": 74382910482910
}

Donde tipoCodAut es "E" para CAE y codAut es el número de CAE de 14 dígitos devuelto por el WSFE.


Cómo acelerar tu implementación con Deepyze

Conectar un sistema de gestión, un e-commerce o una aplicación SaaS a la facturación electrónica argentina no debería consumir meses de desarrollo ni provocar dolores de cabeza contables con comprobantes desfasados o servidores caídos.

En Deepyze somos especialistas en software a medida y desarrollo de APIs e integraciones de misión crítica. Hemos implementado motores de facturación automatizada que procesan millones de comprobantes mensuales, integrados con pasarelas de pago, stocks multidepósito y CRMs.

Si necesitás integrar ARCA en tu aplicación existente, migrar sistemas legacy o automatizar la conciliación fiscal de tu empresa con garantía de uptime y cumplimiento normativo:

Preguntas frecuentes

¿Qué cambió en los Web Services con la disolución de AFIP y la creación de ARCA?+

A nivel protocolar, los Web Services mantienen compatibilidad con los endpoints SOAP de WSFEV1 y WSAA bajo los dominios de ARCA y AFIP. Sin embargo, cambiaron las políticas de seguridad (exigencia estricta de TLS 1.2/1.3), se renovaron las cadenas de autoridades certificantes (CA raíz) y las librerías antiguas de npm/PyPI que usaban OpenSSL obsoleto o algoritmos SHA-1 fallan al firmar los tickets TRA con CMS/PKCS#7.

¿Por qué no debo generar un Ticket de Requerimiento de Acceso (TRA) por cada factura emitida?+

El Ticket de Acceso (Token y Sign) otorgado por el WSAA tiene una validez de 12 horas. Solicitar un nuevo ticket por cada comprobante satura los servidores de ARCA, degrada la latencia de tu sistema de 150ms a más de 3 segundos por factura y provoca bloqueos temporales de IP por rate-limiting (FaultCode wsaa:500). El token y el sign deben guardarse en un caché (Redis o memoria) con un TTL de 11 a 11.5 horas.

¿Cómo pruebo mi integración en homologación sin afectar la contabilidad real?+

ARCA provee el entorno de 'Testing / Homologación' (servicios wsaahomo y wsfehomo). Para usarlo necesitás generar un certificado digital específico para testing en el portal con Clave Fiscal mediante el servicio 'WSASS - Autogestión de Certificados de Homologación', asociar tu CUIT al computador fiscal y apuntar tus clientes SOAP a los WSDL de prueba sin valor fiscal.

¿Qué significa el error de ARCA 10016 y cómo se soluciona automáticamente?+

El error 10016 indica que 'El número de comprobante no se corresponde con el próximo a autorizar'. Ocurre cuando se intenta emitir un número que no es exactamente el último autorizado + 1 para ese Punto de Venta y tipo de comprobante. La solución consiste en consultar siempre el método `FECompUltimoAutorizado` antes de armar el payload de `FECAESolicitar` o sincronizar la secuencia en base de datos con locks transaccionales.

¿Querés que esto funcione en tu empresa?

En Deepyze convertimos procesos manuales en sistemas que trabajan solos: automatización con IA, apps web y móviles, y software a medida. Contanos tu caso y en 24 hs tenés una propuesta concreta.

Sin compromiso · Respuesta en 24 hs · Equipo en tu mismo huso horario

Servicio relacionado

¿Necesitás Automatización con IA para tu empresa?

En Deepyze lo construimos a medida, con un equipo en tu mismo huso horario y propuesta en 24 hs.

Ver servicio de Automatización con IA

Seguir leyendo