Ventas
Cuando un comprador paga en tu tienda en línea, registra la venta en Zenci con POST /orders. La venta queda cobrada y entregada en una sola llamada: descuenta las existencias, aparece en Ventas → Órdenes con la etiqueta Tienda en línea y, si el comercio tiene la facturación electrónica configurada, se factura sola unos segundos después.
| Endpoint | Permiso |
|---|---|
POST /orders | orders:write |
GET /orders | orders:read |
GET /orders/{id} | orders:read |
Antes de vender
- Revisa la clave con . Si
defaultBranchIdesnull, cada venta tiene que enviarbranchId: búscalo en . - Lee las formas de pago con y guarda sus
id. - Ten a mano los SKU de tus productos, que salen de .
- Si la venta lleva factura a nombre del comprador, búscalo o créalo con y envía su
customerId.
La petición
Encabezados
| Encabezado | Obligatorio | Descripción |
|---|---|---|
Authorization o X-API-Key | Sí | Tu clave de API |
Content-Type | Recomendado | application/json |
X-Idempotency-Key | Sí | Un identificador único de esta venta en tu tienda, por ejemplo el número de tu pedido. Consulta |
Cuerpo
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
items | lista | Sí | Al menos una línea |
items[].sku | texto | Sí | SKU de la variante. Debe coincidir exactamente, con mayúsculas y minúsculas; no se busca por código de barras |
items[].quantity | texto | Sí | Cantidad mayor que cero, hasta 6 decimales: "1", "2", "0.5" |
items[].price | texto | No | Precio por unidad sin impuesto, hasta 2 decimales. Solo con el permiso orders:set-price; consulta |
payments | lista | Sí | Al menos un pago |
payments[].paymentMethodId | texto | Sí | Un id de GET /payment-methods |
payments[].amount | texto | Sí | Monto mayor que cero, hasta 2 decimales |
branchId | texto | No | Sucursal de la venta. Si no lo envías, se usa la de la clave |
customerId | texto o null | No | Cliente al que se le vende y se le factura. Búscalo o créalo con . Sin cliente, la factura sale a consumidor final |
companyId | texto o null | No | Una de las compañías del cliente, para facturarle a ella y no a la principal. Necesita customerId |
No hay campos para descuentos, notas ni envío. Cualquier campo que no esté en esta tabla se rechaza con 400.
Ejemplo
Un par de sandalias talla 38, pagado en efectivo con 40.00:
curl -X POST https://api.zenci.app/public/v1/orders \
-H "Authorization: Bearer $ZENCI_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: pedido-10482" \
-d '{
"items": [
{ "sku": "MB-029-CRU-38", "quantity": "1" }
],
"payments": [
{ "paymentMethodId": "m1a5s9d3f7g2h6j0k4l8z2x6", "amount": "40.00" }
]
}'Respuesta 201:
{
"id": "o5h9j3k7l1z4x8c2v6b0n4m8",
"displayId": "004518",
"status": "paid",
"customerId": null,
"companyId": null,
"invoice": null,
"subtotal": "36.00",
"taxes": "2.52",
"total": "38.52",
"items": [
{
"sku": "MB-029-CRU-38",
"name": "Sandalia Crucero",
"quantity": "1",
"price": "36.00",
"total": "36.00"
}
],
"payments": [
{
"paymentMethodId": "m1a5s9d3f7g2h6j0k4l8z2x6",
"paymentMethodName": "Efectivo",
"amount": "38.52",
"tenderedAmount": "40.00",
"change": "1.48"
}
],
"change": "1.48",
"createdAt": "2026-10-06T15:56:12.000Z"
}| Campo | Descripción |
|---|---|
id | Identificador de la venta en Zenci. Guárdalo junto a tu pedido |
displayId | Número de orden que ve el comercio en Órdenes |
status | Estado del cobro: paid en una venta completa |
customerId, companyId | El cliente y la compañía a la que se factura, o null |
invoice | La factura electrónica (ver ). Al registrar la venta todavía es null: se emite unos segundos después |
subtotal | Suma de las líneas, sin impuesto |
taxes | Impuesto total |
total | Lo que se cobró: subtotal + taxes |
items[].sku | El SKU que enviaste |
items[].name | Nombre del producto |
items[].price | Precio por unidad sin impuesto con el que se vendió |
items[].total | price × quantity, sin impuesto |
payments[].amount | Lo que se aplicó a la venta |
payments[].tenderedAmount | Lo que entregó el comprador. Igual a amount si pagó exacto |
payments[].change | El vuelto de ese pago; "0.00" si no hubo |
change | La suma de los vueltos |
Cómo se calcula el total
Los precios de Zenci no incluyen el impuesto. Para cobrarle al comprador lo mismo que registrará Zenci, calcula así, línea por línea:
- Base de la línea =
price×quantity, redondeada a centavos. - Impuesto de la línea = base ×
taxRate÷ 100, redondeado a centavos. SitaxRateesnull, el impuesto es0.00. - Subtotal = suma de las bases. Impuesto = suma de los impuestos de cada línea. Total = subtotal + impuesto.
El redondeo es al centavo más cercano y, en el medio exacto, hacia arriba (0.125 → 0.13).
Ejemplo con tres líneas al 7 %:
| Línea | Base | Impuesto |
|---|---|---|
1 × MB-029-CRU-38 a 36.00 | 36.00 | 2.52 |
1 × MB-087-PAN-M a 12.50 | 12.50 | 0.88 (de 0.875) |
1 × MB-087-PAN-L a 12.50 | 12.50 | 0.88 (de 0.875) |
| Suma | subtotal 61.00 | impuesto 4.28 |
Total: 61.00 + 4.28 = 65.28.
Redondea el impuesto por línea, no sobre el subtotal. Sobre el subtotal saldría 61.00 × 7 % = 4.27, un centavo menos: tu tienda cobraría 65.27 y la venta se rechazaría porque el pago no cubre el total.
El taxRate y el price que debes usar son los de GET /products con la misma clave.
Los pagos
- La suma de los pagos tiene que cubrir el total. Si queda corta, la respuesta es
409 conflictcon el mensajePayments do not cover the order total. La venta se anula en el acto: no queda cobrada ni descuenta existencias, aunque el comercio puede verla como anulada en Órdenes. Corrige los montos y reenvía con la mismaX-Idempotency-Key. - Pagar de más solo se puede con una forma de pago que da vuelto, como el efectivo. El pago se aplica hasta el total y el resto se devuelve en
change. - Con las demás formas de pago, el monto debe ser exacto. Un monto de más con tarjeta responde
400(Payment amount exceeds remaining balance of …) y la venta se anula, igual que en el caso anterior. - Puedes combinar pagos, por ejemplo una parte con tarjeta y otra en efectivo. Lo más simple para una tienda en línea es un solo pago por el total exacto.
El precio de venta
Por defecto, el precio lo pone Zenci: cada línea se vende al precio de la variante en el catálogo o, si la clave tiene una lista de precios, en esa lista. Es el mismo price que ves en GET /products.
Si tu tienda necesita vender a otro precio (una oferta propia, por ejemplo), la clave necesita el permiso orders:set-price. Con él, puedes enviar price en cada línea: es el precio por unidad sin impuesto, y el impuesto se suma igual que siempre. Las líneas sin price siguen usando el precio de Zenci.
Si envías price con una clave que no tiene ese permiso, la venta se rechaza con 400 (This API key cannot set prices…). No se ignora en silencio: así nunca cobras un precio y registras otro.
Reintentos e idempotencia
Todo POST /orders lleva X-Idempotency-Key. Sin ella, la respuesta es 400 (The X-Idempotency-Key header is required).
La regla es una sola: la clave manda, el cuerpo no. Zenci registra una sola venta por cada X-Idempotency-Key de tu clave de API.
- Reintento con la misma clave y el mismo cuerpo: recibes
201con la venta original. No se cobra ni se descuenta otra vez. - Reintento con la misma clave y un cuerpo distinto: también recibes
201con la venta original. Zenci no compara el cuerpo: los cambios que hayas hecho no se aplican. - Una venta nueva lleva una clave nueva. Usa el identificador de tu pedido, no uno al azar en cada intento: así un reintento siempre reconoce su venta.
La clave es propia de cada clave de API. Dos tiendas del mismo comercio que usan claves de API distintas pueden mandar la misma X-Idempotency-Key sin chocar.
Cuándo reintentar
| Respuesta | Qué hacer |
|---|---|
| Sin respuesta (corte de red, tiempo agotado) | Reintenta con la misma clave. Si la venta ya entró, recibes la original |
409 The original sale is still being registered. Retry in a few seconds. | La venta original todavía se está registrando. Espera lo que dice el encabezado Retry-After y reintenta con la misma clave |
500 | Reintenta con la misma clave en unos segundos |
409 Payments do not cover the order total | Corrige los pagos y reenvía con la misma clave: la venta fallida no la ocupa |
409 We could not confirm whether the original sale completed. Review it before charging again. | No reintentes. La venta con esa clave quedó a medias hace más de cinco minutos: pídele al comercio que la revise en Órdenes |
409 The original sale was cancelled while completing. Register it manually. | No reintentes. La venta se anuló mientras se registraba: el comercio debe registrarla a mano |
400, 403, 404 | Corrige la petición y reenvíala. Una venta rechazada no ocupa la clave, así que puedes usar la misma |
429 | Espera unos segundos y reintenta con la misma clave |
Qué pasa con la venta en Zenci
- Se registra en la sucursal de la clave, o en la de
branchId, y descuenta las existencias de la bodega de esa sucursal en el momento. - Queda pagada y entregada. No pasa por ninguna caja, así que no aparece en el cierre de una caja.
- Se factura sola si el comercio tiene configurada la facturación electrónica, a nombre del cliente y la compañía de la venta. Si el cliente tiene correo, la DGI se la envía como cualquier otra factura. Si el comercio no factura electrónicamente, la venta queda sin factura y no da error.
- Aparece en Ventas → Órdenes con la etiqueta Tienda en línea.
- Una presentación enlazada (por ejemplo, una caja de 12) descuenta sus unidades de la variante base.
- Los servicios no mueven existencias.


Sin existencias suficientes
Depende de cómo tenga configurado el comercio la venta sin existencias. Si la permite, la venta se registra y las existencias quedan en negativo. Si la bloquea, la venta se rechaza con 400 y no se cobra nada; el mensaje dice qué producto no alcanzó: Not enough stock for "Sandalia Crucero". Consulta GET /stock antes de cobrar para evitarlo.
Errores frecuentes
| HTTP | Mensaje | Causa |
|---|---|---|
400 | The X-Idempotency-Key header is required | Falta el encabezado |
400 | A JSON body is required | El cuerpo no es JSON |
400 | Unknown SKU "…" | El SKU no existe, está eliminado o tiene otra escritura |
400 | No branch was provided and this API key has no default branch… | La clave no tiene sucursal y no enviaste branchId |
400 | This API key cannot set prices… | Enviaste price sin el permiso orders:set-price |
400 | Invalid "items.0.quantity": … | Un campo con formato inválido; el mensaje dice cuál |
400 | Payment amount exceeds remaining balance of … | Pago de más con una forma de pago que no da vuelto |
400 | Unknown or disabled payment method "…" | El paymentMethodId no existe o el comercio lo apagó |
400 | "…" cannot be used to charge a sale through the API… | Una forma de pago que la API no acepta: crédito, nota de crédito, tarjeta de regalo o retención |
400 | "companyId" requires "customerId" | Enviaste una compañía sin cliente |
400 | The billing company does not belong to this customer | La compañía no es de ese cliente |
400 | Not enough stock for "…" | El comercio no permite vender sin existencias |
403 | Insufficient permissions | La clave no tiene orders:write |
404 | Customer not found | El customerId no es un cliente del comercio |
404 | Warehouse not found for branch … | El branchId no es una sucursal del comercio |
409 | Payments do not cover the order total | Los pagos no llegan al total |
El formato de todos los errores está en la .
La factura
Después de registrar la venta, Zenci emite la factura electrónica por detrás. Mientras la DGI responde, la venta trae la factura en pending; cuando la acepta, en enrolled, con su número, su CUFE y un enlace al PDF. Normalmente tarda unos segundos.
Para enterarte sin consultar a cada rato, suscríbete al order.invoiced. También puedes consultar la venta:
GET /orders/{id}{
"id": "o5h9j3k7l1z4x8c2v6b0n4m8",
"displayId": "004518",
"status": "paid",
"customerId": "u3i7o1p5a9s2d6f0g4h8j2k6",
"companyId": null,
"invoice": {
"id": "f7g1h5j9k3l6z0x4c8v2b6n0",
"status": "enrolled",
"number": "004518",
"cufe": "FE01200000155612345-2-2015-001000000000451820261006…",
"issuedAt": "2026-10-06T15:56:19.000Z",
"pdfUrl": "https://api.zenci.app/public/v1/invoices/f7g1h5j9k3l6z0x4c8v2b6n0/pdf?tenant=…&expires=…&signature=…",
"pdfUrlExpiresAt": "2026-10-06T16:11:24.000Z"
},
"subtotal": "36.00",
"taxes": "2.52",
"total": "38.52",
"items": [ … ],
"payments": [ … ],
"change": "1.48",
"createdAt": "2026-10-06T15:56:12.000Z"
}invoice.status | Qué significa |
|---|---|
pending | Se envió a la DGI y todavía no hay respuesta |
enrolled | La DGI la aceptó. Trae number, cufe, issuedAt (en UTC) y pdfUrl |
failed | La DGI la rechazó. El comercio la corrige y reintenta desde Zenci |
review_required | La respuesta fue ambigua y el comercio la revisa en Zenci |
cancelled | Se anuló |
number es el número de la factura en Zenci, el mismo que ve el comercio en Órdenes. El comprobante de la DGI muestra además el número de documento del proveedor; lo que identifica la factura ante la DGI es el cufe.
invoice es null mientras la factura se crea, y también si el comercio no tiene la facturación electrónica configurada o si la factura no se pudo crear. En ese último caso llega el aviso order.invoice_failed con el motivo, por ejemplo que el cliente no tiene DV: corrige el cliente y Zenci la vuelve a intentar sola en las horas siguientes.
El PDF
pdfUrl es un enlace temporal: vence a los 15 minutos (pdfUrlExpiresAt) y no necesita la clave de API, así que puedes mostrárselo al comprador o descargar el PDF desde tu servidor. Si venció, vuelve a consultar la venta y recibirás uno nuevo. Úsalo tal cual: cualquier cambio en el enlace lo invalida.
Consultar tus ventas
GET /orders| Parámetro | Tipo | Descripción |
|---|---|---|
page | entero | Página, desde 1. Por defecto 1 |
limit | entero | De 1 a 100. Por defecto 50 |
createdSince | fecha ISO 8601 | Solo las ventas registradas en ese momento o después |
createdUntil | fecha ISO 8601 | Solo las registradas antes de ese momento |
Cada clave ve solo las ventas que registró ella, de la más reciente a la más antigua. Cada elemento trae id, displayId, status, total, customerId, companyId, invoice y createdAt; para las líneas y los pagos, consulta GET /orders/{id}. Una venta de otra clave responde 404.
Siguientes pasos
- — A quién le vendes y a qué compañía se factura.
- — Con qué puedes cobrar.
- — Entérate cuando la venta se factura, se anula o se devuelve.