Appearance
Ventas
Una venta (Sale) es la operación comercial completa: qué se vendió, por cuánto y cómo se cobra. Agrupa los ingresos (el dinero que entra) y las cuentas por cobrar de una misma operación.
Venta o ingreso: cuál usar
| Ingresos | Ventas | |
|---|---|---|
| Responde | «cuánto dinero entró y cuándo» | «qué se vendió» |
| Cobros | uno | uno o varios |
| Se puede facturar | no | sí |
Si vas a facturar, registra ventas
Una factura no se corresponde uno a uno con un cobro. Una orden pagada en tres parcialidades se factura una vez, y doce meses de una suscripción son doce cobros donde el cliente espera una factura. El CFDI se emite sobre la venta, no sobre el ingreso: sin agrupar, tendrías doce facturas.
Para un cobro suelto que no se va a facturar, POST /incomes sigue siendo más directo.
Registrar una venta dispara los mismos efectos que hacerlo desde la aplicación: crea el ingreso o la cuenta por cobrar, deposita en la cuenta, descuenta inventario, mueve los contadores de unidades vendidas y dispara las automatizaciones de sale_created.
El objeto Sale
json
{
"id": "65c2a1f1234567890abcdef0",
"object": "sale",
"status": "partially_paid",
"currency": "mxn",
"occurred_at": "2026-09-17T18:04:10Z",
"description": "Paquete anual de mantenimiento",
"customer_id": "65a1b2c3d4e5f6789012abcd",
"customer_name": "Laura Méndez",
"account_id": "65b3a1f1234567890abcdef0",
"account_name": "BBVA MXN",
"category_id": null,
"category_name": null,
"total_cents": 1160000,
"total": 11600.0,
"subtotal_cents": 1000000,
"tax_cents": 160000,
"tax_rate_percent": 16.0,
"tax_breakdown": [{ "rate_percent": 16.0, "base_cents": 1000000, "tax_cents": 160000 }],
"retentions_cents": 0,
"without_tax": false,
"payment_scheme": "PPD",
"paid_cents": 400000,
"receivable_cents": 760000,
"cobros_count": 1,
"installments_total": 3,
"installments_paid": 1,
"invoiced": false,
"invoiced_total_cents": 0,
"complemented_cents": 0,
"cfdi_uuid": null,
"income_ids": ["65c2a2001234567890abcdef"],
"accounts_receivable_ids": ["65c2a2011234567890abcdef"],
"recurring_transaction_id": null,
"recurring_occurrence_on": null,
"metadata": { "external_ref": "ord_123", "orderId": "ord_123" },
"created_at": "2026-09-17T18:04:11Z",
"updated_at": "2026-09-17T18:04:11Z"
}Estado
status | Qué significa |
|---|---|
open | No se ha cobrado nada. |
partially_paid | Entró parte del dinero. |
paid | Saldada. Dispara sale.paid. |
canceled | Cancelada. |
Cobranza
Se publican los dos números y no sólo el estado, porque paid_cents y receivable_cents son lo que necesitas para decirle a tu cliente «te faltan $X» — derivarlo del estado es imposible.
Siempre se cumple total_cents = paid_cents + receivable_cents.
Facturación
payment_scheme lo decide Finova: PUE si la venta se liquidó en una sola exhibición, PPD si hay crédito o varios cobros. Ante el SAT no es opcional, y determina si cada cobro necesita complemento de pago.
Ver Facturas.
Listar ventas
GET
/api/v1/salesread-salesFiltros: status, customer_id, recurring_transaction_id, external_ref.
Obtener una venta
GET
/api/v1/sales/:idread-salesIncluye items.
Registrar una venta
POST
/api/v1/salescreate-salesRequeridos: account_id, currency, total_amount_cents, occurred_at.
Opcionales: customer_id, category_id, employee_id, project_id, description, without_tax, items, payment_terms, metadata.
Los items usan los mismos campos que en ingresos, y product_id es obligatorio en cada uno.
A crédito, category_id es obligatorio
Una venta que no se cobra al momento genera una cuenta por cobrar, y una CxC sin categoría no se puede clasificar en el estado de resultados. Al contado (cash_paid: true) no hace falta.
payment_terms: cómo se cobra
Es lo que decide si la venta nace como dinero en la cuenta o como cuenta por cobrar. Si lo omites, se asume contado sin pagar ({ "type": "cash" }).
json
{ "type": "cash", "cash_paid": true }json
{ "type": "cash", "cash_paid": false, "due_on": "2026-10-17" }json
{
"type": "msi",
"installments_count": 3,
"first_due_on": "2026-10-01",
"paid_installments_count": 1
}json
{
"type": "deposit",
"deposit_mode": "percent",
"deposit_percent": 30,
"remainder_plan": "cash",
"due_on": "2026-11-17"
}json
{
"type": "custom",
"custom_payments": [
{ "amount_cents": 400000, "due_on": "2026-09-17", "paid": true },
{ "amount_cents": 380000, "due_on": "2026-10-17" },
{ "amount_cents": 380000, "due_on": "2026-11-17" }
]
}Con type: "custom" los pagos tienen que sumar exactamentetotal_amount_cents, o la petición se rechaza con 422.
Ejemplo completo
bash
curl -X POST https://developers.fi-nova.com/api/v1/sales \
-H 'Authorization: Bearer finova_sk_TU_SECRETO' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"account_id": "65b3a1f1234567890abcdef0",
"customer_id": "65a1b2c3d4e5f6789012abcd",
"currency": "mxn",
"total_amount_cents": 1160000,
"occurred_at": "2026-09-17T18:04:10Z",
"description": "Paquete anual de mantenimiento",
"items": [
{
"product_id": "65b0a1f1234567890abcdef0",
"name": "Mantenimiento anual",
"quantity": 1,
"unit_price_cents": 1160000,
"tax_percent": 16
}
],
"payment_terms": {
"type": "msi",
"installments_count": 3,
"first_due_on": "2026-10-01"
},
"metadata": { "external_ref": "ord_123", "orderId": "ord_123" }
}
}'Idempotencia
Manda metadata.external_ref. Si repites la petición con la misma referencia, recibes 200 con la venta que ya existía en vez de una segunda. En una operación que mueve dinero, reintentar a ciegas sin esto es contar el dinero dos veces sin forma de notarlo. Ver idempotencia.
Periodo cerrado
Si occurred_at cae en un mes que tu contabilidad ya cerró, recibes 422 period_locked. La llave no puede saltarse un cierre.
No hay DELETE
Deshacer una venta no es borrarla: es una devolución, que deja rastro contable y genera el egreso que la compensa. Borrar el agregado dejaría sus ingresos y cuentas por cobrar apuntando a un id que no resuelve.
Las devoluciones se registran desde la aplicación y emiten sale.refunded.