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.
http://localhost:5014 o http://localhost:5000. Todos los endpoints cuelgan de /api/afip.ok del JSON.Arquitectura recomendada en VFP
Tres capas, para no mezclar HTTP con lógica fiscal en los formularios.
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.
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
ENDIFFacturadorAfip 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
{ "ok": true, "data": { /* … */ } }Error
{ "ok": false, "data": {
"error": "mensaje claro",
"detail": "detalle técnico",
"status": 500
} }Manejo en VFP
IF loResp.Ok
* usar loResp.Body / campos parseados
ELSE
* mostrar mensaje y loguear: fecha, CUIT, PV, tipo,
* request, loResp.Body, loResp.Status, loResp.ErrorMessage
ENDIFok.Clase 1 · AfipApi.prg
Cliente técnico HTTP/JSON. Usa MSXML2.ServerXMLHTTP.6.0 y helpers de parseo por texto.
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)
ENDDEFINECAE, 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…).
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
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
ENDFUNCClase 3 · AfipHelpers.prg
Catálogo de constantes para no usar números mágicos. Ver todos los valores en Tablas de códigos.
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
ENDIFEndpoint · login
loFact.Inicializar().{ "cuit": 20238233195, "servicio": "wsfe",
"certificadoPath": "C:\\Certificados\\cert.pfx",
"certificadoPassword": "clave", "produccion": false }{ "token": "abc123", "sign": "xyz456",
"expiration": "2026-03-20T12:00:00" }Endpoint · status
loFact.EstaDisponible().{ "cuit": 20238233195, "tokenActivo": true,
"certificadoRegistrado": true,
"expira": "2026-03-20T12:00:00", "minutosRestantes": 120 }Endpoint · health
{ "api": { "estado": "OK", "fecha": "…" },
"afip": { "AppServer": "OK", "DbServer": "OK", "AuthServer": "OK" },
"cache": { "cuitsRegistrados": 2, "tokensActivos": 2 } }Endpoint · cache
Endpoint · ultimo
loFact.GetUltimoComprobante(pv, tipo).{ "cuit": 20238233195, "puntoVenta": 1,
"tipoComprobante": 6, "produccion": false }{ "puntoVenta": 1, "tipoComprobante": 6, "ultimoNumero": 152 }Endpoint · consultar nuevo
ok:false.{ "cuit": 20238233195, "produccion": false,
"puntoVenta": 1, "tipoComprobante": 6, "numero": 152 }{ "puntoVenta": 1, "tipoComprobante": 6, "numeroComprobante": 152,
"documentoTipo": "80", "documentoNumero": "20111111112",
"fechaComprobante": "20260318", "importeTotal": 121,
"moneda": "PES", "cotizacion": 1,
"resultado": "A", "CAE": "743…",
"fechaVencimientoCAE": "20260328" }AfipApi.prg (sigue el patrón de Ultimo()):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)
ENDFUNCEndpoint · emitir
loFact.EmitirFactura(...).{ "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? } }{ "resultado": "A", "CAE": "74328954736291",
"fechaVencimientoCAE": "20260321", "numeroComprobante": 153 }/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
{ "cuit": 20238233195, "produccion": false,
"puntoVenta": 1, "tipoComprobante": 6,
"desde": 1, "hasta": 50 // opcionales: sin hasta usa el último; sin desde arranca en 1 }{ "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} */ ] }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()).
| Endpoint | Devuelve | |
|---|---|---|
| POST /puntos-venta | Puntos de venta habilitados | |
| POST /tipos-comprobante | Tipos de comprobante | |
| POST /tipos-documento | Tipos de documento | |
| POST /tipos-concepto | Conceptos (prod/serv/ambos) | |
| POST /tipos-iva | Alícuotas de IVA | |
| POST /tipos-moneda | Monedas habilitadas | nuevo |
| POST /tipos-tributo | Tipos de tributo | nuevo |
| POST /tipos-opcional | Campos opcionales | nuevo |
| POST /condiciones-iva-receptor | Condiciones IVA receptor (RG 5616) | nuevo |
| POST /actividades | Actividades del emisor (Id, Orden, Desc) | nuevo |
[ { "Id": "5", "Descripcion": "21%" },
{ "Id": "4", "Descripcion": "10.5%" } ]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
{ "cuit": 20238233195, "moneda": "DOL", "produccion": false }{ "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
| Helper | Id | Descripción |
|---|---|---|
| CbteFacturaA | 1 | Factura A |
| CbteFacturaB | 6 | Factura B |
| CbteFacturaC | 11 | Factura C |
| CbteNotaDebitoA / B / C | 2 / 7 / 12 | Nota de Débito |
| CbteNotaCreditoA / B / C | 3 / 8 / 13 | Nota de Crédito |
Tipos de documento
| Helper | Id | Descripción |
|---|---|---|
| DocCUIT | 80 | CUIT |
| DocCUIL | 86 | CUIL |
| DocDNI | 96 | DNI |
| DocConsumidorFinal | 99 | Consumidor Final |
Concepto
fechaServicioDesde, fechaServicioHasta y fechaVencimientoPago.Condición IVA del receptor (RG 5616)
| Id | Descripción |
|---|---|
| 1 | Responsable Inscripto |
| 4 | Sujeto Exento |
| 5 | Consumidor Final |
| 6 | Monotributo |
| 13 | Monotributo Social |
| 15 | No Alcanzado |
Alícuotas de IVA
| Helper | Id | % |
|---|---|---|
| AlicIva0 | 3 | 0% |
| AlicIva105 | 4 | 10.5% |
| AlicIva21 | 5 | 21% |
| AlicIva27 | 6 | 27% |
| AlicIva5 | 8 | 5% |
| AlicIva25 | 9 | 2.5% |
Monedas
Flujo y buenas prácticas
Flujo recomendado
- Inicializar() al abrir el módulo o cambiar de empresa (una vez).
- EstaDisponible() en diagnóstico o antes de la primera factura del día.
- GetUltimoComprobante() para la numeración real (no calcular local).
- EmitirFactura() para autorizar.
- consultar / resumen-emitidos para verificar o reconciliar.
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
/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.JsonGetValue sirve para valores simples. Para respuestas con arrays (tablas, resumen-emitidos), usá un parser JSON real o recorré el Body.