afiPAPI

Cómo integrar la API desde Visual FoxPro · v0.1.4+

Introducción

afiPAPI es una API HTTP (.NET 8) que hace de puente hacia los webservices SOAP de AFIP/ARCA. Desde VFP solo consumís endpoints HTTP con JSON: no manejás SOAP, ni token, ni vencimientos.

La API resuelve por vos:

  • Autenticación WSAA (firma con certificado).
  • Obtención, cache y renovación de token / sign.
  • Acceso a WSFEv1 (emisión y consulta de comprobantes, tablas).
  • Diagnóstico (health, status, dashboard).

VFP solo debe: registrar el certificado una vez, pedir operaciones de negocio, interpretar resultados y loguear errores.

ℹ️
Base URL. En desarrollo suele ser http://localhost:5014 o http://localhost:5000. Todos los endpoints cuelgan de /api/afip.
⚠️
Regla de oro. Un HTTP 200 no implica éxito. Siempre revisá primero el campo ok del JSON.

Arquitectura recomendada en VFP

Tres capas, para no mezclar HTTP con lógica fiscal en los formularios.

AfipApi.prg · capa técnicaGET/POST, arma JSON, parseo simple
FacturadorAfip.prg · capa de negocioInicializar, EstaDisponible, Emitir…
AfipHelpers.prg · semántica fiscalConstantes y códigos AFIP

El módulo de facturación habla solo con FacturadorAfip y usa AfipHelpers para no escribir números mágicos. Si cambia la API, se toca una sola clase.

Inicio rápido

Instanciar, inicializar y emitir, en pocas líneas.

foxpro
LOCAL loFact, loH, loEmi

loH = NEWOBJECT("AfipHelpers", "AfipHelpers.prg")

loFact = NEWOBJECT("FacturadorAfip", "FacturadorAfip.prg", "", ;
   "http://localhost:5014", ;        && Base URL de la API
   20238233195, ;                    && CUIT
   .F., ;                            && Producción (.F. = homologación)
   "C:\Certificados\20238233195.pfx", ;
   "CLAVE_CERTIFICADO")

* 1) Registrar certificado + login (una vez por empresa)
loFact.Inicializar()

* 2) ¿AFIP y la API están operativos?
IF NOT loFact.EstaDisponible().Ok
   ? "AFIP no disponible"
   RETURN
ENDIF

* 3) Emitir una Factura B a Consumidor Final
loEmi = loFact.EmitirFactura( ;
   1, ;                               && Punto de venta
   loH.CbteFacturaB(), ;             && Tipo (6)
   loH.ConceptoProductos(), ;        && Concepto (1)
   loH.DocConsumidorFinal(), ;       && Doc tipo (99)
   0, ;                              && Doc número
   121, 0, 0, 100, 21, ;            && Total, NoGrav, Exento, Gravado, IVA
   loH.MonedaPesos(), 1, ;          && Moneda, cotización
   loH.IvaReceptorConsumidorFinal(),; && Cond. IVA receptor (5)
   loH.AlicIva21(), ;               && Alícuota IVA (5 = 21%)
   "", "", "")                       && Fechas de servicio (solo concepto 2/3)

IF loEmi.Ok
   ? "CAE:", loEmi.CAE, " Número:", loEmi.NumeroComprobante
ELSE
   ? "Error:", loEmi.ErrorMessage
ENDIF
Instanciá FacturadorAfip una vez por empresa/CUIT (al abrir caja o el módulo), no dentro de cada botón.

Formato de respuesta

Todas las respuestas tienen la misma forma.

Éxito

json
{ "ok": true, "data": { /* … */ } }

Error

json
{ "ok": false, "data": {
    "error": "mensaje claro",
    "detail": "detalle técnico",
    "status": 500
} }

Manejo en VFP

foxpro
IF loResp.Ok
   * usar loResp.Body / campos parseados
ELSE
   * mostrar mensaje y loguear: fecha, CUIT, PV, tipo,
   * request, loResp.Body, loResp.Status, loResp.ErrorMessage
ENDIF
⚠️
AFIP a veces responde 200 pero con un rechazo funcional adentro del JSON. Por eso siempre se chequea ok.

Clase 1 · AfipApi.prg

Cliente técnico HTTP/JSON. Usa MSXML2.ServerXMLHTTP.6.0 y helpers de parseo por texto.

foxpro
DEFINE CLASS AfipApi AS Custom
    BaseUrl = ""
    Cuit = 0
    Produccion = .F.

    PROCEDURE Init(tcBaseUrl, tnCuit, tlProduccion)
        IF PCOUNT() >= 1 THEN THIS.BaseUrl = ALLTRIM(tcBaseUrl)
        IF PCOUNT() >= 2 THEN THIS.Cuit = tnCuit
        IF PCOUNT() >= 3 THEN THIS.Produccion = tlProduccion
    ENDPROC

    FUNCTION ApiRequest(tcMetodo, tcUrl, tcJson)
        LOCAL loHttp, loResp
        loResp = THIS.BuildResponse()
        TRY
            loHttp = CREATEOBJECT("MSXML2.ServerXMLHTTP.6.0")
            loHttp.setTimeouts(5000, 5000, 15000, 15000)
            loHttp.Open(UPPER(ALLTRIM(tcMetodo)), tcUrl, .F.)
            loHttp.setRequestHeader("Content-Type", "application/json")
            IF VARTYPE(tcJson) = "C" AND NOT EMPTY(tcJson)
                loHttp.Send(tcJson)
            ELSE
                loHttp.Send()
            ENDIF
            loResp.Status = loHttp.Status
            loResp.Body   = loHttp.responseText
            loResp.Ok     = (loHttp.Status >= 200 AND loHttp.Status < 300)
        CATCH TO loEx
            loResp.Ok = .F.
            loResp.ErrorMessage = loEx.Message
        ENDTRY
        RETURN loResp
    ENDFUNC

    FUNCTION ApiGet(tcPath)
        RETURN THIS.ApiRequest("GET", THIS.BaseUrl + tcPath, "")
    ENDFUNC
    FUNCTION ApiPost(tcPath, tcJson)
        RETURN THIS.ApiRequest("POST", THIS.BaseUrl + tcPath, tcJson)
    ENDFUNC

    * --- Helpers JSON (parseo simple por texto) ---
    FUNCTION JsonGetValue(tcJson, tcPropiedad)   && devuelve string
    FUNCTION JsonGetBool(tcJson, tcPropiedad)    && devuelve .T./.F.
    FUNCTION JsonGetNumber(tcJson, tcPropiedad)  && devuelve número
    * (ver implementación completa en integracion-vfp-api.md)
ENDDEFINE
ℹ️
El parseo JSON es por texto: ideal para valores simples (CAE, ultimoNumero, tokenActivo). Para arrays grandes (ej. resumen-emitidos), conviene un parser JSON real o recorrer el Body.

Clase 2 · FacturadorAfip.prg

Capa de negocio. Devuelve objetos con campos ya parseados (.Ok, .CAE, .UltimoNumero…).

foxpro
loFact = NEWOBJECT("FacturadorAfip", "FacturadorAfip.prg", "", ;
   tcBaseUrl, tnCuit, tlProduccion, tcCertPath, tcCertPassword)

loFact.Inicializar()                            && /login (registra cert + token)
loFact.EstaDisponible()                         && /status -> .TokenActivo, .MinutosRestantes
loFact.GetUltimoComprobante(tnPV, tnTipo)       && /ultimo  -> .UltimoNumero
loFact.EmitirFactura(...)                       && /emitir  -> .CAE, .NumeroComprobante, .Resultado
loFact.GetTiposIva()                            && /tipos-iva  (y demás tablas)

Patrón de un método de negocio

foxpro
FUNCTION GetUltimoComprobante(tnPuntoVenta, tnTipoComprobante)
    LOCAL loResp, loOut
    loResp = THIS.oApi.Ultimo(tnPuntoVenta, tnTipoComprobante)
    loOut = CREATEOBJECT("Empty")
    ADDPROPERTY(loOut, "Ok", loResp.Ok)
    ADDPROPERTY(loOut, "UltimoNumero", 0)
    ADDPROPERTY(loOut, "Body", loResp.Body)
    ADDPROPERTY(loOut, "ErrorMessage", loResp.ErrorMessage)
    IF loResp.Ok
        loOut.UltimoNumero = THIS.oApi.JsonGetNumber(loResp.Body, "ultimoNumero")
    ENDIF
    RETURN loOut
ENDFUNC

Clase 3 · AfipHelpers.prg

Catálogo de constantes para no usar números mágicos. Ver todos los valores en Tablas de códigos.

foxpro
loH = NEWOBJECT("AfipHelpers", "AfipHelpers.prg")

loH.CbteFacturaB()                 && 6
loH.DocCUIT()                      && 80
loH.ConceptoProductos()            && 1
loH.IvaReceptorConsumidorFinal()   && 5
loH.AlicIva21()                    && 5
loH.MonedaPesos()                  && "PES"

* También descripciones legibles para UI / logs:
? loH.DescTipoComprobante(6)       && "Factura B"
? loH.DescCondicionIvaReceptor(5)  && "Consumidor Final"
? loH.DescAlicuotaIva(5)           && "21%"

* Y validaciones:
IF loH.RequiereFechasServicio(tnConcepto)   && concepto 2 o 3
   * pedir fechas de servicio
ENDIF

Endpoint · login

POST/api/afip/login
Autentica un CUIT contra WSAA, registra el certificado y cachea token/sign. En VFP: loFact.Inicializar().
json · request
{ "cuit": 20238233195, "servicio": "wsfe",
  "certificadoPath": "C:\\Certificados\\cert.pfx",
  "certificadoPassword": "clave", "produccion": false }
json · data
{ "token": "abc123", "sign": "xyz456",
  "expiration": "2026-03-20T12:00:00" }

Endpoint · status

GET/api/afip/status?cuit=&produccion=
Estado del token/certificado de un CUIT. En VFP: loFact.EstaDisponible().
json · data
{ "cuit": 20238233195, "tokenActivo": true,
  "certificadoRegistrado": true,
  "expira": "2026-03-20T12:00:00", "minutosRestantes": 120 }

Endpoint · health

GET/api/afip/health?produccion=
Salud de la API + Dummy de AFIP (App/Db/AuthServer) + resumen de cache.
json · data
{ "api": { "estado": "OK", "fecha": "…" },
  "afip": { "AppServer": "OK", "DbServer": "OK", "AuthServer": "OK" },
  "cache": { "cuitsRegistrados": 2, "tokensActivos": 2 } }

Endpoint · cache

GET/api/afip/cache
Lista la cache de tokens/certificados por CUIT y ambiente. Útil para diagnóstico.

Endpoint · ultimo

POST/api/afip/ultimo
Último número autorizado para un punto de venta + tipo. En VFP: loFact.GetUltimoComprobante(pv, tipo).
json · request
{ "cuit": 20238233195, "puntoVenta": 1,
  "tipoComprobante": 6, "produccion": false }
json · data
{ "puntoVenta": 1, "tipoComprobante": 6, "ultimoNumero": 152 }

Endpoint · consultar nuevo

POST/api/afip/consultar
Recupera un comprobante puntual ya emitido (datos + CAE). Si no existe, AFIP devuelve error (ej. código 602) y la respuesta viene con ok:false.
json · request
{ "cuit": 20238233195, "produccion": false,
  "puntoVenta": 1, "tipoComprobante": 6, "numero": 152 }
json · data
{ "puntoVenta": 1, "tipoComprobante": 6, "numeroComprobante": 152,
  "documentoTipo": "80", "documentoNumero": "20111111112",
  "fechaComprobante": "20260318", "importeTotal": 121,
  "moneda": "PES", "cotizacion": 1,
  "resultado": "A", "CAE": "743…",
  "fechaVencimientoCAE": "20260328" }
🧩
Agregar a AfipApi.prg (sigue el patrón de Ultimo()):
foxpro
FUNCTION Consultar(tnPuntoVenta, tnTipoComprobante, tnNumero)
    LOCAL lcJson
    lcJson = "{" + ;
        '"cuit": '            + THIS.JsonInt(THIS.Cuit) + "," + ;
        '"produccion": '      + THIS.JsonBool(THIS.Produccion) + "," + ;
        '"puntoVenta": '      + THIS.JsonInt(tnPuntoVenta) + "," + ;
        '"tipoComprobante": ' + THIS.JsonInt(tnTipoComprobante) + "," + ;
        '"numero": '          + THIS.JsonInt(tnNumero) + ;
        "}"
    RETURN THIS.ApiPost("/api/afip/consultar", lcJson)
ENDFUNC

Endpoint · emitir

POST/api/afip/emitir
Emite un comprobante (WSFE) y devuelve CAE, vencimiento y número. En VFP: loFact.EmitirFactura(...).
json · request
{ "cuit": 20238233195, "produccion": false,
  "puntoVenta": 1, "tipoComprobante": 6, "concepto": 1,
  "documentoTipo": 80, "documentoNumero": 20111111112,
  "importeTotal": 121, "importeNoGravado": 0,
  "importeExento": 0, "importeGravado": 100, "importeIva": 21,
  "moneda": "PES", "cotizacion": 1,
  "condicionIvaReceptorId": 1, "alicuotaIvaId": 5
  // concepto 2/3: fechaServicioDesde/Hasta, fechaVencimientoPago (yyyyMMdd)
  // NC/ND: cbteAsoc { tipo, puntoVenta, numero, cuit?, cbteFch? } }
json · data
{ "resultado": "A", "CAE": "74328954736291",
  "fechaVencimientoCAE": "20260321", "numeroComprobante": 153 }
⚠️
Hoy /emitir admite una sola alícuota de IVA y no envía tributos. Para concepto 2 o 3 (servicios) son obligatorias las tres fechas en formato yyyyMMdd.

Endpoint · resumen-emitidos nuevo

POST/api/afip/resumen-emitidos
Arma un resumen de comprobantes propios emitidos en un rango, iterando internamente la consulta por número. Devuelve la lista + totalizador.
json · request
{ "cuit": 20238233195, "produccion": false,
  "puntoVenta": 1, "tipoComprobante": 6,
  "desde": 1, "hasta": 50   // opcionales: sin hasta usa el último; sin desde arranca en 1 }
json · data
{ "puntoVenta": 1, "tipoComprobante": 6, "desde": 1, "hasta": 50,
  "cantidad": 50, "autorizados": 48, "conError": 2,
  "importeTotal": 152340.50,
  "comprobantes": [ /* cada uno como /consultar; los fallidos: {numeroComprobante, error} */ ] }
⚠️
Tope: 1000 comprobantes por consulta (cada número es una llamada SOAP). Cubre solo emitidos por webservice — ver Limitaciones. Por el tamaño del array, parsealo recorriendo el Body, no con JsonGetValue.

Endpoints · Tablas AFIP

Todas toman el mismo request { cuit, produccion } y devuelven una lista. Las marcadas nuevo aún no tienen método en las clases VFP originales (se agregan con el mismo patrón que TiposIva()).

EndpointDevuelve
POST /puntos-ventaPuntos de venta habilitados
POST /tipos-comprobanteTipos de comprobante
POST /tipos-documentoTipos de documento
POST /tipos-conceptoConceptos (prod/serv/ambos)
POST /tipos-ivaAlícuotas de IVA
POST /tipos-monedaMonedas habilitadasnuevo
POST /tipos-tributoTipos de tributonuevo
POST /tipos-opcionalCampos opcionalesnuevo
POST /condiciones-iva-receptorCondiciones IVA receptor (RG 5616)nuevo
POST /actividadesActividades del emisor (Id, Orden, Desc)nuevo
json · data (tablas simples)
[ { "Id": "5", "Descripcion": "21%" },
  { "Id": "4", "Descripcion": "10.5%" } ]
🧩
Método VFP genérico para las tablas nuevas (cambiá solo la ruta):
foxpro
FUNCTION TablaSimple(tcPath)
    LOCAL lcJson
    lcJson = "{" + ;
        '"cuit": '       + THIS.JsonInt(THIS.Cuit) + "," + ;
        '"produccion": ' + THIS.JsonBool(THIS.Produccion) + ;
        "}"
    RETURN THIS.ApiPost(tcPath, lcJson)
ENDFUNC

* Uso:
loResp = loApi.TablaSimple("/api/afip/tipos-moneda")
loResp = loApi.TablaSimple("/api/afip/condiciones-iva-receptor")

Endpoint · cotizacion nuevo

POST/api/afip/cotizacion
Cotización oficial de una moneda. Útil para facturar en moneda extranjera con el valor que espera AFIP.
json · request
{ "cuit": 20238233195, "moneda": "DOL", "produccion": false }
json · data
{ "moneda": "DOL", "cotizacion": 1045.50, "fechaCotizacion": "20260318" }

Tablas de códigos

Constantes de AfipHelpers. Las tablas oficiales se pueden traer dinámicamente con los endpoints de tablas.

Tipos de comprobante

HelperIdDescripción
CbteFacturaA1Factura A
CbteFacturaB6Factura B
CbteFacturaC11Factura C
CbteNotaDebitoA / B / C2 / 7 / 12Nota de Débito
CbteNotaCreditoA / B / C3 / 8 / 13Nota de Crédito

Tipos de documento

HelperIdDescripción
DocCUIT80CUIT
DocCUIL86CUIL
DocDNI96DNI
DocConsumidorFinal99Consumidor Final

Concepto

1 · Productos2 · Servicios3 · Productos y Servicios
📅
Concepto 2 o 3 → obligatorio informar fechaServicioDesde, fechaServicioHasta y fechaVencimientoPago.

Condición IVA del receptor (RG 5616)

IdDescripción
1Responsable Inscripto
4Sujeto Exento
5Consumidor Final
6Monotributo
13Monotributo Social
15No Alcanzado

Alícuotas de IVA

HelperId%
AlicIva030%
AlicIva105410.5%
AlicIva21521%
AlicIva27627%
AlicIva585%
AlicIva2592.5%

Monedas

PES · PesosDOL · Dólares060 · Euros

Flujo y buenas prácticas

Flujo recomendado

  1. Inicializar() al abrir el módulo o cambiar de empresa (una vez).
  2. EstaDisponible() en diagnóstico o antes de la primera factura del día.
  3. GetUltimoComprobante() para la numeración real (no calcular local).
  4. EmitirFactura() para autorizar.
  5. consultar / resumen-emitidos para verificar o reconciliar.
No pidas login en cada factura: el token queda cacheado del lado de la API y se renueva solo. Reusá la instancia mientras dure la sesión de la empresa.
🔒
Errores: ante ok:false mostrá un mensaje controlado al operador y logueá fecha, CUIT, PV, tipo, request, Body, Status y ErrorMessage. Es clave para soporte.

Limitaciones a tener en cuenta

🧾
Emitidos por web vs. webservice. /consultar y /resumen-emitidos solo ven comprobantes emitidos por webservice (puntos de venta tipo RECE). Lo emitido desde "Comprobantes en Línea" (la web de ARCA) usa puntos de venta distintos y no aparece por estos endpoints.
📥
Comprobantes recibidos. No existe webservice oficial para bajar los comprobantes que te emitieron tus proveedores. La única fuente que los consolida es "Mis Comprobantes" (web, sin API).
🔤
Parseo JSON. El helper JsonGetValue sirve para valores simples. Para respuestas con arrays (tablas, resumen-emitidos), usá un parser JSON real o recorré el Body.