API de FactuKey

Facturación electrónica ante ARCA: comprobantes A, B y C, notas de crédito y débito, remitos, firma electrónica y contingencia.

Inicio rápido

Emití una factura a consumidor final y esperá el CAE en la misma respuesta (hasta 20 segundos):

curl -X POST "https://api.factukey.com.ar/v1/comprobantes?esperar=20" \
  -H "Authorization: Bearer fk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: venta-1043" \
  -d '{
    "items": [{ "descripcion": "Café en grano 1kg", "cantidad": 2, "precio_unitario": 18500 }],
    "precios_con_iva": true
  }'

FactuKey elige la letra (A, B o C) según tu condición frente al IVA y la del receptor, calcula neto e IVA, toma el próximo número de ARCA y devuelve:

{
  "id": "6f1c…",
  "estado": "autorizado",
  "tipo": "Factura B",
  "numero_completo": "00003-00001043",
  "importes": { "neto": 30578.51, "iva": 6421.49, "total": 37000, ... },
  "cae": "76412345678901",
  "cae_vencimiento": "2026-10-14",
  "pdf_url": "https://api.factukey.com.ar/p/c/6f1c…?t=…",
  "qr_url": "https://www.afip.gob.ar/fe/qr/?p=…"
}

Si ARCA demora, la respuesta es 202 con estado: "pendiente": FactuKey sigue intentando sola y te avisa por webhook. Podés consultar con GET /v1/comprobantes/:id.

Autenticación

Todas las requests llevan Authorization: Bearer fk_live_… (producción) o fk_test_… (homologación). Las keys se crean en el panel y se muestran una sola vez, y quedan atadas al ambiente: cuando la cuenta pasa a producción, las fk_test_ dejan de funcionar (así una key de pruebas nunca emite facturas reales). Límite: 120 requests por minuto por key.

Idempotencia: mandá Idempotency-Key en cada POST. Si reintentás con la misma clave y el mismo cuerpo, recibís el mismo comprobante (header Idempotent-Replayed: true); con otro cuerpo, 409 idempotencia_conflicto. Así un corte de red nunca duplica una factura.

Comprobantes

POST/v1/comprobantesEmite. ?esperar=N (máx. 30 s) espera el CAE.
GET/v1/comprobantesLista. Filtros: estado, desde, hasta, tipo_comprobante, receptor, limite, antes_de.
GET/v1/comprobantes/:idDetalle.
GET/v1/comprobantes/:id/pdfPDF con QR (?descargar para adjunto).
GET/v1/meDatos de la cuenta, puntos de venta y letras habilitadas.
GET/v1/parametrosTablas de referencia (sin autenticación).

Cuerpo

clasefactura (default), nota_credito, nota_debito
tipo_comprobanteCódigo ARCA (1, 6, 11…). Opcional: si falta, FactuKey elige la letra.
punto_ventaOpcional; por defecto el primero habilitado.
conceptoproductos (default), servicios, productos_y_servicios. Con servicios: servicio_desde, servicio_hasta, vencimiento_pago (por defecto, el mes en curso).
fechaYYYY-MM-DD, hoy por defecto (ARCA admite ±5 días para productos y ±10 para servicios).
receptor{ tipo_doc, nro_doc, razon_social, domicilio, condicion_iva, email }. Omitilo para consumidor final anónimo. condicion_iva: responsable_inscripto, monotributista, exento, consumidor_final, etc. (obligatorio en ARCA desde la RG 5616).
items[]{ descripcion, cantidad, precio_unitario, alicuota_iva, bonificacion_porcentaje, codigo, unidad }. alicuota_iva: 0, 2.5, 5, 10.5, 21 (default), 27, "exento" o "no_gravado".
precios_con_ivatrue si los precios son finales. El total del comprobante es exactamente la suma de los ítems.
moneda, cotizacionPES por defecto. Para DOL/060, informar la cotización.
tributos[]Percepciones u otros tributos: { id, descripcion, base_imponible, alicuota, importe }.
comprobante_asociadoObligatorio en notas: { tipo_comprobante, punto_venta, numero }.
permitir_contingenciatrue por defecto: si ARCA no responde y tenés CAEA, se autoriza en contingencia.

Nota de crédito

{ "clase": "nota_credito",
  "receptor": { "nro_doc": "30712345678", "condicion_iva": "responsable_inscripto", "razon_social": "Cliente SA" },
  "items": [{ "descripcion": "Devolución", "precio_unitario": 1000 }],
  "comprobante_asociado": { "tipo_comprobante": 1, "punto_venta": 3, "numero": 1043 } }

Remitos

ARCA no tiene remito electrónico general, así que los remitos son documentos de FactuKey con numeración propia y PDF con espacio para la conformidad. Tipo X (interno) o R si el punto de venta tiene CAI de remitos.

POST/v1/remitos{ tipo, receptor, items[{descripcion, cantidad, unidad}], transporte, comprobante_id, valor_declarado }
GET/v1/remitos
GET/v1/remitos/:id
GET/v1/remitos/:id/pdf
POST/v1/remitos/:id/anular{ motivo }

Impresión en comandera (tickets)

Cada comprobante y remito autorizado trae ticket_url, la representación impresa para papel térmico de 80 o 58 mm (datos fiscales, ítems, totales, CAE y QR). El mismo ticket sale en cuatro formatos:

GET/v1/comprobantes/:id/ticketParámetros: ancho=80|58 (default 80), salida=html|pdf|escpos|json (default html), imprimir (el HTML llama a print() al cargar).
GET/v1/remitos/:id/ticketIgual, con espacio para la firma de conformidad.
htmlPara imprimir desde el navegador. Fija la página al ancho del rollo y al alto del contenido.
pdfPara guardar o compartir el ticket.
escposBytes ESC/POS listos para mandar crudos a la impresora (página de códigos PC850, QR nativo y corte de papel).
jsonLas líneas del ticket (texto, par, separador, qr…) para dibujarlo con tus propias plantillas.

Desde el navegador (sin instalar nada)

La API key nunca va al navegador: tu servidor pide el ticket y lo sirve desde tu propio dominio. Así podés imprimirlo en un iframe oculto.

// app/api/facturas/[id]/ticket/route.ts (Next.js, en tu servidor)
export async function GET(_: Request, { params }: { params: { id: string } }) {
  const r = await fetch(`https://api.factukey.com.ar/v1/comprobantes/${params.id}/ticket?ancho=80`, {
    headers: { Authorization: `Bearer ${process.env.FACTUKEY_API_KEY}` },
  });
  return new Response(r.body, { status: r.status, headers: { "content-type": "text/html; charset=utf-8" } });
}

// En el cliente: imprimir sin abrir otra pestaña
function imprimirTicket(id: string) {
  const f = document.createElement("iframe");
  f.style.display = "none";
  f.src = `/api/facturas/${id}/ticket`;
  f.onload = () => { f.contentWindow?.print(); setTimeout(() => f.remove(), 60_000); };
  document.body.appendChild(f);
}

Configurá la comandera como impresora predeterminada de la PC de la caja. Para que imprima sin mostrar el diálogo, abrí Chrome con --kiosk-printing.

Con un agente local (impresión silenciosa, varias impresoras)

Si tu sistema ya tiene un agente en el local que imprime comandas, pedile el ticket en escpos y mandalo a la impresora tal cual. Por ejemplo, a una comandera de red en el puerto 9100:

import net from "node:net";

const r = await fetch(`https://api.factukey.com.ar/v1/comprobantes/${id}/ticket?ancho=80&salida=escpos`, {
  headers: { Authorization: `Bearer ${FACTUKEY_API_KEY}` },   // o pedíselo a tu servidor
});
const bytes = Buffer.from(await r.arrayBuffer());
const s = net.connect(9100, "192.168.1.50", () => s.end(bytes));

Si preferís usar tus propias plantillas (las mismas que para las comandas), usá salida=json y dibujá cada línea. El logo no se incluye en ESC/POS: conviene grabarlo una vez en la memoria de la impresora (NV) desde el agente.

Firma electrónica

Con la integración de Signet habilitada (API key en el panel), cualquier comprobante o remito se puede mandar a firmar:

POST /v1/remitos/:id/firma
{ "firmantes": [{ "rol": "receptor", "nombre": "Juan Pérez", "email": "[email protected]", "dni": "30123456" }],
  "delivery": "email" }

Respuesta 202 con la firma en curso. GET /v1/firmas/:id trae el estado y los links de firma (con delivery: "none" los mostrás vos en tu app). Al completarse: GET /v1/firmas/:id/pdf y webhook firma.completada.

Contingencia (CAEA)

Habilitala en el panel y agregá un punto de venta de tipo CAEA (en ARCA: "RECE para aplicativo y web services - CAEA"). FactuKey pide el CAEA de cada quincena y:

POST /v1/contingencia/lotes          { "tipo_comprobante": 6, "cantidad": 50 }
→ { "lote_id": "…", "caea": "36401234567890", "punto_venta": 90, "desde": 121, "hasta": 170, "vigencia": {...} }

# Sin conexión: imprimí cada comprobante con ese CAEA (QR con tipoCodAut "A").
# Al volver la conexión, rendí cada uno:
POST /v1/contingencia/comprobantes   { "lote_id": "…", "numero": 121, "generado_at": "2026-10-04T15:42:10-03:00",
                                       "comprobante": { ...mismo cuerpo que /v1/comprobantes... } }

# Y cerrá el lote indicando el último número que usaste (desde - 1 si no usaste ninguno):
POST /v1/contingencia/lotes/:id/cerrar   { "usados_hasta": 134 }

Usá los números en orden y cerrá el lote apenas vuelvas a estar en línea: ARCA exige que los comprobantes CAEA se informen correlativos. GET /v1/contingencia muestra el CAEA vigente, los lotes abiertos y lo que falta informar.

Webhooks

Configurá la URL en el panel. Eventos: comprobante.autorizado, comprobante.rechazado, firma.completada, certificado.por_vencer. Cuerpo: { id, evento, creado, datos }. Se reintenta hasta 10 veces con backoff.

Verificá la firma: X-FactuKey-Firma: t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secreto, t + "." + cuerpo).

const [t, v1] = req.headers["x-factukey-firma"].split(",").map((p) => p.split("=")[1]);
const esperado = crypto.createHmac("sha256", SECRETO).update(`${t}.${cuerpoCrudo}`).digest("hex");
if (esperado !== v1 || Date.now() / 1000 - Number(t) > 300) return res.status(401).end();

MCP (agentes de IA)

FactuKey es un servidor MCP (Streamable HTTP). Conectalo a Claude u otro cliente MCP con tu API key:

{
  "mcpServers": {
    "factukey": {
      "type": "http",
      "url": "https://api.factukey.com.ar/mcp",
      "headers": { "Authorization": "Bearer fk_live_..." }
    }
  }
}

Herramientas: datos_emisor, previsualizar_comprobante, emitir_comprobante, consultar_comprobante, listar_comprobantes, emitir_remito, listar_remitos, solicitar_firma, consultar_firma, estado_contingencia.

Por WhatsApp funciona igual: los teléfonos autorizados en el panel le escriben al número de FactuKey ("facturale 3 cafés a consumidor final") y el asistente previsualiza, pide confirmación, emite y devuelve el PDF.

Errores

{ "error": { "codigo": "datos_invalidos", "mensaje": "Datos inválidos", "detalle": [{ "campo": "items", "mensaje": "…" }] } }
400datos_invalidos, json_invalido
401no_autorizado
404no_encontrado
409idempotencia_conflicto, no_autorizado (el comprobante todavía no tiene CAE), firma_no_habilitada
422Reglas de negocio: letra_invalida, receptor_invalido, sin_punto_venta, fecha_fuera_de_rango, sin_caea…
429limite_excedido

Un comprobante rechazado por ARCA no es un error HTTP: la respuesta trae estado: "rechazado" con errores_arca y observaciones_arca. El número no se consume.