Saltar a contenido

Guía técnica · API v1

Cómo envía sus datos operacionales el sistema de una empresa, o el conector que lo lee: ventas, inventario, órdenes de compra en tránsito y órdenes de producción en curso. La API es la misma para todos los clientes y habla en términos de planificación, no de ningún ERP.

Contrato 1.0.0-draft · OpenAPI 3.1
Formato JSON · UTF-8
Errores application/problem+json (RFC 9457)

Datos de ejemplo

Todos los ejemplos usan una empresa ficticia, Acme, con ubicaciones CD01 (centro de distribución), T001 y T002 (tiendas) y SKUs inventados.

Estado

El contrato completo está publicado. En el ambiente de pruebas funciona el ciclo de ventas hasta el dry-run; la confirmación (commit) es el siguiente paso. Una ruta que todavía no está disponible responde 501.

Ruta Para qué Estado
GET /v1/meta Ubicaciones, unidades y base del valor de la empresa En pruebas
POST /v1/ingest/batches Abre un lote En pruebas · ventas
POST /v1/ingest/batches/{id}/parts Envía líneas En pruebas · ventas
POST /v1/ingest/batches/{id}/dry-run Valida y calcula la diferencia sin aplicar En pruebas · ventas
GET /v1/ingest/batches/{id} Estado, conteos y cuarentena En pruebas
GET /v1/ingest/batches Historial de lotes En pruebas
POST /v1/ingest/batches/{id}/commit Valida y aplica Próximo
POST /v1/ingest/{entidad} Atajo de una llamada para lotes chicos Próximo
GET /v1/data-health Frescura por fuente y ubicación Próximo
Inventario, OC, OP Lotes de las otras tres entidades Después de ventas

Ambientes y clave

Cada empresa recibe una clave de API por ambiente, junto con la dirección de ese ambiente. La empresa sale de la clave, nunca del cuerpo del request. SOMA guarda solo una huella de la clave: si se pierde, se emite otra.

Ambiente Prefijo Uso
Pruebas soma_test_… Desarrollo y certificación del conector. No toca el plan real
Producción soma_live_… Solo después de certificar el conector
Authorization: Bearer soma_test_••••••••
SOMA-Connector: acme-erp/1.4.0
Content-Type: application/json

SOMA-Connector identifica quién envía y en qué versión. Es opcional y recomendado: queda registrado en cada lote.

Alcance de la clave Permite
ingest:sales Lotes de ventas
ingest:inventory Lotes de inventario
ingest:purchase_orders Lotes de órdenes de compra
ingest:production_orders Lotes de órdenes de producción
read:health /v1/data-health

Sin clave, o con una clave inválida o revocada: 401 unauthorized. Con una clave válida sin el alcance de esa entidad: 403 forbidden_scope. /v1/meta y la consulta de lotes aceptan cualquier clave válida de la empresa.

El lote

Todo se envía en lotes. Un lote declara una entidad y un alcance, recibe las líneas en una o varias partes, y se aplica completo o no se aplica. Reenviar un lote o una parte con el mismo contenido no duplica nada.

1. Abrir el lote

El batch_id lo genera el cliente: de 8 a 100 caracteres de A-Z a-z 0-9 . _ : -. El mismo batch_id con el mismo encabezado devuelve el mismo lote; con otro encabezado, 409 batch_id_conflict.

POST /v1/ingest/batches
{
  "batch_id": "ventas-2026W45-0001",
  "entity": "sales",
  "source_system": "ERP · ventas diarias",
  "extracted_at": "2026-11-09T02:00:00-04:00",
  "scope": {
    "date_from": "2026-10-12",
    "date_to": "2026-11-08",
    "locations": ["CD01", "T001"]
  },
  "value_basis": "net",
  "expected_lines": 18342,
  "partition_counts": [
    { "location": "CD01", "lines": 9120 },
    { "location": "T001", "lines": 9222 }
  ]
}

Respuesta 201:

{
  "batch_id": "ventas-2026W45-0001",
  "entity": "sales",
  "status": "open",
  "api_version": "1.0.0-draft",
  "connector": "acme-erp/1.4.0",
  "created_at": "2026-11-09T06:00:04Z",
  "expected_lines": 18342,
  "lines_received": 0,
  "lines_quarantined": 0,
  "diff": null,
  "problem_code": null
}
  • extracted_at lleva zona horaria y es el momento en que se leyó la fuente.
  • partition_counts es opcional. Si viene, tiene que sumar expected_lines, y SOMA compara cada partición (ubicación, o ubicación y día) con lo que recibió. Así se detecta una lectura truncada.

2. Enviar las partes

Hasta 10.000 líneas y 5 MB por parte, numeradas desde 1. Reenviar la misma parte con el mismo contenido no suma dos veces; con otro contenido, 409 batch_id_conflict.

POST /v1/ingest/batches/ventas-2026W45-0001/parts
{
  "part_number": 1,
  "lines": [
    { "location": "T001", "sku": "CAM-AZ-M", "date": "2026-10-14",
      "movement": "sale", "quantity": 24, "value_usd": 180.00 },
    { "location": "T001", "sku": "CAM-AZ-M", "date": "2026-10-15",
      "movement": "return", "quantity": 2, "value_usd": 15.00,
      "original_sale_date": "2026-10-14" }
  ]
}

Respuesta 200:

{
  "batch_id": "ventas-2026W45-0001",
  "part_number": 1,
  "lines_received": 10000,
  "total_lines_received": 10000
}

Las líneas no se rechazan al recibir la parte: se validan una por una en el dry-run y en el commit. Un 422 solo puede venir del encabezado del lote o de la parte, nunca de una línea.

3. Probar sin aplicar (dry-run)

Valida todo y compara el lote con lo que SOMA tiene dentro del alcance. No escribe nada y se puede repetir. Es la herramienta principal para poner en marcha un conector.

POST /v1/ingest/batches/ventas-2026W45-0001/dry-run

Respuesta 200:

{
  "batch_id": "ventas-2026W45-0001",
  "would_status": "held",
  "diff": {
    "inserted": 12, "updated": 40, "deleted": 2,
    "unchanged": 6150, "deleted_value_usd": 31.50
  },
  "lines_quarantined": 1,
  "problem_code": "first_batch_review",
  "problems": ["first_batch_review"],
  "problem_detail": "first_batch_review: 6192 filas del alcance vinieron de cargas por archivo…",
  "unmatched": { "locations": 0, "skus": 3 },
  "quarantine_sample": [
    { "line": 418, "part": 1, "field": "quantity",
      "code": "non_positive_quantity", "message": "Input should be greater than 0" }
  ]
}
  • would_status es el estado que tendría el lote si se confirmara.
  • problems lista todos los códigos encontrados, del más grave al menos grave; problem_code es el primero.
  • unmatched cuenta los SKUs y las ubicaciones que SOMA no conoce.
  • quarantine_sample trae hasta 100 líneas apartadas.

4. Confirmar (commit) · próximo

Cierra el lote, valida todo y aplica solo la diferencia en segundo plano. Responde 202 de inmediato; el resultado se consulta en el paso 5. Los commits de una misma empresa y entidad se procesan de a uno.

5. Consultar el resultado

GET /v1/ingest/batches/{id} devuelve el mismo objeto que al abrir, con el estado, la diferencia, los conteos, el problema si lo hubo y una muestra de la cuarentena. Un estado que el conector no conoce se trata como "todavía no terminado".

GET /v1/ingest/batches?entity=sales&status=held&since=2026-11-01T00:00:00Z&limit=50 lista los lotes del más nuevo al más viejo. Si hay más, la respuesta trae next_cursor, que se pasa como cursor.

Alcance y completitud

Dentro del alcance declarado, lo que no viene cuenta como cero

Cero venta, cero stock, ninguna orden abierta. Fuera del alcance SOMA no toca nada. Por eso el conector tiene que leer su fuente completa antes de enviar, y abortar si no puede comprobarlo.

Entidad Alcance Ejemplo
Ventas Ventana de fechas × ubicaciones {"date_from": "2026-10-12", "date_to": "2026-11-08", "locations": ["CD01"]}
Inventario, OC, OP Fecha de corte × ubicaciones {"as_of": "2026-11-09", "locations": ["CD01", "T001"], "empty_locations": []}
  • Reenvío de ventas. Cada envío semanal repite las últimas cuatro semanas (valor inicial, ajustable por empresa), para recoger las correcciones tardías del ERP.
  • Fecha de corte. La define el conector según cómo lee la fuente. Un conector que consulta los lunes a las 02:00 puede enviar as_of = ese lunes y las ventas hasta el domingo. SOMA la guarda tal cual.
  • Ubicación declarada sin líneas (inventario, OC, OP): rechaza el lote, salvo que esté en empty_locations. En ventas es solo un aviso, porque los cierres y feriados son normales.
  • Lote viejo. Si extracted_at es anterior al del último lote aplicado sobre el mismo alcance, el lote se rechaza con batch_stale.

Líneas

Las líneas son estrictas: un campo que no existe (qty en lugar de quantity) es un error, no un dato que se ignora. Fechas en YYYY-MM-DD.

Ventas · sales

Campo Tipo Notas
location string obligatorio location_code o client_code de SOMA
sku string ≤ 100 obligatorio Código del cliente
date date obligatorio Día del movimiento
movement sale | return obligatorio La devolución se indica aquí, no con signo
quantity number > 0 obligatorio Siempre positiva
value_usd number ≥ 0 obligatorio Sin IVA; bruto o neto según la empresa
uom string opcional Sin uom = unidad base del SKU
lost_sales_qty number ≥ 0 opcional Venta perdida
original_sale_date date opcional Solo en return; no posterior a date
document_ref string ≤ 100 opcional Trazabilidad; no forma parte de la identidad

Inventario · inventory

Campo Tipo Notas
location string obligatorio
sku string ≤ 100 obligatorio
quantity_on_hand number ≥ 0 obligatorio Disponible: sin comprometido ni cuarentena
uom string opcional
value_usd number ≥ 0 opcional
lot, exp_date string, date opcional Juntos, para productos que vencen. Sin ellos, no vence

Órdenes de compra en tránsito · purchase_orders

Campo Tipo Notas
location string obligatorio Destino
po_number, line_ref string ≤ 100 obligatorio Orden y línea
sku string ≤ 100 obligatorio
quantity_pending number > 0 obligatorio Pedido menos recibido
expected_date date obligatorio Llegada esperada
uom, supplier_code, order_date opcional
soma_ref string opcional La aprobación de SOMA que originó la orden, para no contarla dos veces

Órdenes de producción en curso · production_orders

Campo Tipo Notas
location string obligatorio Planta
order_ref, sku string ≤ 100 obligatorio
quantity_pending number > 0 obligatorio Planificado menos producido
expected_completion_date date obligatorio
status released | in_progress obligatorio
uom, soma_ref opcional

No se envían órdenes en borrador, canceladas ni anuladas. Una orden que se cancela deja de venir y, por la regla de completitud, sale de SOMA.

Grano: cuándo dos líneas son la misma

Entidad Clave Líneas con la misma clave
Ventas ubicación, SKU, fecha, movimiento, fecha original Se suman, ya en unidad base. Da igual enviar líneas de factura o ya agregadas
Inventario ubicación, SKU, lote, vencimiento Se suman
OC ubicación, po_number, line_ref, SKU La segunda va a cuarentena (duplicate_key)
OP ubicación, order_ref, SKU La segunda va a cuarentena (duplicate_key)

SOMA guarda las ventas como una cifra neta por ubicación, SKU y día: ventas menos devoluciones, con lo devuelto registrado aparte. Al comparar, las cantidades se redondean a 4 decimales y los dólares a 2. Una diferencia menor cuenta como "sin cambio".

Códigos, unidades y valor

GET /v1/meta devuelve lo que el conector necesita para validar antes de enviar:

{
  "api_version": "1.0.0-draft",
  "period_unit": "W",
  "value_basis": null,
  "locations": [
    { "location_code": "CD01", "client_code": "ALM-CENTRAL", "name": "Centro de distribución" },
    { "location_code": "T001", "client_code": "TDA-NORTE", "name": "Tienda Norte" }
  ],
  "uom_conversions": [ { "uom": "DOC", "factor": 12, "sku": null } ]
}
  • Unidad de período de la empresa: D, W o M.
  • Ubicaciones. Se aceptan el código de SOMA o el del cliente. Una ubicación que SOMA no conoce no es error: sus líneas se guardan y el lote queda retenido (unknown_location) hasta que la empresa la defina. Mientras tanto no corre ningún motor de la empresa.
  • SKU. Se envía el código del cliente. Un SKU sin ficha entra igual y se informa en unmatched.skus. Los alias y los códigos ignorados se configuran una vez por empresa, no viajan en cada envío.
  • Unidades. Cada SKU tiene una unidad base, la más fina que se mueve. Con uom, SOMA convierte con el factor del SKU o, si no hay, con el de la empresa (sku: null). Una unidad sin conversión va a cuarentena (unknown_uom). Códigos en mayúsculas: DOC, KG, GAL.
  • Valor. Siempre en dólares y sin IVA. Bruto o neto de descuentos lo decide la empresa una vez (value_basis: null = todavía sin declarar). El primer lote de ventas que trae value_basis lo fija. Sin declarar en ningún lado, el lote se rechaza (value_basis_undeclared); distinto al de la empresa, value_basis_mismatch.

Validación y estados

Nivel Qué pasa Ejemplos
Línea Va a cuarentena con su motivo; el resto del lote sigue Fecha inválida, cantidad cero, campo mal escrito, unidad desconocida
Lote: rechazo No se aplica nada y SOMA conserva los datos anteriores Conteos que no calzan, línea fuera del alcance, cuarentena sobre el 1%, lote viejo
Lote: retención Espera una decisión de la empresa en la revisión de datos; al liberarlo se aplica sin reenviar Ubicación desconocida, borraría más de lo normal, primer lote sobre datos cargados por archivo
Estado Significado
open Recibiendo partes
validating Confirmado, procesándose
applied Aplicado completo
applied_with_quarantine Aplicado; algunas líneas apartadas
held Retenido para revisión
rejected Rechazado; nada cambió
expired Abierto más de 24 h sin confirmar

Protección contra borrados

Antes de aplicar, SOMA calcula cuánto borraría el lote dentro de su alcance. Si supera el umbral de la empresa (valores iniciales: 2% de las filas o 0,5% del valor), el lote queda retenido con deletion_guard_tripped y el detalle de lo que borraría.

Los códigos de error de cada nivel están en el catálogo de errores.

Límites

Límite Valor Al pasarlo
Líneas por parte 10.000 413 payload_too_large
Tamaño de una parte 5 MB 413 payload_too_large
Requests por clave 60 por minuto 429 rate_limited con Retry-After
Lote abierto sin confirmar 24 horas 410 batch_expired
Historial por página 1 a 200 (50 por omisión) 422

Son valores iniciales y pueden subir. Una carga inicial grande (cientos de miles de líneas) se envía en un solo lote con muchas partes.

Versionado

  • La versión mayor va en la URL: /v1/….
  • Dentro de v1 solo se agrega: rutas nuevas, campos opcionales de entrada, campos nuevos en las respuestas, valores nuevos en enums de respuesta y códigos de error nuevos.
  • Lector tolerante. El conector ignora los campos que no conoce en las respuestas, trata un estado de lote desconocido como "todavía no terminado" y un código desconocido como error genérico de su nivel.
  • Qué es un cambio que rompe: quitar o renombrar un campo, volver obligatorio uno opcional, cambiar tipo, unidad o significado, cambiar un código de error o una regla de completitud o de diferencia. Eso es una /v2, aunque el JSON quede igual.
  • Convivencia. v1 y v2 funcionan juntas al menos seis meses. Durante ese tiempo v1 responde con los headers Deprecation y Sunset, este último con la fecha de apagado.
  • Trazabilidad. Cada lote guarda la versión de la API con que llegó y el header SOMA-Connector.
  • Changelog y releases. Cada cambio del contrato queda en el changelog y cada versión es un release con sus notas.

Certificar un conector

Antes de recibir la clave de producción, el conector pasa por el ambiente de pruebas:

  • Lee /v1/meta y valida localmente ubicaciones y unidades antes de enviar.
  • Comprueba que leyó la fuente completa (conteos por día o por ubicación) y aborta si no puede comprobarlo. Envía partition_counts.
  • Genera un batch_id único por envío y lo reutiliza solo al reintentar ese mismo envío.
  • Reintenta con espera ante 429 y errores de red. No reintenta 401, 403 ni 422.
  • Corre un dry-run de varias semanas reales y la empresa valida las cifras contra lo que conoce.
  • Maneja los resultados held y rejected: los registra y avisa, sin reenviar en bucle.
  • Se identifica con SOMA-Connector y nunca escribe la clave en logs.