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
  1. Enviar el stock desde tu sistema (push)
  2. La prueba en seco
  3. El lote de verdad
  4. Un catálogo completo en varias partes
  5. El resumen: ¿está al día el stock?
  6. Operar los perfiles
  7. Perfiles como código (CI)
  8. Entrega de archivos (SFTP o S3)
  9. Y las ventas

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.

FormaQuién mueve los datosSe configura
Lectura de tu base de datosStockAI 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 archivosTu 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.

  1. Crea el perfil con POST /inventory-sync/profiles y source_kind: "push". En mapping, a cada campo de StockAI le dices de qué columna de tus filas sale. sku y current_stock son obligatorios. Nace apagado (enabled: false).
  2. 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.
  3. Enciende el perfil con POST /inventory-sync/profiles/{id}/enable. Se rechaza con inventory_sync_dry_run_required si no hubo una prueba en seco desde el último cambio de configuración.
  4. Manda los lotes de verdad, cada uno con su propio batch_id.
  5. Vigila el estado con GET /inventory-sync/overview o GET /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_inactive o zero.
negative_policy
Stock negativo: reject (la fila se rechaza) o zero.
duplicate_policy
El mismo SKU y bodega dos veces: reject o sum.
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_model
snapshot (cada fila es el stock actual), aggregate o ledger (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_id el 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"
# }
state
none (no hay fuentes), waiting (esperando la primera corrida), fresh (al día), attention (algo espera a una persona) o blocked.
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_7d resume 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 rutaQué 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-runPrueba 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.csvHistorial 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, /ownershipQué 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=yaml

POST /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/ o rejected/ (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.