Registrar ventas y recibir pagos
La venta que entra por la API sigue el mismo camino de la pantalla de ventas: recibe el número de la empresa, cuenta en la cuota del plan, arma las cuotas, registra en finanzas y descuenta el stock de los productos del catálogo. Esta guía registra una venta en cuotas y después da de baja una cuota.
La clave
Sección titulada «La clave»| Permiso | Para qué |
|---|---|
sales:create | Registrar la venta |
sales:update | Cobrar, anular cobros, reprogramar, cancelar y crear una versión nueva |
sales:read | Leer la venta después |
finance:create | Solo cuando la llamada pide el registro en finanzas: track_in_finance: true o una cuenta en payment_plan.account_id |
finance:read | Para elegir la cuenta en payment_plan.account_id, y para ver la cuenta y los movimientos de la venta en la respuesta |
Registrar un cobro pide solo el permiso de ventas, aun con la venta en finanzas: el movimiento es consecuencia de la venta, como en la pantalla.
El acceso del miembro a finanzas también cuenta. Enviar el campo track_in_finance, con true o con false, y elegir la cuenta en payment_plan.account_id exigen que el miembro que la clave representa tenga acceso a finanzas en el panel; sin él, la respuesta es 403 member_permission_denied.
El plan de pago
Sección titulada «El plan de pago»Toda venta lleva un payment_plan, y siempre tiene los seis campos, aunque algunos sean null:
| Campo | Qué es |
|---|---|
mode | paid_full, entry_and_rest, unpaid o installments |
payment_method | pix, credit_card, debit_card, cash, boleto, transfer, other o null |
account_id | La cuenta que recibe, o null para la cuenta predeterminada de la empresa |
paid_at | Fecha del pago al contado o del anticipo (YYYY-MM-DD), o null |
entry_amount_cents | Monto del anticipo, en el modo entry_and_rest; null en los demás |
installments | Las cuotas, cada una con amount_cents y due_date; null en paid_full |
Los cuatro modos son los de la pantalla:
paid_full: pagado al contado. La venta nace pagada.entry_and_rest: un anticipo pagado ahora y el resto en cuotas. El anticipo tiene que ser mayor que cero y menor que el total, y las cuotas suman lo que falta.installments: en cuotas, sin nada pagado todavía. Las cuotas suman el total.unpaid: a cobrar, fiado. Exigeclient_id, y las cuotas suman el total.
La suma de las cuotas tiene que cerrar con el monto. Una diferencia de hasta 1 centavo se ajusta en la última cuota; más que eso responde 422. El total se calcula a partir de los ítems, el descuento, el flete y los costos adicionales, como lo calcula el formulario; usted no envía el total.
Registrar la venta y cobrar la primera cuota
Sección titulada «Registrar la venta y cobrar la primera cuota»# 1. Venta de R$ 1.500,00 en 3 cuotas con boleto# Genere la clave una vez y guárdela: en un reintento, repita con el mismo valor.SALE_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/sales" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $SALE_KEY" \ -d '{ "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "items": [ { "type": "custom", "name": "Instalación de 4 cámaras", "quantity": 1, "unit_price_cents": 150000 } ], "payment_plan": { "mode": "installments", "payment_method": "boleto", "account_id": null, "paid_at": null, "entry_amount_cents": null, "installments": [ { "amount_cents": 50000, "due_date": "2026-10-10" }, { "amount_cents": 50000, "due_date": "2026-11-10" }, { "amount_cents": 50000, "due_date": "2026-12-10" } ] } }'
# 2. Se pagó la primera cuota. Use el id de la venta y el id de la cuota# que vinieron en la respuesta de arriba.SALE_ID="3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"INSTALLMENT_ID="inst-1"# Genere la clave una vez y guárdela: en un reintento, repita con el mismo valor.PAYMENT_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/sales/$SALE_ID/payments" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $PAYMENT_KEY" \ -d "{ \"amount_cents\": 50000, \"paid_at\": \"2026-10-09\", \"payment_method\": \"pix\", \"installment_id\": \"$INSTALLMENT_ID\" }"import { randomUUID } from 'node:crypto';
const API = 'https://api.fatureihoje.com/public/v1';
async function post(path, body, idempotencyKey) { const res = await fetch(`${API}${path}`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FH_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`); return data;}
// Genere las claves junto con la operación y guárdelas: en un reintento, reutilícelas.const saleKey = randomUUID();const paymentKey = randomUUID();
// 1. Venta de R$ 1.500,00 en 3 cuotas con boleto.const sale = await post( '/sales', { client_id: '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', items: [{ type: 'custom', name: 'Instalación de 4 cámaras', quantity: 1, unit_price_cents: 150000 }], payment_plan: { mode: 'installments', payment_method: 'boleto', account_id: null, paid_at: null, entry_amount_cents: null, installments: [ { amount_cents: 50000, due_date: '2026-10-10' }, { amount_cents: 50000, due_date: '2026-11-10' }, { amount_cents: 50000, due_date: '2026-12-10' }, ], }, }, saleKey,);
// 2. La primera cuota se pagó con Pix.const firstInstallment = sale.installments[0];const updated = await post( `/sales/${sale.id}/payments`, { amount_cents: firstInstallment.amount_cents, paid_at: '2026-10-09', payment_method: 'pix', installment_id: firstInstallment.id, }, paymentKey,);
console.log(updated.sale_number, updated.payment_status, updated.paid_cents);<?php
const API = 'https://api.fatureihoje.com/public/v1';
function post(string $path, array $body, string $idempotencyKey): array{ $ch = curl_init(API . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FH_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $idempotencyKey, ], CURLOPT_POSTFIELDS => json_encode($body), ]); $data = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status >= 400) { throw new RuntimeException($status . ' ' . $data['error']['code'] . ': ' . $data['error']['message']); } return $data;}
// Genere las claves junto con la operación y guárdelas: en un reintento, reutilícelas.$saleKey = bin2hex(random_bytes(16));$paymentKey = bin2hex(random_bytes(16));
// 1. Venta de R$ 1.500,00 en 3 cuotas con boleto.$sale = post('/sales', [ 'client_id' => '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', 'items' => [ ['type' => 'custom', 'name' => 'Instalación de 4 cámaras', 'quantity' => 1, 'unit_price_cents' => 150000], ], 'payment_plan' => [ 'mode' => 'installments', 'payment_method' => 'boleto', 'account_id' => null, 'paid_at' => null, 'entry_amount_cents' => null, 'installments' => [ ['amount_cents' => 50000, 'due_date' => '2026-10-10'], ['amount_cents' => 50000, 'due_date' => '2026-11-10'], ['amount_cents' => 50000, 'due_date' => '2026-12-10'], ], ],], $saleKey);
// 2. La primera cuota se pagó con Pix.$first = $sale['installments'][0];$updated = post('/sales/' . $sale['id'] . '/payments', [ 'amount_cents' => $first['amount_cents'], 'paid_at' => '2026-10-09', 'payment_method' => 'pix', 'installment_id' => $first['id'],], $paymentKey);
echo $updated['sale_number'], ' ', $updated['payment_status'], ' ', $updated['paid_cents'], PHP_EOL;import jsonimport osimport urllib.errorimport urllib.requestimport uuid
API = "https://api.fatureihoje.com/public/v1"
def post(path, body, idempotency_key): request = urllib.request.Request( API + path, data=json.dumps(body).encode(), method="POST", headers={ "Authorization": f"Bearer {os.environ['FH_API_KEY']}", "Content-Type": "application/json", "Idempotency-Key": idempotency_key, }, ) try: with urllib.request.urlopen(request) as response: return json.load(response) except urllib.error.HTTPError as failure: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")
# Genere las claves junto con la operación y guárdelas: en un reintento, reutilícelas.sale_key = str(uuid.uuid4())payment_key = str(uuid.uuid4())
# 1. Venta de R$ 1.500,00 en 3 cuotas con boleto.sale = post( "/sales", { "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "items": [ {"type": "custom", "name": "Instalación de 4 cámaras", "quantity": 1, "unit_price_cents": 150000} ], "payment_plan": { "mode": "installments", "payment_method": "boleto", "account_id": None, "paid_at": None, "entry_amount_cents": None, "installments": [ {"amount_cents": 50000, "due_date": "2026-10-10"}, {"amount_cents": 50000, "due_date": "2026-11-10"}, {"amount_cents": 50000, "due_date": "2026-12-10"}, ], }, }, sale_key,)
# 2. La primera cuota se pagó con Pix.first = sale["installments"][0]updated = post( f"/sales/{sale['id']}/payments", { "amount_cents": first["amount_cents"], "paid_at": "2026-10-09", "payment_method": "pix", "installment_id": first["id"], }, payment_key,)
print(updated["sale_number"], updated["payment_status"], updated["paid_cents"])Los campos de la venta, del ítem y de la respuesta están en la Referencia. Tres puntos del ejemplo:
- El ítem
customes suelto y lleva el nombre que usted envía. El ítemcatalogapunta a un producto registrado (product_id, yvariation_idcuando el producto tiene variaciones), y en ese caso el nombre y el precio promocional vienen del registro. - La venta responde con
installments, cada cuota con suid. Eseidva eninstallment_idal cobrar, y en la reprogramación. payment_statuspasa dependingapartialy después apaida medida que entran los cobros, ypaid_centssuma lo que ya se cobró.
POST /public/v1/sales/{id}/payments es la ventana Cobrar de la pantalla:
paid_ates la fecha en que entró el dinero (YYYY-MM-DD), y es obligatorio.- El monto no puede pasar del saldo de la venta.
- Con
installment_id, el monto salda esa cuota, que tiene que estar abierta. Sin él, el monto entra por orden de vencimiento. account_idelige la cuenta que recibió, y pidefinance:readyfinance:createen la clave, además del acceso del miembro a finanzas. Sin él, se usa la cuenta de la venta.
Toda escritura lleva Idempotency-Key, y en el cobro pesa más. Un cobro repetido después de una falla de red, sin la clave, entra dos veces. Con la misma clave, la repetición devuelve la respuesta de la primera y no se graba nada de nuevo. Vea Idempotencia.
Un cobro equivocado se anula con POST /public/v1/sales/{id}/payments/{payment_id}/cancel, con el motivo en reason. Para cambiar montos y vencimientos de las cuotas abiertas, use PATCH /public/v1/sales/{id}/installments, que es la ventana Reprogramar de la pantalla.
Finanzas y stock
Sección titulada «Finanzas y stock»- Finanzas. Sin el campo
track_in_finance, vale la configuración de la empresa, y ese registro automático no pide permiso de finanzas. Enviar el campo, con cualquier valor, exige que el miembro tenga acceso a finanzas;trueexige ademásfinance:createen la clave, yfalseno. Elegir la cuenta enpayment_plan.account_idexigefinance:readyfinance:createen la clave, además del acceso del miembro. - Lo que usted ve.
account_idyfinance_transactionssolo vienen en la respuesta cuando la clave tienefinance:ready su miembro tiene acceso a finanzas. Sin eso, la venta viene sin esos dos campos, como en el panel. Las cuotas y los cobros vienen siempre. - Stock.
decrement_stocknacetrue, como en la pantalla, y descuenta el stock de los ítems del catálogo sin pedirproducts:update. Envíefalsecuando el stock se controla en otro lugar.
Cambiar y cancelar
Sección titulada «Cambiar y cancelar»Una venta no se elimina, tampoco en el panel.
- Un cambio comercial (ítems, montos, cliente, vendedor, plan del saldo) es
POST /public/v1/sales/{id}/versions, con el motivo enreason. Elidde la venta no cambia, yversionsube. Lo ya cobrado sigue valiendo.GET /public/v1/sales/{id}/versionstrae el historial. - Observaciones y producción cambian con un
PATCH /public/v1/sales/{id}, sin versión nueva. - Cancelar es
POST /public/v1/sales/{id}/cancel, con el motivo. Anula los cobros, devuelve el stock descontado y cancela los movimientos.
Las trabas son las de la pantalla, acción por acción: una venta cancelada, con factura activa, con la comisión ya pagada o generada por un contrato rechaza algunas acciones con 409. La tabla completa está en la Visión general.
Cuando la llamada falla
Sección titulada «Cuando la llamada falla»| Respuesta | Qué hacer |
|---|---|
422 validation_failed | Corrija el campo indicado en errors, como cuotas que no cierran con el total |
409 conflict | El estado de la venta no lo permite: cancelada, con factura activa, con comisión pagada o de contrato |
403 permission_missing | Le falta un permiso a la clave, como finance:create con track_in_finance: true |
403 member_permission_denied | El miembro de la clave no tiene acceso a finanzas y la llamada toca finanzas |
429 rate_limit_exceeded | Espere el Retry-After y repita. Vea Límites de uso |
| 5xx o falla de red | Repita con la misma Idempotency-Key |
Seguirla por webhook
Sección titulada «Seguirla por webhook»Para que su sistema sepa que la venta se pagó, incluso cuando el cobro se registró en el panel, suscriba un endpoint a sale.payment_registered y sale.paid. Los eventos de venta nunca llevan la cuenta ni los movimientos; esos tienen sus propios eventos financial_transaction.*. Vea el Catálogo de eventos.
Próximo paso
Sección titulada «Próximo paso»- Sincronización incremental con
updated_after: en la lista de ventas, una versión nueva, un cobro y una cancelación cuentan como cambio. - Fechas y dinero: convertir centavos sin errores de redondeo.