Appearance
Ingresos
CRUD sobre los ingresos (ventas) de tu empresa. Cada ingreso pertenece a una cuenta y opcionalmente a un cliente, una categoría, un proyecto y un empleado/vendedor.
Efectos colaterales
Crear un Income por API dispara los mismos efectos que crearlo desde la app:
- Suma
amount_cents(opaid_amount_centssi lo envías) al balance de la cuenta — crea unDepositinterno. - Si los
itemsreferencian productos, incrementa los contadores de venta de cada producto. - Si los productos tienen inventario activado, reserva los materiales/unidades correspondientes.
- Dispara automatizaciones con trigger
sale_createdque estén activas.
Period locking: si tu contabilidad cerró el periodo (mes) al que pertenece occurred_at, recibirás 422 period_locked. La API key no puede saltarse cierres — usa la app web si necesitas re-abrir.
El objeto Income
json
{
"id": "65c2a1f1234567890abcdef0",
"occurred_at": "2026-05-20T14:23:11Z",
"description": "Servicio web cliente Laura",
"amount_cents": 1500000,
"amount": 15000.0,
"paid_amount_cents": 1500000,
"paid_amount": 15000.0,
"currency": "mxn",
"paid": true,
"refunded": false,
"from_account_receivable": false,
"account_id": "65b3a1f1234567890abcdef0",
"account_name": "BBVA MXN",
"customer_id": "65a1b2c3d4e5f6789012abcd",
"customer_name": "Laura Méndez",
"category_id": null,
"category_name": "Servicios",
"project_id": null,
"project_name": null,
"employee_id": null,
"employee_name": null,
"sale_id": "65c2a1f1234567890abcdef0",
"recurring_transaction_id": null,
"metadata": { "external_ref": "cobro_2026_09", "orderId": "ord_123" },
"created_at": "2026-05-20T14:23:12Z",
"updated_at": "2026-05-20T14:23:12Z"
}Sobre GET /incomes/:id el response también incluye items (line items embedded).
Campos
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ObjectId. |
occurred_at | string | Fecha del ingreso (ISO 8601). |
description | string | Texto libre. |
amount_cents / amount | int / float | Total en centavos / decimal. |
paid_amount_cents | int | Monto efectivamente cobrado (puede ser menor a amount_cents para crédito parcial). |
currency | string | ISO 4217 lowercase. |
paid | bool | true si está cobrado al 100% — false genera una cuenta por cobrar. |
refunded | bool | Marca si fue reembolsado. |
from_account_receivable | bool | true cuando el ingreso se creó al cerrar un AR (no lo envíes manualmente). |
account_id / account_name | string | Cuenta destino. |
customer_id / customer_name | string | Cliente (opcional). |
category_id / category_name | string | Categoría (opcional). |
project_id / project_name | string | Proyecto (opcional). |
employee_id / employee_name | string | Vendedor / responsable (opcional). |
sale_id | string | La venta a la que pertenece este cobro, si la hay. |
recurring_transaction_id | string | La recurrencia que lo generó, si la hay. |
metadata | object | Tu bolsa libre. Ver abajo. |
metadata
Un objeto plano donde guardas tus identificadores. Vuelve tal cual en cada lectura y, lo importante, dentro de cada webhook sobre este ingreso.
Es lo que convierte un aviso de «se pagó el ingreso 65c2a1f...» —un id de Finova que no sabes a qué corresponde— en uno de «se pagó tu orden ord_123».
json
{
"data": {
"account_id": "65b3a1f1234567890abcdef0",
"amount_cents": 150000,
"currency": "mxn",
"occurred_at": "2026-09-17T12:00:00Z",
"metadata": {
"external_ref": "cobro_2026_09",
"orderId": "ord_123",
"clientId": "portal-clientes"
}
}
}Límites
| Claves | hasta 50 |
| Longitud de clave | 40 bytes |
| Longitud de valor | 500 bytes |
| Anidamiento | ninguno — sólo valores simples, ni objetos ni listas |
Un valor null borra la clave. Fuera de esos límites, 422 invalid_metadata.
Son los mismos límites que Stripe, a propósito: si ya integraste con ellos, no tienes que aprender otra regla.
external_ref: idempotencia del alta
metadata.external_ref es especial. Lleva un índice único por empresa, así que si repites el POST con la misma referencia, Finova devuelve 200 con el ingreso que ya existía en lugar de crear un segundo.
Sirve para el caso que la cabecera Idempotency-Key no cubre: aquel en que se pierde la respuesta y reintentas horas después, o desde otro proceso. Reintentar a ciegas sin esto duplica un movimiento contable —dinero contado dos veces— y desde la contabilidad no hay forma de distinguir el duplicado del original.
Ver idempotencia.
Listar ingresos
GET
/api/v1/incomesread-incomesPaginado estándar. Ver guía de paginación.
Acepta search (alias q): búsqueda full-text/autocomplete sobre description, category_name, customer_name, employee_name y account_name. Devuelve hasta los 100 ingresos más relevantes, luego paginados.
Obtener un ingreso
GET
/api/v1/incomes/:idread-incomesDevuelve el ingreso completo incluyendo items (line items embedded).
Crear un ingreso
POST
/api/v1/incomescreate-incomesCampos aceptados en data: account_id (requerido), customer_id, category_id, amount_cents (requerido), paid_amount_cents, currency, occurred_at, description, paid, project_id, employee_id, items, metadata.
¿Vas a facturar esto?
Entonces registra una venta, no un ingreso. Un CFDI se emite sobre la venta: doce cobros de una suscripción anual son una sola factura, y desde un ingreso suelto no se pueden agrupar.
items es un array de line items embedded. Cada item:
| Campo | Tipo | Descripción |
|---|---|---|
product_id | string | Requerido. El producto del catálogo. Ver la nota de abajo. |
quantity | int | Cantidad. Por defecto 1. |
unit_price_cents | int | Precio unitario en centavos. Si lo omites, se toma el del producto. |
name | string | Etiqueta del concepto. Alias de description. |
variant_id | string | Para productos variables. |
option_value_id | string | Valor de opción elegido. |
tax_percent | float | Tasa de IVA del concepto. Alias de tax_rate_percent. |
price_includes_tax | bool | Si unit_price_cents ya trae el IVA. |
tax_exempt / tax_zero_rated | bool | Exento o tasa 0% ante el SAT. No es lo mismo que "sin IVA". |
sat_key / sat_unit_key / sat_unit_name | string | Claves del SAT. Si las omites, se heredan del producto. |
components | array | Para productos compuestos: { component_id, quantity }. |
product_id es obligatorio en cada concepto
En Finova un concepto apunta siempre a algo del catálogo: es lo que permite descontar inventario, mover los contadores de unidades vendidas y heredar las claves del SAT al facturar. Un concepto suelto sin producto se rechaza con 422.
Si lo que vendes no está en el catálogo, créalo primero con POST /products — un producto de tipo service sirve para conceptos que no llevan inventario.
discount_cents ya no se documenta
Nunca llegó a guardarse: no existe descuento por concepto en el modelo. Si lo mandas, se ignora en silencio. Para aplicar un descuento, baja unit_price_cents.
Al leer (GET /incomes/:id), cada item incluye además su id y unit_price (decimal, espejo de unit_price_cents).
bash
curl -X POST https://developers.fi-nova.com/api/v1/incomes \
-H 'Authorization: Bearer finova_sk_TU_SECRETO' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"account_id": "65b3a1f1234567890abcdef0",
"customer_id": "65a1b2c3d4e5f6789012abcd",
"amount_cents": 1500000,
"currency": "mxn",
"occurred_at": "2026-05-20T14:23:11Z",
"description": "Servicio web cliente Laura",
"paid": true,
"items": [
{
"product_id": "65b0a1f1234567890abcdef0",
"name": "Diseño",
"quantity": 1,
"unit_price_cents": 1500000
}
]
}
}'Actualizar un ingreso
PUT
/api/v1/incomes/:idupdate-incomesMismos campos que create. Si cambias occurred_at a otro periodo, ambos periodos deben estar abiertos — si alguno está bloqueado, 422 period_locked.
Eliminar un ingreso
DELETE
/api/v1/incomes/:iddelete-incomesEliminar revierte los efectos colaterales (depósito, balance de cuenta, contadores de producto, reserva de inventario). Si el ingreso está en un periodo cerrado, recibes 422 period_locked.