Sincronización de stock y archivos por API
Mantener el stock al día desde tu sistema: perfiles de sincronización, envío de lotes idempotentes (push), su estado y el resumen de la sincronización, y la entrega de archivos por SFTP o S3.
En esta página
Hay tres formas de que el stock de StockAI siga al de tu sistema sin que nadie lo cargue a mano. Las tres pasan por el mismo motor: tu mapeo de columnas, la validación de cada fila, un freno de seguridad que detiene un cambio sospechoso y un registro de cada corrida.
| Forma | Quién mueve los datos | Se configura |
|---|---|---|
| Lectura de tu base de datos | StockAI consulta tu base (SQL o MongoDB) cada cierto tiempo. | En Sincronización de inventario o por API. |
| Envío (push) | Tu sistema manda las filas a StockAI. | Por API (esta página) o en la pantalla. |
| Entrega de archivos | Tu sistema deja un archivo en un SFTP o un bucket S3 y StockAI lo recoge. | Solo en la pantalla, por un administrador. |
Enviar el stock desde tu sistema (push)
Si no puedes abrir tu base de datos a StockAI, tu sistema manda las filas. Primero creas un perfil push que dice qué columna es qué; después mandas lotes.
- Crea el perfil con
POST /inventory-sync/profilesysource_kind: "push". Enmapping, a cada campo de StockAI le dices de qué columna de tus filas sale.skuycurrent_stockson obligatorios. Nace apagado (enabled: false). - Haz una prueba en seco: manda un lote con
dry_run: true. No escribe nada y te dice qué crearía, qué actualizaría y qué filas rechazaría. - Enciende el perfil con
POST /inventory-sync/profiles/{id}/enable. Se rechaza coninventory_sync_dry_run_requiredsi no hubo una prueba en seco desde el último cambio de configuración. - Manda los lotes de verdad, cada uno con su propio
batch_id. - Vigila el estado con
GET /inventory-sync/overviewoGET /inventory-sync/health.
curl -X POST https://app.stockai.es/api/v1/inventory-sync/profiles \
-H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: erp-push-profile-1" \
-d '{"name":"ERP push","source_kind":"push","default_warehouse":"principal",
"mapping":{"sku":{"column":"item_code"},
"current_stock":{"column":"on_hand"},
"warehouse":{"column":"site"}}}'
# 201 -> "data": { "id": "isp_d194eddb57d3", "source_kind": "push", "enabled": false,
# "mode": "full", "missing_policy": "ignore", "negative_policy": "reject",
# "duplicate_policy": "reject", "brake_max_vanished_pct": 20.0, ... }Campos de StockAI que puedes mapear: sku, warehouse, current_stock, min_stock, unit_cost, supplier, lead_time_days, category, brand, unit_of_measure, barcode, sale_price, display_name. Cada uno acepta, además de column: cast (auto, number, integer, text), trim, case (none, upper, lower), decimal (auto, dot, comma), multiplier, default y value_map. GET /inventory-sync/fields devuelve la lista completa de opciones y límites.
missing_policy- Qué pasa con un SKU que deja de venir en un envío completo:
ignore(nada),mark_inactiveozero. negative_policy- Stock negativo:
reject(la fila se rechaza) ozero. duplicate_policy- El mismo SKU y bodega dos veces:
rejectosum. default_warehouse- La bodega de las filas que no traen una.
allow_new_warehouses- Si una bodega desconocida se crea (por defecto no: la fila se rechaza).
brake_max_vanished_pct,brake_min_row_ratio,brake_max_rejected_pct,brake_max_jump_pct- El freno de seguridad: por defecto se detiene si desaparece más del 20 % de los SKUs, si llegan menos de la mitad de las filas de lo normal o si se rechaza más del 50 %.
stock_modelsnapshot(cada fila es el stock actual),aggregateoledger(cada fila es un movimiento).
La prueba en seco
curl -X POST https://app.stockai.es/api/v1/inventory-sync/profiles/$PROFILE/ingest \
-H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
-d '{"batch_id":"night-2026-10-22","dry_run":true,
"rows":[{"item_code":"SKU-001","on_hand":118,"site":"principal"},
{"item_code":"SKU-002","on_hand":"42","site":"principal"},
{"item_code":"","on_hand":5}]}'"result": {
"rows_read": 3, "valid": 2, "rejected": 1,
"rejected_by_code": { "sku_blank": 1 },
"rejected_sample": [{ "code": "sku_blank", "params": {}, "sku": null, "row": 3 }],
"would_create": 0, "would_update": 2, "would_leave_unchanged": 0,
"changes_sample": [
{ "sku": "SKU-001", "warehouse": "principal", "action": "update",
"fields": { "current_stock": { "before": 100.0, "after": 118.0 } } },
{ "sku": "SKU-002", "warehouse": "principal", "action": "update",
"fields": { "current_stock": { "before": 570.0, "after": 42.0 } } }
],
"brake_warning": null, "stock_model": "snapshot"
}En el ejemplo, dos filas actualizarían el stock (con el antes y el después) y la tercera se rechaza con sku_blank. Cada rechazo trae su código, sus parámetros y el número de fila.
El lote de verdad
# Turn the profile on (refused with inventory_sync_dry_run_required until a dry run passed)
curl -X POST https://app.stockai.es/api/v1/inventory-sync/profiles/$PROFILE/enable \
-H "Authorization: Bearer $STOCKAI_KEY"
# Send the batch for real
curl -X POST https://app.stockai.es/api/v1/inventory-sync/profiles/$PROFILE/ingest \
-H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
-d '{"batch_id":"night-2026-10-22",
"rows":[{"item_code":"SKU-001","on_hand":118,"site":"principal"},
{"item_code":"SKU-002","on_hand":"42","site":"principal"}]}'
# "data": { "batch_id": "night-2026-10-22", "replayed": false,
# "run": { "id": "isr_052d3da33a21", "status": "success", "run_kind": "push",
# "rows_read": 2, "rows_valid": 2, "created": 0, "updated": 2,
# "unchanged": 0, "rejected": 0, "duration_ms": 500 } }
# Same batch_id + same rows again: "replayed": true, same run, nothing applied twice.
# Same batch_id + different rows: 409 inventory_sync_batch_id_reused.batch_id: tu id del lote, de 1 a 128 caracteres[A-Za-z0-9._:-]. Reintentar con el mismo id y las mismas filas no aplica nada dos veces.- Sin
snapshot_idel lote es parcial: actualiza los SKUs que nombra y nunca da por «desaparecido» uno que no vino. - Hasta 50.000 filas por lote (más:
413 inventory_sync_ingest_too_large; manda varios). - Si tus filas traen objetos anidados, se aplanan a rutas con punto (
stock.on_hand).
También puedes mandar NDJSON (una fila JSON por línea), con los demás campos como parámetros de la URL:
printf '{"item_code":"SKU-003","on_hand":7}\n{"item_code":"SKU-005","on_hand":9}\n' |
curl -X POST "https://app.stockai.es/api/v1/inventory-sync/profiles/$PROFILE/ingest?batch_id=nd-1" \
-H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/x-ndjson" --data-binary @-Un catálogo completo en varias partes
# A full catalogue in 3 parts: the same snapshot_id, last=true only on the final part.
.../ingest {"batch_id":"snap-22-a","snapshot_id":"snap-22","last":false,"rows":[...]}
.../ingest {"batch_id":"snap-22-b","snapshot_id":"snap-22","last":false,"rows":[...]}
.../ingest {"batch_id":"snap-22-c","snapshot_id":"snap-22","last":true, "rows":[...]}
# Parts are staged; the last one applies the whole snapshot with every safety brake.Con snapshot_id, las partes se guardan aparte y la que lleva last: true aplica el catálogo entero de una vez, con todos los frenos (SKUs que desaparecen, caída de filas). Es la forma correcta de mandar «este es todo mi stock» cuando no cabe en un lote.
El resumen: ¿está al día el stock?
curl https://app.stockai.es/api/v1/inventory-sync/overview -H "Authorization: Bearer $STOCKAI_KEY"
# "data": {
# "state": "fresh", // none | waiting | fresh | attention | blocked
# "attention": [], // what only a person can settle, worst first
# "sources": [{
# "id": "isp_d194eddb57d3", "name": "ERP push", "origin": "push",
# "state": "fresh", "health": "healthy", "enabled": true,
# "data_as_of": "2026-10-22T20:46:46-06:00", "running": false, "expected_minutes": 60,
# "last_run": { "id": "isr_17e09a684d26", "status": "success", "rows_read": 2,
# "created": 0, "updated": 2, "unchanged": 0, "rejected": 0,
# "error_code": null, "brake_overridden": false },
# "files_7d": null
# }],
# "data_as_of": "2026-10-22T20:46:46-06:00"
# }statenone(no hay fuentes),waiting(esperando la primera corrida),fresh(al día),attention(algo espera a una persona) oblocked.attention[]- Lo que solo una persona puede resolver, lo peor primero: un archivo de stock retenido para decidir, una sincronización que el freno detuvo, una fuente que nos rechaza, una sincronización atrasada, filas rechazadas. Cada elemento trae los ids que la acción necesita.
sources[]- Cada fuente — base de datos, entrega de archivos o push — con su frescura y su última corrida.
files_7dresume los archivos de los últimos 7 días en las fuentes de archivo.
/overview nunca responde 503: es para leer. Para un monitor de disponibilidad usa /health?strict=true, que responde 503 si algún perfil no está bien:
# For an uptime checker: answers 503 when any profile is not ok
curl -i "https://app.stockai.es/api/v1/inventory-sync/health?strict=true" -H "Authorization: Bearer $STOCKAI_KEY"Operar los perfiles
| Método y ruta | Qué hace |
|---|---|
GET /inventory-sync/profiles, GET .../profiles/{id} | Perfiles con su estado: activo, último éxito, atrasado, frenado. |
PATCH /inventory-sync/profiles/{id} | Cambia un perfil. Si cambia lo que escribe, se apaga hasta una nueva prueba en seco. |
POST .../profiles/{id}/dry-run | Prueba en seco de un perfil guardado que lee una base de datos. |
POST .../profiles/{id}/run | «Sincronizar ahora»: responde 202 con la corrida en marcha. {"override_brake": true} pasa el freno una vez (queda auditado). |
POST /inventory-sync/refresh | «Actualizar ahora» (todo, un perfil o algunos skus); espera hasta wait_seconds y respeta un tiempo mínimo entre corridas (status: cooldown, retry_after_s). |
GET .../profiles/{id}/runs, .../runs/{run_id}, .../runs/{run_id}/rejected.csv | Historial de corridas, el detalle de una y sus filas rechazadas en CSV. |
POST .../profiles/{id}/disable, DELETE .../profiles/{id} | Apagar o borrar el perfil. El stock que sincronizó se queda como está. |
GET /inventory-sync/freshness, /ownership | Qué tan al día está el stock y qué perfil es dueño de cada campo. |
Perfiles como código (CI)
curl -X POST "https://app.stockai.es/api/v1/inventory-sync/profiles/apply?dry_run=true" \
-H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
--data-binary @inventory-sync.json
# The document format: GET /inventory-sync/schema/profile.json ("schema": "stockai.inventory-sync/v1").
# Export an existing one: GET /inventory-sync/profiles/{id}/export?format=yamlPOST /inventory-sync/profiles/apply recibe un documento JSON o YAML: lo crea, o lo actualiza por su slug. Aplicar el mismo archivo otra vez responde action: unchanged, así que tu CI puede aplicarlo en cada despliegue. POST /inventory-sync/profiles/validate solo valida y devuelve cada problema con su ruta.
Entrega de archivos (SFTP o S3)
Si tu ERP ya deja un export en una carpeta, no hace falta programar nada: un administrador configura en Sincronización de inventario un servidor SFTP o un bucket S3, y StockAI recoge cada archivo nuevo. Un archivo puede alimentar las ventas (reemplaza un dataset) o el stock.
- Solo lee un archivo cuando terminó de subirse (dos lecturas que coinciden en tamaño y fecha, y al menos un minuto de antigüedad).
- El mismo contenido nunca se aplica dos veces.
- Un archivo sospechoso — vacío, con otras columnas o con muchas menos filas — se retiene para que una persona decida, en lugar de borrar la historia.
- Después mueve el archivo a
processed/orejected/(con una nota.reason.txt). Nunca lo borra.
Y las ventas
Para las ventas, lo habitual es reemplazar el archivo de la fuente cada noche con POST /data-sources/{id}/file. Ver Pronósticos y sesiones.