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_atlleva zona horaria y es el momento en que se leyó la fuente.partition_countses opcional. Si viene, tiene que sumarexpected_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_statuses el estado que tendría el lote si se confirmara.problemslista todos los códigos encontrados, del más grave al menos grave;problem_codees el primero.unmatchedcuenta los SKUs y las ubicaciones que SOMA no conoce.quarantine_sampletrae 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_ates anterior al del último lote aplicado sobre el mismo alcance, el lote se rechaza conbatch_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,WoM. - 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 traevalue_basislo 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
DeprecationySunset, 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/metay 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
429y errores de red. No reintenta401,403ni422. - Corre un
dry-runde varias semanas reales y la empresa valida las cifras contra lo que conoce. - Maneja los resultados
heldyrejected: los registra y avisa, sin reenviar en bucle. - Se identifica con
SOMA-Connectory nunca escribe la clave en logs.