Envío por API
Para el equipo que prefiere integrar en vez de subir archivos a mano. Si no es su caso, la página de envío hace lo mismo sin escribir una línea de código.
Esta guía alcanza para el primer envío. El contrato completo —cada endpoint, cada campo y cada respuesta, con ejemplos que se pueden probar— está en la referencia, que se abre en otra pestaña para poder leerla al lado del código:
Un 202 significa que lo guardamos, no que el contenido esté bien. No validamos nada al recibir: revisamos después y le escribimos si algo no cierra.
El token
Le entregamos un token por empresa. No vence.
Authorization: Bearer su-tokenViaja sólo desde su servidor, nunca desde un navegador ni una aplicación móvil. Si se filtra, escríbanos y lo rotamos.
Cada cuánto
| Cadencia | Qué lleva cada envío | |
|---|---|---|
| Ventas | Un envío por día | Un solo date: el día cerrado, con una fila por artículo, tienda y banner |
| Stock | Un envío por día | Un solo as_of: la foto de ese mismo cierre |
Es un ciclo por día, los siete días de la semana: cada día se envían la venta del día anterior ya cerrado y la foto de stock de ese cierre. date y as_of coinciden. El día en curso no se envía.
Mande el stock aunque no haya vendido nada. Los dos envíos van todos los días. Si un día llega el stock y no llegan ventas, sabemos que ese día no vendió; si no llega ninguno de los dos, no sabemos si no vendió o no reportó.
Los días sin ventas no se llama a /v1/sales: un cuerpo vacío devuelve 400.
Por eso los dos envíos de un día van juntos o no va ninguno, y en ese orden: primero las ventas, después el stock. Si el envío de ventas falla, no mande el stock de ese día — preferimos un día sin reportar antes que un día que afirma que no vendió. Y si un envío se atrasa, mándelo igual cuando pueda: reenviar el día completo lo corrige.
Los dos envíos
Ventas y stock van por separado, cada uno con su endpoint. El cuerpo es un array, no un objeto: las filas y nada más.
Los ejemplos de abajo dan por sentadas estas dos variables:
BASE=https://sellout.yupana.ai
TOKEN=su-tokenVentas
curl -X POST "$BASE/v1/sales" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @ventas.jsonStock
curl -X POST "$BASE/v1/stock" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @stock.jsonEl cuerpo es un array de filas. Una fila de ventas:
[
{
"date": "2026-08-05",
"banner": { "name": "Yupana Sport" },
"point_of_sale": { "name": "Yupana Sport Palermo", "type": "physical" },
"product": {
"sku": { "model": "FI310882", "variation": "NEGRO", "size": "42", "size_scale": "AR" },
"brand": "Fila"
},
"currency": "ARS",
"quantity": 2,
"total_amount_at_sale_price_excl_vat": 141308.54,
"total_amount_at_list_price_excl_vat": 163333.33,
"total_amount_at_cost_excl_vat": 84000.00
}
]Todos los campos están en Qué lleva cada fila y en la referencia enlazada arriba.
Envíos grandes
El límite es 10 MB por envío. Una foto de stock completa suele pasarlo, así que mándelo comprimido:
gzip -c stock.json | curl -X POST "$BASE/v1/stock" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "Content-Encoding: gzip" \
--data-binary @-Un JSON de sell-out comprime cerca de diez veces, así que con gzip entran unas 200.000 filas de ventas.
Conviene declarar el envío comprimido con Content-Type: application/octet-stream, que es lo que es. Con application/json también funciona.
Qué devuelve
{
"upload_id": "9f3a1c7e4b2d48a6",
"company": "Yupana Sport",
"kind": "sales",
"rows": 4212,
"bytes": 2098431,
"received_at": "2026-08-12T15:04:05+00:00",
"key": "yupanasport/sales/2026/08/12/9f3a1c7e4b2d48a6-api.json"
}rows son las filas que contamos. Compárelo con lo que mandó: si no coincide, el problema está de su lado y conviene verlo ahora y no dentro de un mes.
Viene en null cuando el cuerpo no se pudo interpretar o era demasiado grande para contarlo. El envío se guardó igual.
Guarde el upload_id: es lo que hay que citar si nos escribe por ese envío.
Errores
| HTTP | Qué pasó |
|---|---|
400 | El cuerpo vino vacío |
401 | Falta el token o no es válido |
413 | El envío supera los 10 MB. Mándelo con gzip |
500 | No pudimos guardarlo. Ya estamos avisados; reintente |
No hay errores de validación de contenido porque no validamos el contenido.
Antes de producción
Le damos un token de prueba contra el ambiente de staging, que es igual a producción pero guarda aparte:
BASE=https://sellout-staging.yupana.aiMande unos días reales ahí, los revisamos juntos, y recién entonces le entregamos el token de producción. Lo único que cambia entre un ambiente y el otro son el BASE y el token.
El token de staging no sirve en producción ni al revés. Si se confunde de par, la respuesta es 401.