Appearance
Notas de versión
Historial de cambios de la API pública de Finova for Developers. Cada release deja también su huella en las respuestas: todos los endpoints regresan los headers X-Finova-API-Version y X-Finova-API-Released-At, así sabes exactamente contra qué versión está corriendo la API cuando depuras una integración.
Si te interesa saber qué cambios pueden ocurrir sin avisar y cuáles requieren una versión nueva, lee la guía de Versionado.
1.0.06 — 2026-09-17
Webhooks salientes, ventas, facturas (CFDI), recurrentes y metadata. Cambios aditivos — no afecta integraciones existentes.
Webhooks
Hasta ahora, la única forma de enterarte de algo en Finova era preguntar: un GET /incomes cada pocos minutos que casi siempre devolvía lo mismo, y que de todas formas llegaba tarde. Ya no.
- Nuevo recurso
/webhook-endpoints: registras una URL, eliges a qué eventos se suscribe y Finova te avisa. Scopesread-/create-/update-/delete-webhook_endpoints. - Firma HMAC-SHA256 con marca de tiempo, formato Stripe:
X-Finova-Signature: t=<segundos>,v1=<hex>sobre"{t}.{cuerpo crudo}". La marca va dentro de lo firmado, así que una petición capturada no se puede reenviar mañana. - Id de evento estable en cada aviso, el mismo en todos los intentos: es lo que te permite descartar un reenvío.
- Seis reintentos con espera creciente (inmediato, 1 min, 5 min, 30 min, 2 h, 6 h). Tras 20 fallos seguidos el endpoint se desactiva solo.
- Comodín de suscripción:
invoice.*cubre los cuatro de factura,*cubre todos. Solo al final y sobre un segmento entero. - Historial de entregas:
GET /webhook-endpoints/:id/deliveriescontesta «no me llegó el aviso» — si el evento se emitió, qué contestó tu servidor y cuándo toca el siguiente intento. Se guardan 30 días.
16 eventos: sale.created, sale.paid, sale.refunded, income.created, income.paid, income.deleted, recurring.created, recurring.occurrence_created, recurring.ended, recurring.deactivated, invoice.emitted, invoice.cancelled, invoice.substituted, invoice.payment_complement_added, customer.created, lead.created.
Ver la guía de webhooks.
metadata
Ingresos, ventas y recurrentes aceptan ahora un objeto
metadata: una bolsa libre de primer nivel donde guardas tus identificadores. Hasta 50 claves, sin anidamiento — los mismos límites que Stripe.Vuelve tal cual en cada lectura y, lo importante, dentro de cada webhook sobre ese recurso. Es lo que convierte un aviso sobre
65c2a1f...—un id de Finova que no sabes a qué corresponde— en uno sobre tu propia ordenord_123.metadata.external_refhace el alta idempotente para siempre. Lleva índice único por empresa: si repites elPOSTcon la misma referencia, recibes200con el recurso que ya existía en vez de un segundo.La cabecera
Idempotency-Keycubre el reintento inmediato (24 h de caché). Esto cubre el otro caso, el que duele: elPOSTllegó, la respuesta se perdió, y tres horas después un barrido de reconciliación lo vuelve a registrar. Sinexternal_refeso duplica un movimiento contable —dinero contado dos veces— y desde la contabilidad no hay forma de distinguir la copia del original.
Ver idempotencia.
Ventas
- Nuevo recurso
/sales(read-sales,create-sales), con su esquema de cobro: contado, meses, anticipo + saldo, o esquema propio. - Es sobre lo que se factura. Una factura no se corresponde uno a uno con un cobro: una orden pagada en tres parcialidades se factura una vez, y sin agrupar, doce meses de una suscripción serían doce facturas donde el cliente espera una.
- Registrar una venta dispara los mismos efectos que hacerlo desde la aplicación: ingreso o cuenta por cobrar, depósito, inventario, contadores de unidades vendidas y automatizaciones de
sale_created. - Sin
DELETE: deshacer una venta no es borrarla, es una devolución, que deja rastro contable.
Facturas (CFDI 4.0)
- Nuevo recurso
/sales/:id/invoicey el libro de facturas en/invoices. Requiere el módulo Facturación. - Emitir, cancelar, sustituir, timbrar complementos de pago y descargar el PDF y el XML.
- El timbrado responde 202: llama al proveedor ante el SAT, que tarda segundos y a veces más. Espera el webhook
invoice.emitteden lugar de sondear. - Cancelar pide
delete-invoices, nocreate-invoices: es irreversible ante el SAT y no debería caber dentro de la misma llave que emite.
Transacciones recurrentes
- Nuevo recurso
/recurring-transactions(read-,create-,update-), con el calendario completo: frecuencia, intervalo, días, inicio, fin y próxima corrida. - El
indexno es un extra. Al migrar una cartera es muy probable que varias recurrencias ya estén dadas de alta en Finova, capturadas a mano; crear una segunda sobre el mismo cliente genera el mismo mes dos veces. Buscar antes de crear es la diferencia entre migrar de una vez y migrar rezando. - No escribas tú el periodo de cada mes. El generador de Finova corre a diario, se pone al día si falta un día y su idempotencia es un índice único en la base. Suscríbete a
recurring.occurrence_createdy anótalo. - Sin
DELETE: para cancelar una suscripción se mandaactive: false, que deja el histórico en pie y emiterecurring.deactivated.
Ingresos
- Ahora exponen
metadata,sale_idyrecurring_transaction_id.
1.0.05 — 2026-07-15
Filtro has_products y conteo de productos por categoría (con subárbol). Cambios aditivos — no afecta integraciones existentes.
Categorías de producto
- Ahora exponen dos conteos:
product_count: productos asignados directamente a la categoría (no cuenta los de sus hijas).product_count_including_descendants: productos del subárbol — los directos más los de todas sus categorías descendientes. Para una categoría hoja coincide conproduct_count; para un padre como "Lentes" incluye lo que cuelga de "De sol", "Graduados", etc.
- Ambos conteos cuentan solo productos activos (
is_active: true) — misma visibilidad que el default deGET /products, no incluyen borradores/desactivados. - Nuevo filtro
?has_products=trueenGET /product-categories: devuelve solo las categorías con al menos un producto activo en su subárbol. Así una categoría padre sin productos directos pero con productos en sus hijas sí aparece.
Marcas
- Nuevo filtro
?has_products=trueenGET /brands: devuelve solo las marcas con al menos un producto activo asignado (product_count > 0).
General
- El
product_countde marcas y categorías cuenta solo productos activos y ahora es un valor cacheado: listar ya no dispara una consulta de conteo por página.
1.0.04 — 2026-06-22
Filtros de productos (marca/categoría), totales filtrados y conteo de productos por marca. Cambios aditivos — no afecta integraciones existentes.
Productos
- Filtro por marca y categoría en
GET /products:brand(aliasbrand_id) ycategory(aliasproduct_category_id).- Cada uno acepta un solo valor o una lista separada por comas que mezcla ObjectIds y slugs, ej.
?category=comida,bebidaso?brand=acme,5f.... - Trae los productos que pertenezcan a cualquiera de los valores listados. Un id/slug desconocido no aporta resultados — el filtro nunca se ignora silenciosamente.
?include_filtered_total=true: agregafiltered_totalyfiltered_total_pagesametacon el conteo del resultado ya filtrado (porq/search,brand,category). Útil para "Mostrando X de Y" cuando filtras.- Distinto de
include_total, que cuenta todo el catálogo e ignora los filtros. - El conteo está acotado a 10000 (si lo supera, se satura ahí y
meta.filtered_total_cappedviene entrue); conq/searchactivo el tope es 100.
- Distinto de
Marcas
- Las marcas ahora exponen
product_count: número de productos no eliminados que apuntan a esa marca en tu empresa. EnGET /brandsse calcula con una sola agregación por página (no una consulta por marca).
1.0.03 — 2026-06-09
Formularios (Forms) multi-idioma. Cambios aditivos — no afecta integraciones existentes.
Formularios
- Nuevo recurso Formularios: lee la definición de un formulario de Marketing para renderizarlo en tu sitio y envía las respuestas de vuelta.
GET /formsyGET /forms/:id(scoperead-forms) —:idacepta ObjectId oslug.GET /forms/:form_id/submissionsyGET /forms/:form_id/submissions/:id(scoperead-forms).POST /forms/:form_id/submissions(scopecreate-form_submissions) — registra una respuesta; valida contra los campos del formulario y dispara las post-acciones (lead/cliente, email, notificación) en background.
- Multi-idioma: el formulario declara
locales+default_locale. Pasa?locale=en losGETpara recibirtitle/descriptionylabel/placeholder/optionsde cada campo ya resueltos en ese idioma; además siempre vienen los mapas*_translationscon todos los idiomas. EnPOSTde respuestas,data.locale(o?locale=) guarda el idioma usado y determina, entre otras cosas, qué plantilla de email se envía.
1.0.02 — 2026-06-07
Productos multi-moneda. ⚠️ Cambio incompatible (breaking change). Un producto ya no está atado a una sola moneda: ahora tiene un precio por divisa.
Productos
- Eliminados los campos escalares
currencyyprice_centsdel objetoproduct, de cadavariant, yextra_price_centsde cadacomponent. - Nuevos campos de precio como mapa multi-moneda
{ <iso4217_lowercase>: <centavos> }:product.prices_cents— ej.{ "mxn": 450000, "usd": 25000 }.variant.prices_cents— vacío cuando la variante hereda el precio del producto.component.extra_prices_cents— ajuste de precio del componente por divisa.
- En la escritura (
POST/PATCH) se aceptanprices_cents/extra_prices_cents(objeto divisa→centavos) y ya no se aceptan loscurrency/price_cents/extra_price_centsescalares. - Cada producto debe tener al menos una divisa con precio.
extra_price_centsde los valores de opción (option_types[].values[]) se mantiene escalar (sin cambio).
Migración: si tu integración leía
product.price_cents/product.currency, cámbiala aproduct.prices_cents[<divisa>]. Para escribir, mandaprices_cents: { "mxn": 450000 }en lugar deprice_cents+currency.
1.0.01 — 2026-06-06
Imágenes de producto, campos personalizados, categorías de producto y búsqueda por slug/sku. Cambios aditivos — tus integraciones existentes siguen funcionando sin cambios.
Productos
- Nuevos campos en el producto:
image_url,thumbnail_urly una galeríaimages([{ url, alt, position }]). - El detalle (
GET /products/:id) ahora incluyevariants,option_types,componentseinventory— disponibilidad total y desglose por almacén/rack (solo cantidades, nunca costos). - Nuevos campos personalizados en productos y marcas: especificaciones/atributos estructurados definidos por el usuario (texto, número, booleano, listas, objetos, listas de objetos y archivos) para la tabla de specs de la ficha. En la lectura se devuelven aplanados como claves de primer nivel del recurso (ej.
"voltaje": 110), no como un arraycustom_properties; en la escritura se pasan como el arraycustom_propertiesdentro dedata. Las claves que coincidan con un campo reservado del recurso se descartan. Ver Campos personalizados. - Nuevos endpoints para gestionar imágenes (multipart): subir/eliminar la imagen principal del producto y la imagen de cada variante.
- El CRUD acepta
variants,option_typesycomponentsanidados (con semántica de crear/actualizar/eliminar porid+_destroy). - El objeto
productahora incluyeproduct_category({ id, name, slug }), la referencia estructurada a la categoría de catálogo. El campocategory_namese mantiene (deprecado pero presente). La categoría de catálogo (en productos) es distinta de la categoría financiera (en ingresos/egresos), aunque compartan el nombrecategory_name.
Categorías de producto
- Nuevo recurso
/product-categoriescon CRUD completo: la taxonomía de catálogo con la que organizas tus productos (jerarquía padre/hijas,icon, imagen yproduct_count). - Cada categoría expone
product_count(productos activos que apuntan a ella),icon(Material Symbol) eimage_url. - Nuevo scope por credencial:
read-product_categories(ycreate/update/delete).
Marcas
- Nuevo
logo_url(y el media itemlogo) en el objetobrand, editable desde la app o vía API (logo/logo_url). Útil para chips de marca y la cabecera de la página de marca.
Búsqueda por identificador
- Los endpoints de detalle (
GET /{recurso}/:id) aceptan ahoraslugoskuademás del id, resueltos dentro de tu empresa. Aplica a los recursos que tienen esos campos: productos (slugysku), marcas, categorías de producto y ligas de agenda (slug). Ver Identificar un recurso.
1.0.0 — 2026-05-18
Lanzamiento inicial de Finova for Developers.
Primera versión pública estable. La API y la forma de cada response es ahora un contrato — los cambios que rompan compatibilidad pasarán por una versión nueva (v2) con aviso por correo y al menos 12 meses corriendo en paralelo.
Lo que incluye
- Endpoints disponibles: Productos, Clientes, Leads, Automatizaciones, Eventos del calendario, Ligas de agenda, Reservas, Usuarios y Empresa.
- Autenticación con credenciales (
finova_sk_...) y permisos a la medida por credencial. Lista blanca opcional de dominios desde donde se permite usar cada credencial. - Paginación tipo cursor (
starting_after,ending_before) con tope automático para evitar páginas muy profundas. Detalles en la guía de Paginación. - Cache de respuestas opt-in (por cuenta) para endpoints de lectura, con invalidación automática cuando hay un cambio. Más en la guía de Cache.
- Idempotencia: las llamadas que crean datos aceptan
Idempotency-Keypara que un reintento no duplique registros. Ver guía de Idempotencia. - Cobro por consulta real: cada llamada se factura por las consultas a la base de datos que ejecutó (incluyendo la validación de tu credencial, la verificación de que pertenezca a tu empresa y los permisos). Verás el conteo por llamada en el panel de uso, útil para optimizar tu integración.
- Headers de versión: todas las respuestas incluyen
X-Finova-API-VersionyX-Finova-API-Released-At. Si más adelante necesitas reportar un problema, incluir estos dos valores ayuda a saber contra qué versión sucedió.
Cómo enterarte de cambios
- Esta página se actualiza con cada release nuevo. Se ordena con la versión más reciente arriba.
- Para cambios que rompen compatibilidad (siempre en una versión nueva, nunca dentro de
v1), te llegará un correo al titular de la cuenta y verás un headerSunseten las respuestas de la versión que va a deprecarse. - Si exponemos algo en beta, lo verás con
X-Finova-Beta: <feature>en la respuesta y un badge (beta) en la documentación. Sin esos marcadores, lo que estás usando es estable.