Guía de inicio
De la key al primer request
Esta guía recorre el camino mínimo de una integración, en orden: qué hace falta, cómo se autentica, cuál es la primera llamada, cómo se encola una extracción, cómo se emite sin duplicar comprobantes y cómo se lee un error. Todos los endpoints, campos y códigos que aparecen acá salen del contrato OpenAPI 3.1.0; si esta página y el contrato no coinciden, el contrato manda.
01
Qué hace falta
- Una API key. Se emite desde la consola, se muestra una sola vez y se guarda en ese momento: si se pierde, se rota y no se recupera. Un despliegue local necesita la API corriendo del otro lado. Ir a la consola →
- Un cliente HTTP.
curlalcanza para todo lo que sigue; cualquier cliente equivalente sirve. - El contrato. OpenAPI 3.1.0, con todos los paths de negocio bajo el prefijo de versión y el esquema de cada respuesta. La copia del repositorio se sirve en /openapi.json y la versión en vivo en https://api.arcanoe.com.ar/api/v2/openapi.json.
02
Autenticación
La API pública declara un solo esquema de seguridad, BearerAuth (type: http, scheme: bearer), y lo describe con estas palabras: Token API provisto en la cabecera Authorization: Bearer <api-key>
. En la práctica, una cabecera por request, sin sesión ni cookie:
Authorization: Bearer <api-key>El ambiente lo determina la key. El contrato lo dice en el campo ambiente: homologacion
con una API Key de test y produccion
con una de live, según el ambiente de la key. No se elige por URL ni por parámetro, y no hay fallback cruzado: una key presentada contra el despliegue del otro ambiente recibe 403 con motivo: api_key_ambiente_invalido.
La clave fiscal viaja cifrada en cada request y no se custodia. El contrato describe el patrón como cifrado efímero de credenciales fiscales (RSA-OAEP-SHA256) sin custodia de contraseñas: la clave se cifra en el cliente contra la clave pública vigente, que se obtiene con GET /api/v2/credenciales/public-key, y el servidor la descifra en memoria efímera, sin persistencia. De ahí que el reintento de un job vuelva a pedir la clave cifrada: no hay una copia guardada que reutilizar.
La identidad del tenant sale exclusivamente de la key. No hay parámetro que la elija, así que un job de otro tenant no aparece nunca en una lista ni en un total.
03
El primer request
Antes de encolar nada conviene comprobar dos cosas que no cuestan nada: que la key viaja bien y que el servidor de ARCA contesta. GET /api/v2/ws/status es esa llamada, y el contrato la describe sin ambigüedad: es liveness del servidor WSFE y nunca lee credenciales ni emite comprobantes
.
Cada parte del comando tiene un motivo:
exportdefine la variable que el comando interpola. Sin ella, curl mandaAuthorization: Bearervacío y recibe401.- Las comillas del header evitan que el espacio de
Bearercorte la cabecera en dos argumentos. -sSimprime un fallo de DNS o TLS en lugar de tragárselo.-wimprime el código de estado: es la única forma de distinguir un200de un401a simple vista.
export ARCA_KEY="la key que se copió"
curl -sS -w "\nHTTP %{http_code}\n" "https://api.arcanoe.com.ar/api/v2/ws/status" \
-H "Authorization: Bearer $ARCA_KEY"La respuesta es el cuerpo de WsStatusResponse, con cinco campos, todos requeridos por el contrato:
{
"servicio": "wsfe",
"ambiente": "homologacion",
"app_server": "<AppServer informado por AFIP>",
"db_server": "<DbServer informado por AFIP>",
"auth_server": "<AuthServer informado por AFIP>"
}Un 200 acá no dice que una clave fiscal sea válida: dice que la key viaja bien y que el servidor de ARCA responde. La credencial fiscal se valida recién cuando se encola una extracción. Los tres campos de servidor son los que AFIP informa en ese momento, así que cambian entre corridas; compararlos entre dos llamadas es una forma barata de detectar que el otro lado se movió.
04
Extracción asíncrona
Una extracción no devuelve los comprobantes en la misma respuesta. POST /api/v2/jobs valida el pedido, descifra la clave en memoria efímera y responde 202 con un identificador de job:
{
"job_id": "job_a1b2c3d4e5f60718",
"estado": "encolado"
}La clave fiscal nunca viaja en claro: se cifra contra la clave pública del servicio (GET /api/v2/credenciales/public-key, RSA-OAEP con SHA-256) y viaja en clave_encriptada. El servicio la descifra en memoria efímera y no la persiste. El listado de campos del esquema CrearJobRequest está en la referencia; este programa hace los tres pasos con Node 18 o superior, sin dependencias:
// node 18+, sin dependencias
const base = process.env.ARCANOE_API_URL;
const key = process.env.ARCANOE_API_KEY;
const { publicEncrypt, constants } = require("node:crypto");
const { public_key_pem } = await (await fetch(`${base}/api/v2/credenciales/public-key`, {
headers: { authorization: `Bearer ${key}` },
})).json();
const clave_encriptada = publicEncrypt(
{ key: public_key_pem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
Buffer.from(process.env.ARCA_CLAVE_FISCAL, "utf8"),
).toString("base64");
const creado = await fetch(`${base}/api/v2/jobs`, {
method: "POST",
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
body: JSON.stringify({
cuit_representante: "<CUIT>",
cuit_representado: "<CUIT>",
desde: "<DESDE>", // ISO: 2026-09-01
hasta: "<HASTA>", // ISO, máximo 365 días
opciones: { tipo: "mc_recibidos" },
clave_encriptada,
}),
});
console.log(creado.status, await creado.json()); // 202 { job_id, estado: "encolado" }El estado autoritativo se lee después, con la llamada de polling sobre el mismo job. Un job puede pedir varios servicios con un solo login: cada uno trae su propio estado, su motivo tipado, su conteo y sus filas, en el orden en que se pidieron.
export JOB_ID="job_a1b2c3d4e5f60718"
curl -sS -w "\nHTTP %{http_code}\n" "https://api.arcanoe.com.ar/api/v2/jobs/$JOB_ID" \
-H "Authorization: Bearer $ARCA_KEY"El ciclo de vida consolidado es encolado · ejecutando · completado · parcial · requiere_humano · fallido. Se consulta hasta que el estado deje de ser encolado o ejecutando. El contrato no declara webhook ni callback: el seguimiento es polling.
Una corrida cuyo conteo cerró se ve así:
{
"job_id": "job_a1b2c3d4e5f60718",
"estado": "completado",
"creado_en": "2026-09-17T15:40:02.000Z",
"actualizado_en": "2026-09-17T15:42:11.000Z",
"login": {
"estado": "ok",
"transmisiones_de_clave": 1
},
"servicios": [
{
"servicio": "mc_recibidos",
"estado": "completado",
"conteo": {
"declarado": 10,
"obtenido": 10
}
}
]
}El candado del producto está en el conteo. declarado es el total de comprobantes que ARCA informa para el período; obtenido es el total descargado y verificado. Cuando los dos números no coinciden, el servicio no se da por completo: lleva motivo: guardia_conteo, y el job agrega a parcial cuando hay resultados mixtos. Las filas solo aparecen cuando la guardia de conteo de ese servicio pasó.
Un parcial no es un éxito con asterisco. Es la corrida diciendo que el conteo no cerró, en lugar de entregar menos filas como si estuvieran todas. Un cliente que necesite el período completo tiene que tratar parcial como una falla de completitud, no como un resultado usable.
05
Emisión idempotente
La emisión es POST /api/v2/ws/wsfe/vouchers y el header Idempotency-Key es obligatorio: sin él, la respuesta es 400 con motivo: idempotency_key_requerida. La clave se elige del lado del cliente y tiene que identificar la operación, no el intento.
curl -sS -w "\nHTTP %{http_code}\n" -X POST "https://api.arcanoe.com.ar/api/v2/ws/wsfe/vouchers" \
-H "Authorization: Bearer $ARCA_KEY" \
-H "Idempotency-Key: orden-12345" \
-H "Content-Type: application/json" \
-d @comprobante.jsonLa primera emisión vuelve 201. Un reintento con la misma clave y el mismo payload vuelve 200 con replay: true y no emite un segundo comprobante: la respuesta es la del comprobante ya emitido, sin volver a invocar a ARCA. Los otros dos desenlaces también están en el contrato: la misma clave con otro payload responde 409 con idempotency_conflict_payload, y una clave con una emisión en vuelo responde 409 con idempotency_in_flight.
{
"servicio": "wsfe",
"ambiente": "homologacion",
"idempotency_key": "orden-12345",
"numero": 11,
"cae": "74012345678901",
"replay": false
}La creación de jobs tiene la misma mecánica con una diferencia: Idempotency-Key es opcional en POST /api/v2/jobs. Con el header, el replay vuelve 200 en lugar de 202, con el identificador del job original y replay: true; la clave se particiona por ambiente y por tenant, y vive 7 días, la misma ventana que la emisión. Sin el header, cada request crea un job nuevo.
06
Errores
Hay dos taxonomías cerradas y no se inventan motivos nuevos. Cada una vive en su esquema del contrato, y las dos se publican: están en components.schemas del contrato vendorizado y en la copia en vivo.
Extracción y ejecución — MotivoError. Es la taxonomía del motor: se devuelve en ErrorResponse, en el motivo de cada servicio y en JobErrorResponse, que le agrega los cuatro códigos de idempotencia de la creación. interno es la red de seguridad para un fallo que no encaja en ningún otro motivo, y existe solo en esta lista.
- credencial_invalida
- captcha
- transporte
- portal_rechazo
- guardia_conteo
- servicio_no_soportado
- parametros_invalidos
- storage
- interno
Superficie WS — WsMotivoError. Es la de las consultas y la emisión: se devuelve en WsErrorResponse. No tiene catch-all, así que no incluye interno, y distingue dos casos que en la otra taxonomía no existen: ws_incierto_verificar, para un timeout con estado incierto que hay que verificar antes de reintentar, y emision_rechazada_afip, para un rechazo formal del comprobante.
- servicio_no_configurado
- ws_no_disponible
- not_found
- parametros_invalidos
- credencial_invalida
- credencial_no_soportada
- idempotency_key_requerida
- idempotency_conflict_payload
- idempotency_in_flight
- intento_rechazado
- ws_incierto_verificar
- emision_rechazada_afip
El cuerpo del error es un objeto con un campo motivo. La excepción es el 401, que trae error en lugar de motivo; el contrato lo documenta como {"error": "unauthorized"} y es lo que se ve cuando la key falta, está mal escrita o fue revocada.
07
Siguientes pasos
El contrato en vivo. Es la fuente de verdad de endpoints, esquemas y códigos: https://api.arcanoe.com.ar/api/v2/openapi.json. La copia del repositorio, para leer a ojo, está en /openapi.json.
El prompt para un agente. Si la integración la escribe un agente, el camino corto es darle el contrato y el primer objetivo, no esta página:
Lee el contrato OpenAPI 3.1.0 servido en
https://api.arcanoe.com.ar/api/v2/openapi.json y trátalo como la fuente de verdad de endpoints,
esquemas, enums y códigos de error. No uses campos ni motivos que no estén ahí.
Implementa primero GET /api/v2/ws/status para verificar que la key viaja bien.
Después, la extracción asincrónica: POST /api/v2/jobs y polling con
GET /api/v2/jobs/{job_id} hasta que el estado deje de ser "encolado" o "ejecutando".
Trata "parcial" como un resultado que hay que informar, nunca como un éxito.
Para la emisión, manda siempre el header Idempotency-Key y no reintentes con otra
clave el mismo comprobante.La referencia interactiva. Cada endpoint con su esquema, sus respuestas y un botón de prueba vive en la referencia de la API. Esta guía enseña el orden; la referencia contesta el detalle.
El contrato está vendorizado en spec/openapi.yaml y los tests del sitio lo leen desde ahí: un endpoint o un motivo que no esté en el contrato no puede aparecer en esta página. El host de las llamadas es el que declara servers: https://api.arcanoe.com.ar.