API Core Documentación
Visión general
Autenticación, envío de jobs, resultados, catálogo de engines, webhooks y canales de notificación.
Esta referencia cubre 12 endpoints. Cada endpoint trae ruta, permiso, parámetros, respuesta y los errores posibles.
Crear cuenta
Propósito
Alta self-service de evaluación: crea la cuenta de quien la solicita y el tenant de evaluación detrás de ella, y devuelve una clave de API ya funcionando. Es la única escritura anónima de esta superficie - existe para crear el propio tenant que todas las demás rutas autentican.
X-PLATFORM-KEY
Obligatorio. Debe coincidir con la clave pública del despliegue; sin él, o con un valor distinto, la respuesta es 401 y no se crea nada. Es un filtro que mantiene a los escáneres y a las altas automatizadas fuera de una ruta que por lo demás está abierta, no autenticación: el valor viaja dentro del bundle del front-end, así que trátalo como público.
Las defensas reales son otras y no han cambiado: la ruta solo existe donde el alta self-service está configurada (404 en caso contrario), los intentos se limitan por dirección de origen por hora (los rechazados incluidos, y una clave de plataforma equivocada cuenta), y el rol de base de datos detrás de ella tiene INSERT sin SELECT. Un despliegue sin clave pública configurada rechaza toda alta con 503 en lugar de aceptarlas sin filtro.
Campos
Solo name, email, password y accepted_terms son obligatorios. company_name es el discriminador: presente, la cuenta es de una empresa; ausente, la persona es la cuenta.
| Campo | Tipo | Obligatorio | Significado |
|---|---|---|---|
name | string | sí | Nombre de quien crea la cuenta |
email | string | sí | Correo de la cuenta, también el identificador de acceso |
password | string | sí | Contraseña inicial; la política es la del proveedor de identidad |
accepted_terms | boolean | sí | Debe ser true; false responde 400 |
company_name | string | no | El discriminador de cuenta empresarial |
phone | string | no | Teléfono de contacto |
country | string | no | País |
website | string | no | Sitio web |
tax_id | string | no | Identificación fiscal |
users_count | entero | no | Cantidad estimada de usuarios |
purpose | string | no | Uso previsto de la plataforma |
source | string | no | Origen del alta, para atribución |
Respuesta
201 con {"customer_uuid": "...", "api_key": "...", "trial_ends_at": "...", "plan": {...}}.
La api_key vuelve una sola vez. Solo se almacena un digest con clave, y nada la recupera después: guárdala en el momento en que aparece. Para reemplazarla, POST /api/v1/tenant/api-key/rotate.
plan trae el código del plan de evaluación y sus límites (max_jobs_per_day, max_requests_per_min, max_upload_mb, retention_days), omitiendo los que sean ilimitados.
Errores
| Estado | Cuándo |
|---|---|
400 | Un campo falló la validación, no se aceptaron los términos, o la contraseña fue rechazada por la política |
401 | X-PLATFORM-KEY ausente o distinto |
404 | El despliegue no configuró el alta self-service |
409 | Este correo ya tiene cuenta |
429 | Ventana horaria de intentos agotada |
503 | El despliegue no configuró la clave pública |
Ruta anónima, filtrada por la clave pública de la plataforma. El campo ya viene con la clave de este portal.
curl -X POST "https://intelligence.echosistema.live/api/v1/register" \
-H "X-PLATFORM-KEY: eb7f5270-9b15-3d54-9bad-9f1b15489295" \
-H "Content-Type: application/json" \
-d '{ "name": "Agrias Oaks", "email": "agrias@empresa.exemplo", "password": "trocar-esta-senha-1", "accepted_terms": true, "company_name": "Empresa Exemplo", "country": "BR", "website": "https://empresa.exemplo", "tax_id": "00.000.000/0001-00", "users_count": 12, "purpose": "Avaliar os engines de previsão de demanda" }'Datos de la empresa
Propósito
El registro comercial de la propia empresa, tal como fue dado de alta: quién la registró, para qué, y cómo contactarlo.
Es distinto de GET /api/v1/me, que trae los derechos de la empresa - plan, cuotas, límites - y distinto de Equipo de la empresa, que trae las personas.
Campos
| Campo | Tipo | Significado |
|---|---|---|
customer_uuid | string | La empresa a la que pertenece este registro |
company_name | string | Razón social. Ausente cuando la cuenta es la propia persona |
contact_name | string | Nombre de quien registró |
email | string | El correo usado en el alta |
phone, country, website, tax_id | string | Como fueron informados, cuando lo fueron |
users_count | number | Cantidad de personas declarada |
purpose | string | Finalidad declarada |
source | string | De dónde vino el alta |
accepted_terms_at | string | Cuándo se aceptaron los términos |
registered_at | string | Cuándo se escribió el registro |
registered_by | objeto | user_id siempre; email y name cuando el proveedor aún tiene a la persona |
Lo que esta ruta nunca devuelve
La dirección de red y el navegador del alta. Existen para responder si un alta fue un script, y el rol de base detrás de esta ruta no tiene permiso de lectura sobre esas columnas - una consulta que las pidiera sería rechazada por Postgres antes de que existiera respuesta. No es una omisión de quien escribió el handler; es un permiso que no existe.
Errores
| Status | Cuándo |
|---|---|
401 | Sin token, o con una API key |
403 | Quien llama no administra esta empresa, o está inactiva |
404 | Esta empresa no tiene registro comercial |
500 | Fallo de dependencia |
404 es una respuesta legítima, no un error de implementación. Una empresa aprovisionada fuera del alta self-service nunca completó ese registro. Su perfil es el bloque organization de GET /api/v1/me.
Editar empresa
Propósito
Edita el perfil de la propia empresa de quien llama. La empresa viene del token: no existe parámetro de tenant y, por lo tanto, no existe camino para editar otra.
Una clave de API se rechaza con 401 - nombra a una empresa, no a una persona, y no prueba quién está autorizado a editarla. Sin el rol company-<tenant>-administrator en el token, 403.
Campos editables
Envía solo lo que cambia. Una cadena vacía limpia un campo opcional; name no puede vaciarse.
| Campo | Tipo | Nota |
|---|---|---|
name | string | No acepta cadena vacía |
country | string | La cadena vacía limpia |
website | string | La cadena vacía limpia |
tax_id | string | La cadena vacía limpia |
users_count | entero | |
purpose | string | La cadena vacía limpia |
Contactos y personas no son editables aquí: el dato de persona pertenece al proveedor de identidad.
Renombrar
Cambiar el nombre también renombra la organización en el proveedor de identidad, para que ambos lados coincidan. Un nombre que otra organización ya ocupa responde 409 y no se altera nada.
Respuesta
200 con el mismo envoltorio de GET /api/v1/me, releído después de la escritura.
Errores
| Estado | Cuándo |
|---|---|
400 | Un campo falló la validación, o name vino vacío |
401 | Sin token, token inválido, o se presentó una clave de API |
403 | El token no lleva el rol de administrador de esta empresa |
409 | Otra organización ya ocupa ese nombre |
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X PATCH "https://intelligence.echosistema.live/api/v1/company" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Empresa Exemplo S.A.", "website": "https://empresa.exemplo" }'Equipo de la empresa
Propósito
Quién forma parte de la propia empresa: todos los que son miembros de su Organization en el proveedor de identidad, con los roles de empresa que tiene cada uno.
La empresa viene del token y no puede nombrarse en la petición - esta ruta solo sabe responder por la empresa de quien llama.
Campos de cada persona
| Campo | Tipo | Significado |
|---|---|---|
id | string | El id de la persona en el proveedor de identidad |
email | string | El correo de acceso, cuando el proveedor tiene uno |
name | string | El nombre, cuando el proveedor tiene uno |
enabled | boolean | Si el proveedor la dejaría entrar |
email_verified | boolean | Si el proveedor considera el correo comprobado |
roles | array | Roles de empresa: administrator, supervisor, coordinator, support, developer |
email y name se omiten cuando el proveedor no los tiene - nunca vuelven nulos. Una persona sin nombre en el proveedor aparece por su correo.
Quién aparece
Quien es miembro de la Organization. Alguien con un rol company-<empresa>-* que fue retirado de la Organization no aparece: eso es residuo de un rol concedido con una pertenencia deshecha, y listarlo respondería "¿quién es de esta empresa?" con quien no lo es.
Un miembro sin ningún rol vuelve con roles: []. Y solo administrator tiene autoridad en esta API - los otros cuatro son etiquetas que lee el producto, y ninguna ruta los verifica.
Límite
El listado se detiene en 200 personas. No hay paginación: una empresa es un equipo, no un directorio.
Errores
| Status | Cuándo |
|---|---|
401 | Sin token, o con una API key |
403 | Quien llama no administra esta empresa, o está inactiva |
503 | Proveedor inaccesible, o entorno sin credencial de administración |
404 | El proveedor de identidad no tiene organización para esta empresa |
Los datos se leen del proveedor en cada llamada - este servicio no guarda personas. Por eso un proveedor caído responde 503, y no una lista vacía que se leería como "la empresa no tiene a nadie".
Añadir persona
Propósito
Añade una persona a tu propia empresa: la crea en el proveedor de identidad, la hace miembro de la Organization de la empresa y le concede un rol.
Campos
| Campo | Tipo | Significado |
|---|---|---|
name | string | Su nombre; se acepta un nombre único |
email | string | Será su usuario en el proveedor, y es la clave de unicidad |
password | string | Valor de primer acceso - ver abajo |
role | string | master, administrator, supervisor, coordinator, support o developer |
El rol se convierte en company-<tenant>-<rol> en el proveedor, la forma que ya usa el resto de la plataforma.
Solo master y administrator tienen autoridad
Son ellos los que permiten añadir, quitar y re-rolar personas, editar la empresa y rotar la clave; master puede todo lo que puede administrator. Los otros cuatro son etiquetas que lee el producto: ninguna ruta los comprueba hoy.
El conjunto es cerrado del lado de la API y GET /api/v1/company/roles lo devuelve entero, con la antigüedad (rank) y quién tiene autoridad (administers). Arma el selector a partir de ahí: una lista repetida en el cliente diverge el día en que aparezca un séptimo rol.
La contraseña es de traspaso
La eligió otra persona y viajó por un tercero hasta llegar. Por eso el usuario nace con el cambio de contraseña pendiente en el proveedor: ese valor funciona una sola vez, y la persona define la suya en el primer inicio de sesión. También por eso esta ruta no depende de tener correo configurado.
Respuesta
201 con id, email y el rol concedido por extenso.
Errores
| Status | Cuándo |
|---|---|
400 | Un campo vacío, demasiado largo, o una contraseña que el proveedor rechazó |
401 | Sin token, o se presentó una API key |
403 | Quien llama no administra esta empresa |
409 | Ese correo ya tiene cuenta |
Un fallo a medio camino elimina lo ya creado, así que reintentar con el mismo correo es la recuperación soportada.
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X POST "https://intelligence.echosistema.live/api/v1/company/users" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Ramza Beoulve", "email": "ramza@empresa.exemplo", "password": "trocar-esta-senha-3", "role": "supervisor" }'Rotar clave de API
Propósito
Emite una clave de API nueva para la empresa autenticada y la devuelve una sola vez. La clave anterior dejó de funcionar antes de que esta respuesta se escribiera.
La única ruta cuya credencial no es intercambiable
Cualquier otra ruta autenticada acepta X-TENANT-KEY o Authorization: Bearer. Esta acepta solo el token, y rechaza la clave de forma explícita en lugar de simplemente ignorarla: la clave es lo que se está reemplazando, así que honrarla dejaría que quien tiene una clave filtrada la rotara y dejara fuera al dueño real. Reemplazar un secreto se prueba por identidad, nunca por el secreto.
El rol exigido es company-<customer_uuid>-administrator para la misma empresa que el token resuelve: un administrador válido de otra empresa recibe 403.
Respuesta
200 con {"customer_uuid": "...", "api_key": "...", "rotated_at": "..."}.
La api_key aparece aquí y en ningún otro lugar - solo se almacena un digest con clave.
Errores
| Estado | Cuándo |
|---|---|
401 | Sin token, token inválido, o se presentó una clave de API |
403 | El token no lleva el rol de administrador de esta empresa |
404 | El despliegue no configuró el alta self-service |
429 | Ventana de peticiones por minuto agotada |
500 | Fallo de dependencia; el motivo se registra, nunca se devuelve |
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X POST "https://intelligence.echosistema.live/api/v1/tenant/api-key/rotate" \
-H "Authorization: Bearer $ACCESS_TOKEN"Iniciar sesión
Propósito
Cambia e-mail y contraseña por la sesión del proveedor de identidad. El token devuelto es el del propio proveedor, sin modificar: es el mismo que verifican todas las demás rutas.
Dos modos, elegidos por la cabecera X-PLATFORM-KEY
| Modo | Cuándo | Qué vuelve |
|---|---|---|
token | Envías una platform key válida | access_token y refresh_token en el cuerpo |
cookie | No envías platform key | Cookies HttpOnly; el cuerpo no trae token |
El modo token es para clientes que pueden guardar una credencial: app nativa, integración server-side, script. El modo cookie (BFF) es para el navegador, que no puede: lo que el script lee, un XSS también.
Una key incorrecta no es ninguno de los dos - responde 401. Caer al modo cookie haría inútil la cabecera, ya que quien quisiera el modo seguro solo tendría que omitirla. Lee el campo session para saber qué modo te tocó.
Campos
| Campo | Tipo | Significado |
|---|---|---|
email | string | El e-mail de la persona, que es su usuario en el proveedor |
password | string | Su contraseña |
Respuesta
200 con session, expires_in, user, companies y, cuando hay una empresa que describir, tenant. En modo token, además access_token, refresh_token y token_type.
El bloque user trae nombre, e-mail y el avatar definido con PATCH /api/v1/me, leídos de los claims del propio token. El bloque tenant es el mismo envelope de GET /api/v1/me - y los límites salen de la base de datos, nunca de un claim.
companies siempre viene; tenant puede faltar
companies lista todas las empresas de la persona que este entorno atiende, con una entrada en el caso común. tenant falta cuando pertenece a más de una: no hay empresa que describir antes de que una sea elegida, y elegir aquí sería elegir a quién servirle los datos.
La regla es la misma de todas las otras rutas: el bloque está presente cuando una solicitud sin la cabecera X-COMPANY resolvería una empresa, y ausente cuando preguntaría cuál. Quien pertenece a una sola no ve ninguna diferencia - pero el código ya no puede asumir que tenant existe.
El modo cookie asume same-site
Las cookies llevan Path=/ y ningún Domain: el front-end y la API deben ser un mismo sitio, con un proxy inverso delante de ambos. Un front-end en otro dominio nunca las envía y debe usar el modo token. SameSite=Lax es la defensa CSRF - un POST cross-site no lleva cookie alguna.
Errores
| Status | Cuándo |
|---|---|
400 | Un campo vacío o demasiado largo |
401 | Credenciales rechazadas, o X-PLATFORM-KEY presente e incorrecta |
403 | La empresa está inactiva o suspendida |
429 | Ventana horaria de intentos agotada para esta dirección |
503 | Proveedor de identidad inalcanzable, o sin client de login configurado |
E-mail incorrecto, contraseña incorrecta y cuenta deshabilitada responden el mismo 401: distinguirlos convertiría esta ruta en una forma de averiguar qué e-mails tienen cuenta. La contraseña nunca la guarda este servicio.
Ruta anónima, filtrada por la clave pública de la plataforma. El campo ya viene con la clave de este portal.
curl -X POST "https://intelligence.echosistema.live/api/v1/auth/login" \
-H "X-PLATFORM-KEY: eb7f5270-9b15-3d54-9bad-9f1b15489295" \
-H "Content-Type: application/json" \
-d '{ "email": "agrias@empresa.exemplo", "password": "trocar-esta-senha-1" }'Renovar sesión
Propósito
Renueva la sesión sin volver a pedir la contraseña, respondiendo en el mismo modo en que fue llamada: con refresh_token en el cuerpo, los nuevos tokens vuelven en el cuerpo; con el cuerpo vacío, se reemplazan las cookies de sesión.
No hace falta platform key - el refresh token es la credencial, y un navegador en modo cookie no tiene cabecera que enviar.
Campos
| Campo | Tipo | Significado |
|---|---|---|
refresh_token | string | Solo en modo token; omítelo en modo cookie |
Errores
| Status | Cuándo |
|---|---|
401 | Sin refresh token, o el proveedor lo rechazó - inicia sesión de nuevo |
403 | La empresa fue suspendida desde el inicio de sesión |
503 | Proveedor inalcanzable, o sin client de login configurado |
La empresa se vuelve a comprobar en cada renovación, así que una suspensión termina la sesión en la siguiente en lugar de aparecer después como un fallo inexplicable.
Cerrar sesión
Propósito
Termina la sesión. Las cookies se limpian en esta respuesta siempre; al proveedor se le pide terminar la suya en modo best effort, de modo que un proveedor inalcanzable nunca bloquea el cierre.
Campos
| Campo | Tipo | Significado |
|---|---|---|
refresh_token | string | Solo en modo token, para terminar también la sesión del proveedor |
En modo token, descarta el access token por tu lado - eso no puede hacerlo el servidor por ti.
Respuesta
204 No Content, incluso cuando no había sesión: cerrar sesión de nada no es un error, y rechazarlo dejaría a alguien con el token ya expirado sin poder limpiar su propio navegador.
Validar credencial
Propósito
Valida la credencial presentada y devuelve la identidad y los entitlements del llamante. No se emite ningún token - la emisión de tokens corresponde al proveedor de identidad. Los secretos (ej.: webhook_secret) nunca se incluyen en la respuesta.
Cuando la credencial es un token - de la cabecera o de la cookie de sesión - la respuesta trae además el bloque user, con la persona detrás de él. Ver Identidad actual, donde el bloque está descrito por completo.
Los tres métodos de autenticación
Las credenciales se prueban en un orden fijo, sin fallthrough: una credencial presentada que falla produce 401, nunca un reintento con el método siguiente.
- API key (
X-TENANT-KEY) - el servidor almacena solohex(HMAC-SHA256(key, pepper))y la resuelve con una única consulta indexada O(1) más una comparación en tiempo constante. Las claves nunca se almacenan ni se registran en claro. - JWT (
Authorization: Bearer) - la misma ruta de verificación sirve para tokens de usuario y tokens de máquina (client_credentials). El token debe serRS256(fijado:alg:noney HS256 se rechazan), firmado por una clave del JWKS del realm, coniss,expy, cuando está configurado,audválidos. Solo se lee la claimorganization- la empresa *es* el id de la organización en el proveedor. Los entitlements vienen SIEMPRE de la base de datos en cada request; las claims que falsifican entitlements se ignoran. - Empresa activa (
X-COMPANY) - no es un cuarto método, sino lo que dice para qué empresa es la petición cuando la persona pertenece a más de una. Con una empresa se resuelve sola y la cabecera sobra; con varias y sin la cabecera, la respuesta es400pidiendo que nombre una. La cabecera estrecha, nunca concede: una empresa fuera del token es403, como si hubiera sido inventada. Ver Empresas de la persona. - Token de operador (
X-Admin-Token) - concede únicamente la vista de operador deGET /api/v1/platform/health, sin acceso a datos de tenant. Queda totalmente deshabilitado cuando la variable correspondiente no está definida en el deploy.
Envelope uniforme de fallo
| Status | Cuerpo |
|---|---|
401 | {"error": "missing credentials"} o {"error": "invalid credentials"} |
403 | {"error": "tenant inactive or suspended"} |
429 | {"error": "rate limit exceeded"} |
Respuesta
200 con {"authenticated": true, "tenant_id": "...", "subscription_code": "...", "allowed_engines": null, ...}. allowed_engines: null significa todas las engines del catálogo.
Entitlements
Leídos en cada request desde la base de datos (JOIN de tenants con suscripciones), con caché en memoria breve (15s por defecto). Nunca desde una claim del token.
| Campo | Origen | Significado |
|---|---|---|
allowed_engines | suscripción | Allowlist de engines; NULL = todas las catalogadas |
max_jobs_per_day | suscripción | Cuota diaria de envíos; NULL = ilimitado |
max_requests_per_min | suscripción | Cuota de requests por minuto; NULL = ilimitado |
max_upload_mb | suscripción | Techo de tamaño de subida por envío |
retention_days | suscripción | Ventana de retención de resultados no efímeros |
webhook_url / webhook_secret / webhook_events | tenant | Configuración del webhook de finalización |
default_result_mode | tenant | Configuración legada de otro flujo de integración; los envíos por la API corren siempre en modo queue (este servicio es su almacenamiento) |
ip_allowlist | tenant | Presente en el esquema pero NO aplicado en esta versión. No confíes en él. |
curl -X GET "https://intelligence.echosistema.live/api/v1/auth" \
-H "X-TENANT-KEY: $TENANT_KEY"Empresas de la persona
Propósito
La única ruta que responde antes de que haya empresa elegida. Todo lo demás en esta API sirve los datos de una empresa y por eso necesita saber cuál; esta dice entre cuáles elegir.
Una persona puede ser miembro de varias empresas. El token las nombra todas, y cada solicitud sirve una: cuál de ellas va en la cabecera X-COMPANY, cuyo valor es el customer_uuid de una de estas entradas.
La cabecera X-COMPANY
| Situación | Respuesta |
|---|---|
| Una empresa, sin la cabecera | Sirve esa - nada cambia para quien ya integra |
| Varias empresas, sin la cabecera | 400 pidiendo que nombres una |
| Cabecera con una empresa que el token nombra | Sirve esa |
| Cabecera con cualquier otra | 403 |
La cabecera estrecha, nunca concede. Elige entre lo que la credencial ya probó; una empresa fuera del token se rechaza como si hubiera sido inventada.
Campos de cada entrada
| Campo | Tipo | Significado |
|---|---|---|
customer_uuid | string | El valor a enviar en X-COMPANY en las llamadas siguientes |
name | string | Nombre de la empresa, cuando un alta dejó uno |
active | boolean | Empresa y suscripción vivas. Una suspendida se lista, no se esconde |
subscription_code | string | Código del plan |
is_administrator | boolean | Si esta persona administra esta empresa |
Respuesta
200 con {"companies": [...], "count": n}.
Lista lo que este entorno puede servir, no todo lo que el proveedor de identidad conoce: alguien puede pertenecer a una organización para la que no hay empresa aquí, y ofrecerla sería ofrecer una elección que falla en la llamada siguiente.
is_administrator viene de los roles del propio token, los mismos que las rutas de administración verifican. Se informa temprano para que un cliente dibuje la pantalla correcta, en vez de descubrir la respuesta en un 403.
Errores
| Status | Cuándo |
|---|---|
401 | Sin credencial, o con una que no es un token de persona |
500 | Falló la lectura de los derechos de las empresas |
Identidad actual
Propósito
La misma identidad de GET /api/v1/auth. Solo devuelve el contexto del propio llamante - no existe ruta cross-tenant - y rechaza a un llamante anónimo con 401, como cualquier ruta autenticada.
El bloque user
Ambas rutas traen un bloque user cuando la credencial es un token, ya sea de la cabecera Authorization o de la cookie de sesión:
{
"authenticated": true,
"tenant_id": "...",
"subscription_code": "trial",
"user": {
"id": "8f1c...",
"name": "Agrias Oaks",
"email": "agrias@empresa.exemplo",
"avatar_url": "https://storage.echosistema.live/live/avatar/exemplo.png"
}
}| Campo | Cuándo |
|---|---|
id | Siempre presente en el bloque - el id de la persona en el proveedor de identidad |
name, email | Cuando el proveedor emite el claim |
avatar_url | Cuando se definió un avatar con PATCH /api/v1/me |
Así retoma la sesión un navegador. Quien entró en modo cookie no guarda nada que sus propios scripts puedan leer; llamar a esta ruta tras un reload es como averigua si la sesión sobrevivió y de quién es.
Con X-TENANT-KEY el bloque está ausente, y esa ausencia es información y no un hueco: una key nombra una empresa y ninguna persona. Todo en él se relaya de los claims del propio token - este servicio no guarda nada sobre la persona más allá de la organización a la que pertenece.
curl -X GET "https://intelligence.echosistema.live/api/v1/me" \
-H "X-TENANT-KEY: $TENANT_KEY"Actualizar perfil
Propósito
Edita el perfil de la persona autenticada: avatar, idioma, moneda y género. Solo acepta token - una clave de API nombra a una empresa, no a una persona, y se rechaza con 401.
Cada campo se convierte en atributo de la persona en el proveedor de identidad, de modo que el siguiente token de acceso ya lo lleva como claim (picture, language, currency, gender). Este servicio no almacena nada sobre la persona.
Campos
| Campo | Tipo | Significado |
|---|---|---|
avatar_url | string | URL absoluta http(s) del avatar |
language | string | Etiqueta de idioma de hasta 5 caracteres, normalizada a pt-BR |
currency | string | Hasta 3 letras, guardada en mayúsculas (pyg pasa a PYG) |
gender | string | m, f u o, en cualquier caja |
Parcial y campo a campo
Un campo ausente o null no toca lo que había, "" elimina el atributo, y un valor lo reemplaza. uuid no es editable: enviarlo se rechaza con 400, no se ignora en silencio.
Solo se comprueba la forma, nunca el significado: la URL del avatar debe ser absoluta http(s) y nunca es descargada por este servicio; idioma y moneda se normalizan, no se consultan en ninguna tabla.
El claim solo cambia con un token nuevo
El token que ya tienes sigue llevando los valores anteriores. Después de editar, renueva la sesión (POST /api/v1/auth/refresh) para leerlos de vuelta - GET /api/v1/me describe la empresa y la persona, no estas preferencias.
Respuesta
204 No Content.
Errores
| Estado | Cuándo |
|---|---|
400 | Un campo no pasó la comprobación de forma, o llegó un campo desconocido |
401 | Sin token, token inválido, o se presentó una clave de API |
404 | El proveedor de identidad ya no tiene a esta persona |
503 | El proveedor no declaró uno de los atributos; es un cambio de configuración suyo, no un error de cliente |
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X PATCH "https://intelligence.echosistema.live/api/v1/me" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "avatar_url": "https://storage.echosistema.live/live/avatar/exemplo.png" }'Cambiar contraseña
Propósito
Reemplaza la contraseña de la persona autenticada en el proveedor de identidad. Solo acepta token, por la misma razón que PATCH /api/v1/me.
Campos
| Campo | Tipo | Significado |
|---|---|---|
new_password | string | La contraseña nueva |
No se pide la contraseña actual
Un token válido es lo que autoriza el cambio, así que el tiempo de vida de token definido en el proveedor es lo que limita la exposición de un token robado. La política de contraseñas es la del proveedor - un rechazo vuelve como 400. La contraseña nunca llega a la base de datos de este servicio.
Respuesta
204 No Content.
Errores
| Estado | Cuándo |
|---|---|
400 | El proveedor rechazó la contraseña por su política |
401 | Sin token, token inválido, o se presentó una clave de API |
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X POST "https://intelligence.echosistema.live/api/v1/me/password" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "new_password": "trocar-esta-senha-2" }'Enviar job
Propósito
Solicita el procesamiento de los datos enviados por {engine} (la clave de la engine viene del path). customer_uuid viene siempre de la credencial - cualquier valor en el cuerpo se ignora, igual que un campo engine en el cuerpo.
Formas de envío
application/jsoncon filas inline:{"params": {...}, "data": [...]}.multipart/form-datacon un archivo.json/.csv/.parquet(detectado por magic bytes, nunca por el nombre) y un campo JSONparams; los campos extra del form se fusionan enparamscomo metadata.
Modos
Flags por query string (?sync=true, ?dry_run=true) o campos del cuerpo JSON. dry_run=true gana sobre sync=true.
| Modo | Comportamiento | Éxito |
|---|---|---|
| asíncrono (por defecto) | Sube el input al almacenamiento, publica en la cola de la engine, retorna de inmediato | 202 {"process_uuid", "result_url"} |
sync | El mismo publish más una espera directa acotada por el timeout del deploy (60s por defecto) | 200 {"process_uuid", "result"} |
dry_run | Totalmente efímero: nada se persiste en el almacenamiento de entrada, respuesta esperada | 200 {"process_uuid", "result"} |
En un timeout de sync/dry_run la respuesta es 504 {"process_uuid": "..."} - guarda el process_uuid: el resultado aún puede llegar y consultarse en GET /api/v1/results/{process_uuid} (para dry_run esa consulta es de lectura única; ver burn-on-read).
Clasificación playground
playground=true marca la submisión como prueba - es clasificación, no modo: el job ejecuta exactamente igual (misma cola, mismo worker, misma precedencia dry_run > sync) y cuenta como uso en el reporte y el medidor; solo dry_run queda fuera. Lo que cambia es el registro: el listado de resultados puede entonces separar prueba de producción con el filtro playground.
Se acepta en los tres lugares, con el mismo patrón de sync/dry_run: query string (?playground=true), campo del cuerpo JSON y campo del multipart. El cuerpo gana sobre la query cuando ambos están presentes; ausente, vale false. La consola de esta documentación envía playground=true siempre, sin opción de apagarlo: lo que nace aquí es una prueba, y no contamina el registro de producción.
Engines aceptadas
El segmento {engine} del path debe ser una de las 105 engines del catálogo de la plataforma (11 grupos, ver GET /api/v1/catalog/engines). Las claves de engine del catálogo son la nomenclatura pública - enviar una clave interna responde 422, exactamente como una engine desconocida. Cinco engines de observabilidad, tracking_pages, tracking_sessions, tracking_visitors, edge_traffic y log_analyzer, se sirven bajo una clave pública distinta de la interna, y algunos campos de payload siguen la misma regla (first_tracking_id, last_tracking_id, tracking_id, log_query): la traducción se aplica en ambos bordes, params y el data inline se reescriben en el envío y los resultados vuelven ya en la nomenclatura pública. El contenido de un archivo subido pasa byte a byte y nunca se reescribe.
Orden de validación
Fail-fast: un envío rechazado no tiene ningún efecto secundario - nada se sube, nada se publica.
{engine}existe en el catálogo y está activa, si no422.{engine}está enallowed_enginesde la suscripción (NULL= todas), si no403.- La validación de
paramsse delega al worker en tiempo de ejecución. - Subida dentro de
max_upload_mb, si no413. - Cuota diaria de jobs no excedida, si no
429.
Un cuerpo malformado responde 400.
Idempotencia
process_uuid es determinístico (UUID v5 del hash del contenido + engine + params canónicos, con namespace por tenant): reenviar contenido idéntico produce el mismo id, y la ingestión converge de forma idempotente.
Catálogo detallado por grupo
Cada engine de abajo acepta exactamente este endpoint (POST /api/v1/{engine}/process) con su propio contrato de params y data[]; el link abre la documentación completa, con tablas de campos y consola de ejemplo.
Analytics (15)
| Engine | Clave | Qué hace |
|---|---|---|
| Test A/B | ab_test | Significancia de test A/B: z-test de dos proporciones o t-test de Welch, con análisis de poder. |
| Test A/B (rápido) | ab_test_fast | Significancia de test A/B: z-test de dos proporciones (t-test continuo opcional). |
| Scorecard de afiliados | affiliate_scorecard | Scorecard multidimensional de desempeño de afiliados/socios, con clasificación en tiers. |
| Atribución por canal | channel_attribution | Atribución de canales de marketing con modelos last-touch, first-touch, linear o position-based. |
| Embudo de conversión | funnel | Embudo de conversión estricto: conversión por etapa, abandono, usuarios perdidos y cuellos de botella. |
| Jornada (Markov) | journey_markov | Análisis de matriz de transición en cadena de Markov sobre secuencias de etapas de jornada. |
| Jornada (Markov, rápido) | journey_markov_fast | Análisis rápido de transiciones en cadena de Markov sobre secuencias de etapas de jornada. |
| Secuencias de jornada | journey_sequences | Minería de caminos frecuentes de jornada vía árbol de prefijos (Trie), con conversión por camino. |
| Atribución de marketing | marketing_attribution | Atribución de canales de marketing con modelos last-touch, first-touch, linear o position-based. |
| Indicadores móviles | moving_stats | Indicadores de media móvil y volatilidad (SMA, EMA, WMA, Bollinger, ROC, YoY/MoM, CAGR) sobre una serie temporal. |
| Pareto & concentración | pareto | Análisis de Pareto/concentración: coeficiente de Gini, HHI, curva de Lorenz, punto 80/20 y clasificación ABC. |
| Pipeline de engines | pipeline | Meta-engine que encadena varias engines en secuencia sobre el mismo conjunto de datos. |
| Estacionalidad (profunda) | seasonality_deep | Detección de estacionalidad por FFT con aceleración dedicada y descomposición tendencia/estacional/residuo. |
| Estacionalidad (FFT) | seasonality_fft | Detección de estacionalidad por FFT con descomposición en tendencia, estacional y residuo. |
| Atribución de Shapley | shapley_attribution | Atribución de marketing multi-touch con valores de Shapley (teoría de juegos). |
Inteligencia de Clientes (22)
| Engine | Clave | Qué hace |
|---|---|---|
| Churn (profundo) | churn_deep | Predicción de churn multi-modelo con aceleración dedicada (MLP, TabNet, Random Forest, XGBoost) y horizontes temporales. |
| Etiqueta de churn | churn_label | Etiquetado de churn por reglas determinísticas en tres modelos de negocio: ecommerce, saas, prepaid. |
| Predicción de churn | churn_prediction | Clasificación de churn con detección automática de features, selección de algoritmo por validación cruzada y métricas solo de holdout. |
| CLV (profundo) | clv_deep | Predicción de Customer Lifetime Value multi-modelo con aceleración dedicada (LSTM, MLP, XGBoost) en múltiples horizontes. |
| Análisis de cohortes | cohort_analysis | Triángulo de retención por cohorte de adquisición (mensual o semanal). |
| Retención por cohorte | cohort_retention | Matriz de retención y tasas de churn por cohorte de adquisición (día, semana o mes). |
| Cliente 360 | customer_360 | Perfil compuesto del cliente que fusiona las sub-engines RFM, loyalty, features y etiqueta de churn en un único score de salud. |
| Clustering de clientes | customer_clustering | Segmentación de clientes con múltiples algoritmos de clustering y selección automática de K. |
| CLV del cliente | customer_clv | Customer Lifetime Value vía aproximaciones heurísticas BG/NBD + Gamma-Gamma basadas en RFM. |
| Features de CLV | customer_clv_features | Extracción de features BG/NBD + Gamma-Gamma por cliente (frecuencia, recencia, T, valor monetario) para modelado de CLV. |
| Migración de segmentos | customer_migration | Matriz de transición de segmentos entre los dos períodos más recientes, con estado estacionario y proyección multi-período. |
| Velocidad de engagement | engagement_velocity | Velocidad y aceleración de engagement del cliente a partir de la cadencia de eventos y el gasto. |
| Features de clientes | features | Ingeniería de features por cliente a partir del historial bruto de compras. |
| Fidelidad | loyalty | Score de fidelidad en tiers vía normalización MinMax ponderada de 5 métricas del cliente. |
| NPS | nps | Net Promoter Score calculado a partir de respuestas 0-10 de encuesta. |
| Segmentos de compra | purchasing_segment | Segmentación de comportamiento de compra vía clustering multi-algoritmo con aceleración dedicada. |
| RFM | rfm | Segmentación RFM (Recencia, Frecuencia, Monetario) de clientes vía score por cuantiles. |
| Segmentación genérica | segment | Segmentación genérica de clientes vía clustering multi-algoritmo con aceleración dedicada. |
| Perfiles de clusters | segment_cluster_profiles | Perfil estadístico y features discriminantes de cada cluster de clientes ya calculado. |
| Reporte de segmentación | segmentation_report | Reporte consolidado de la distribución de clientes y métricas entre segmentos existentes. |
| Suscripciones & recurrencia | subscription_engine | Parámetros óptimos de suscripción/recurrencia por par cliente-producto. |
| Supervivencia (Kaplan-Meier) | survival_kaplan_meier | Análisis de supervivencia Kaplan-Meier para estudios de vida útil del cliente / churn. |
Financiero (6)
| Engine | Clave | Qué hace |
|---|---|---|
| Punto de equilibrio | break_even | Análisis de punto de equilibrio: BEP en unidades/ingresos, margen de contribución, margen de seguridad, DOL y sensibilidad. |
| Monte Carlo (VAN) | monte_carlo | Simulación Monte Carlo de VAN: distribución probabilística de VAN y TIR con VaR/CVaR y percentiles. |
| Monte Carlo (profundo) | monte_carlo_deep | Simulación Monte Carlo de VPN/TIR a gran escala con aceleración dedicada (fallback en CPU). |
| VPN | npv | Calculadora de VPN: valor presente neto, TIR, payback fraccionario e índice de rentabilidad. |
| Cascada de ingresos | revenue_waterfall | Cascada de ingresos: descompone la variación de ingresos entre dos períodos en clientes nuevos/perdidos, frecuencia y ticket promedio. |
| Desempeño de ventas | sales_performance | Desempeño de ventas: ingresos por producto, cantidad, tendencia de ingresos, participación de mercado y margen de ganancia opcional. |
Pronósticos (10)
| Engine | Clave | Qué hace |
|---|---|---|
| Pronóstico de costos | cost_forecasting | Pronóstico de costos vía ExponentialSmoothing vs regresión lineal, con el mejor modelo elegido por RMSE en holdout. |
| Planificación de demanda | demand_planning | Pronóstico de demanda multi-SKU con ranking por volumen y alertas de tendencia. |
| Pronóstico de costos (CV) | forecast_cost_improved | Pronóstico de costos con validación cruzada de ventana expansiva, comparación por MAPE e intervalos de confianza. |
| Pronóstico de costos (dominio) | forecast_cost_totus | Pronóstico de costos de dominio con ajuste de inflación opcional y detalle por categoría. |
| Pronóstico (profundo) | forecast_deep | Pronóstico multi-horizonte con deep learning y aceleración dedicada (N-BEATS / N-HiTS, fallback MLP). |
| Pronóstico de ingresos | forecast_revenue | Pronóstico de ingresos con crecimiento YoY y descomposición opcional volumen vs precio. |
| Pronóstico (modelo guardado) | forecast_saved | Pronóstico con un modelo preentrenado cargado desde el almacenamiento de objetos, o fallback ExponentialSmoothing entrenado en la entrada. |
| Pronóstico de unidades | forecast_units | Pronóstico de unidades/demanda con detección de estacionalidad y clasificación de tendencia. |
| Pronóstico de unidades (lote) | forecast_units_asyncio | Wrapper de sales_forecasting ruteable por callback para envíos en lote con metadatos de entrega asíncrona. |
| Pronóstico de ventas | sales_forecasting | Pronóstico de ventas multi-modelo (ExponentialSmoothing, ARIMA, tendencia lineal) con selección automática del mejor. |
Inventario (9)
| Engine | Clave | Qué hace |
|---|---|---|
| Exceso de inventario (reporte) | excess_inventory_nlp | Reporte de exceso de inventario: LLM primero, con fallback basado en reglas cuando el LLM no está disponible o falla. |
| Promoción por vencimiento | expiry_promo | Calculadora de descuento óptimo de remarcación para productos próximos a vencer. |
| Optimizador entre sucursales | inter_branch_optimizer | Detecta desequilibrios de stock entre sucursales por producto y recomienda transferencias. |
| Historial de inventario | inventory_history | Análisis de movimientos de inventario por producto con 15+ métricas: rotación, volatilidad, clase ABC, aging FIFO y riesgo. |
| Optimización de inventario | inventory_optimization | Calculadora de EOQ y stock de seguridad con análisis de sensibilidad. |
| Exceso de inventario (LLM) | nlp_llm_excess_inventory | Reporte en lenguaje natural generado por LLM sobre exceso de inventario (resumen ejecutivo, principales ítems, aging, recomendaciones). |
| Predictor de recompra | refill_predictor | Predice la fecha de la próxima recompra de productos de uso continuo a partir del historial de compras del cliente. |
| Aging de stock | stock_aging | Clasifica los ítems de inventario en activos/lentos/muertos por días desde la última venta. |
| Reposición de stock | stock_replenishment | Calcula stock de seguridad, punto de reposición, EOQ y clasificación de demanda por producto, con alertas de reposición. |
Lenguaje Natural (8)
| Engine | Clave | Qué hace |
|---|---|---|
| Embeddings de imagen | image_embeddings | Embeddings de imagen con aceleración dedicada: embed, similitud, búsqueda o dedup por modelo de visión. |
| LLM | llm | Procesamiento de texto por LLM en el tier de aceleración dedicada: sentimiento, resumen, clasificación, extracción, análisis, traducción o prompt custom. |
| Análisis de texto (EN) | nlp_analisys_en | Análisis de texto por LLM fijado en inglés: sentimiento, resumen, clasificación, extracción, análisis o traducción al portugués. |
| Embeddings de texto | nlp_embeddings | Embeddings semánticos de texto con aceleración dedicada: embed, similitud, búsqueda o clustering. |
| Análisis de texto | nlp_local | Análisis de texto vía LLM: sentimiento, resumen, clasificación, extracción, análisis, traducción o prompt custom. |
| Sanitización de PII | sanitize | Remoción de PII: enmascara emails, teléfonos, CPF/CNPJ, tarjetas de crédito e IPs en texto libre. |
| Sentimiento en tiempo real | sentiment_realtime | Sentimiento por reglas para micro-lotes: léxico + emojis + negación, sin LLM. |
| Voz del cliente | sentiment_voc | Análisis de voz del cliente vía LLM local (sentimiento, palabras clave o tópicos) con fallback por reglas. |
Observabilidad (5)
| Engine | Clave | Qué hace |
|---|---|---|
| Accesos de borde | edge_traffic | Analiza logs de acceso del borde: clustering de IPs, jornadas de navegación, desempeño de backends y anomalías de seguridad. |
| Análisis de logs | log_analyzer | Analiza logs de aplicación: frecuencia de errores, amenazas de seguridad y anomalías de errores inéditos. |
| Tracking de páginas | tracking_pages | Clasifica páginas web por reglas de URL/título y construye un índice de búsqueda semántica. |
| Tracking de sesiones | tracking_sessions | Detecta jornadas de sesión anómalas por similitud de embeddings contra una baseline vectorial. |
| Tracking de visitantes | tracking_visitors | Clasifica visitantes como humano, bot, scraper o crawler por similitud de embeddings más flags de señal. |
Propensión (4)
| Engine | Clave | Qué hace |
|---|---|---|
| Propensión de compra | propensity | Score de propensión de compra con aceleración dedicada vía competencia multi-modelo (Random Forest / XGBoost / red neuronal). |
| Propensión a campaña | propensity_respond_campaign | Propensión a responder a una campaña de marketing, vía XGBoost con aceleración dedicada y fallback en CPU. |
| Score de propensión | propensity_score | Score genérico de propensión a evento binario, con probabilidades calibradas y análisis de lift por deciles. |
| Propensión a upgrade | propensity_upgrade_plan | Propensión a hacer upgrade de plan/tier, vía XGBoost con aceleración dedicada y fallback en CPU. |
Recomendaciones (11)
| Engine | Clave | Qué hace |
|---|---|---|
| Score de bundles | bundle_scoring | Puntúa pares candidatos de bundle de productos por lift de coocurrencia, soporte y precio sugerido de descuento. |
| Cross-sell | cross_sell | Matriz de coocurrencia de productos: soporte, confianza y lift para pares de cross-sell. |
| Precios dinámicos | dynamic_pricing | Recomendación de precio óptimo a partir de regresión log-log de elasticidad-precio. |
| Monitor de precios | price_monitor | Monitoreo competitivo de precios: PPI, tendencia, volatilidad, detección de guerra de precios y alertas de variación entre sitios. |
| Sensibilidad al precio | price_sensitivity | Correlación precio-demanda por producto y proxy de dirección de elasticidad. |
| Canasta de productos | product_basket | Análisis de canasta de compras: itemsets frecuentes Apriori y reglas de asociación (soporte/confianza/lift). |
| Recomendación híbrida | recommend | Engine híbrida de recomendación: filtrado colaborativo SVD con aceleración dedicada y fallbacks por contenido y popularidad. |
| Recomendación de ítems | recommend_items | Recomendaciones por filtrado colaborativo vía factorización de matriz SVD (usuario-ítem o ítem-ítem). |
| Ítems similares | recommend_similar_items | Recomendaciones ítem a ítem: colaborativa (SVD+coseno) o por contenido, con aceleración dedicada opcional. |
| Modelo de uplift | uplift_model | Modelado de uplift T-Learner: efecto individual de tratamiento, segmentación de Radcliffe y Qini/AUUC. |
| Sugerencias de upsell | upsell_suggestions | Sugerencias de upsell rankeadas por lift de reglas de asociación, confianza o margen. |
Riesgo (10)
| Engine | Clave | Qué hace |
|---|---|---|
| Anomalías (fraude) | anomaly | Detección de fraude/anomalías con aceleración dedicada vía competencia multi-algoritmo (Isolation Forest, LOF, autoencoder opcional). |
| Anomalías en cuentas | anomaly_accounts | Detección multidimensional de anomalías por Z-score sobre features numéricas por cuenta. |
| Anomalías (profundas) | anomaly_deep | Detección profunda de anomalías con aceleración dedicada vía VAE/DAGMM más baselines Isolation Forest y LOF. |
| Detección de anomalías | anomaly_detection | Detección no supervisada de anomalías vía Isolation Forest o Local Outlier Factor. |
| Anomalías en grafo | anomaly_graph | Detección de anomalías en redes de transacciones vía Z-scores de centralidad/PageRank. |
| Anomalías multivariadas | anomaly_multivariate | Detección multivariada de anomalías (Isolation Forest / LOF) para lotes grandes, con estadísticas univariadas opcionales por columna. |
| Anomalías (Z-score) | anomaly_zscore | Detección univariada de anomalías por columna numérica: Z-score, IQR, MAD o Grubbs. |
| Riesgo de crédito | credit_risk | Score de riesgo de incumplimiento vía clasificadores calibrados Regresión Logística / Random Forest. |
| Riesgo de crédito (explicable) | credit_risk_explain | Score de riesgo de crédito explicable: importancia global por permutación más explicaciones de factores por muestra. |
| Detector de descuento falso | fake_discount_detector | Detecta patrones fraudulentos de descuento en que los precios se inflan antes de la promoción para simular un descuento mayor. |
Ventas & Tendencias (5)
| Engine | Clave | Qué hace |
|---|---|---|
| Matriz BCG | bcg_matrix | Clasificación en matriz BCG de crecimiento-participación: Stars/Cash Cows/Opportunities/Problems. |
| Distribución por categoría | category_distribution | Detalle categórico genérico con conteos, porcentajes y tabulación cruzada opcional. |
| Análisis demográfico | demographic_analysis | Detalle demográfico de clientes por edad, género y afinidad género-categoría. |
| Preferencias de compra | purchase_preferences | Análisis de preferencias de compra por categoría: horario, método de pago y día de la semana. |
| Distribución horaria | time_distribution | Distribución horaria de transacciones en rangos configurables, con detección de horas pico. |
Cada engine exige sus propias columnas. Cambiar el engine aquí trae su payload de ejemplo al cuerpo de abajo.
curl -X POST "https://intelligence.echosistema.live/api/v1/rfm/process?dry_run=true&playground=true" \
-H "X-TENANT-KEY: $TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{ "params": { "bins": 5, "reference_date": "2026-08-01" }, "data": [ { "customer_id": "cloud-strife", "purchase_date": "2026-07-28", "amount": 1890 }, { "customer_id": "cloud-strife", "purchase_date": "2026-07-05", "amount": 1450 }, { "customer_id": "cloud-strife", "purchase_date": "2026-06-18", "amount": 1220 }, { "customer_id": "cloud-strife", "purchase_date": "2026-05-30", "amount": 980 }, { "customer_id": "ramza-beoulve", "purchase_date": "2026-07-30", "amount": 2100 }, { "customer_id": "ramza-beoulve", "purchase_date": "2026-07-11", "amount": 1680 }, { "customer_id": "ramza-beoulve", "purchase_date": "2026-06-24", "amount": 1340 }, { "customer_id": "ramza-beoulve", "purchase_date": "2026-06-02", "amount": 1105 }, { "customer_id": "tifa-lockhart", "purchase_date": "2026-07-19", "amount": 640 }, { "customer_id": "tifa-lockhart", "purchase_date": "2026-06-28", "amount": 585 }, { "customer_id": "tifa-lockhart", "purchase_date": "2026-06-06", "amount": 520 }, { "customer_id": "tifa-lockhart", "purchase_date": "2026-05-14", "amount": 610 }, { "customer_id": "agrias-oaks", "purchase_date": "2026-07-22", "amount": 720 }, { "customer_id": "agrias-oaks", "purchase_date": "2026-06-30", "amount": 665 }, { "customer_id": "agrias-oaks", "purchase_date": "2026-05-27", "amount": 590 }, { "customer_id": "barret-wallace", "purchase_date": "2026-07-15", "amount": 3250 }, { "customer_id": "cidolfas-orlandeau", "purchase_date": "2026-06-21", "amount": 2980 }, { "customer_id": "cidolfas-orlandeau", "purchase_date": "2026-03-09", "amount": 2400 }, { "customer_id": "cid-highwind", "purchase_date": "2026-03-12", "amount": 880 }, { "customer_id": "cid-highwind", "purchase_date": "2026-02-20", "amount": 940 }, { "customer_id": "cid-highwind", "purchase_date": "2026-01-28", "amount": 760 }, { "customer_id": "delita-heiral", "purchase_date": "2026-02-14", "amount": 1120 }, { "customer_id": "delita-heiral", "purchase_date": "2026-01-19", "amount": 1050 }, { "customer_id": "yuffie-kisaragi", "purchase_date": "2026-01-09", "amount": 180 }, { "customer_id": "mustadio-bunansa", "purchase_date": "2026-01-15", "amount": 215 }, { "customer_id": "mustadio-bunansa", "purchase_date": "2026-01-03", "amount": 160 } ] }'Listar resultados
Propósito
Resúmenes paginados (más recientes primero), con alcance del tenant, en el envelope de paginación estándar. Los resultados efímeros (dry_run) nunca aparecen aquí, con o sin filtros.
Parámetros de query
| Parámetro | Significado |
|---|---|
page | Número de página, 1-based. Por defecto 1 |
per_page | Por defecto 50, máximo 200 |
engine | Filtra por nombre de engine |
status | completed o failed |
from / to | Límites de created_at (RFC 3339, inclusivos) |
include | summary trae el payload compacto en cada fila |
playground | true lista solo lo que nació clasificado como playground; false lista el resto, incluidos los jobs que la API nunca vio nacer (publicados directo en el broker), que cuentan como no-playground. Ausente, lista todo |
paginate | cursor devuelve el cuerpo keyset (obsoleto) |
Un parámetro fuera de esta tabla responde 400, nunca se ignora en silencio: ?stauts=failed devolvería el listado entero, que la pantalla lee como "esta empresa no tiene fallos".
Respuesta
{
"current_page": 2,
"data": [
{ "process_uuid": "0c099b32-f000-48d0-92d7-dfe4a6f33def", "engine": "rfm",
"status": "completed", "execution_ms": 1432, "created_at": "2026-08-27T18:22:31Z" }
],
"first_page_url": "/api/v1/results?engine=rfm&page=1&per_page=50",
"from": 51,
"last_page": 4,
"last_page_url": "/api/v1/results?engine=rfm&page=4&per_page=50",
"links": [
{ "url": "/api/v1/results?engine=rfm&page=1&per_page=50", "label": "« Previous", "active": false },
{ "url": "/api/v1/results?engine=rfm&page=2&per_page=50", "label": "2", "active": true },
{ "url": "/api/v1/results?engine=rfm&page=3&per_page=50", "label": "Next »", "active": false }
],
"next_page_url": "/api/v1/results?engine=rfm&page=3&per_page=50",
"path": "/api/v1/results",
"per_page": 50,
"prev_page_url": "/api/v1/results?engine=rfm&page=1&per_page=50",
"to": 100,
"total": 187
}from y to son posiciones 1-based dentro del conjunto entero, y vienen null cuando la página está vacía - no 0. total cuenta todas las filas que coinciden con los filtros. links trae el pager ya renderizado, incluidos los ... donde se elidió un tramo, y cada link repite los filtros vigentes: seguirlo nunca amplía el conjunto sin avisar.
execution_ms y error se omiten cuando no aplican. created_at es el instante de finalización, no el de envío.
Las URLs pueden ser relativas
path y los *_url son absolutos solo cuando el despliegue define PUBLIC_BASE_URL; sin él salen relativos a la raíz (/api/v1/results?page=2). Es deliberado: el servicio se niega a derivar su propia URL del header Host, que detrás de un proxy inverso es un valor que el cliente controla. Resuelva con new URL(url, base) en vez de concatenar, o arme la navegación a partir de current_page y last_page.
include=summary
Sin él, una pantalla que muestra lo que produjo cada job necesita un GET /api/v1/results/{process_uuid} por fila - cincuenta y una peticiones para una página de cincuenta, contra la misma cuota por minuto que gasta el resto de la integración. Con él, cada fila trae result_summary y la página entera cuesta una petición.
Es opt-in porque quien lista solo para seguir el estado no usa el payload y no debe pagarlo en bytes. Los links de paginación repiten la flag, así que la página dos no cambia de forma a mitad de la navegación. El payload completo sigue en la ruta de detalle.
Paginación keyset (obsoleta)
?paginate=cursor - o cualquier petición que ya envíe ?cursor= - devuelve el cuerpo original {"items": [...], "next_cursor": "..."}, con limit en lugar de per_page y next_cursor ausente en la última página. Un cursor inválido responde 400; nunca se trata como primera página.
Sigue siendo la herramienta correcta para recorrer el historial entero: un cursor cuesta lo mismo a cualquier profundidad, mientras que page se rechaza más allá de 100.000 filas adentro. Tampoco se desliza - filas que llegan a mitad del recorrido nunca hacen que los ítems se repitan o desaparezcan.
Regla práctica: una pantalla paginada usa page; exports y recorridos completos usan paginate=cursor.
Errores
| Status | Cuándo |
|---|---|
400 | parámetro desconocido, page menor que 1 o más allá de 100.000 filas, cursor inválido |
429 | ventana de peticiones por minuto agotada |
curl -X GET "https://intelligence.echosistema.live/api/v1/results?page=1&per_page=50&engine=rfm" \
-H "X-TENANT-KEY: $TENANT_KEY"Detalle de resultado
Propósito
El detalle completo de un resultado, con alcance del tenant. Un process_uuid que no existe, pertenece a otro tenant o ya fue quemado responde 404 (nunca 403 - la existencia no se filtra).
Cuerpo
process_uuid, engine, status, params (como fueron aceptados), result_summary (siempre inline), execution_ms (cuando está presente), ephemeral, created_at, más exactamente uno de:
- Payload inline:
result_fullpresente,result_full_urlausente. - Payload con claim-check (resultados grandes van al almacenamiento de objetos):
result_fullausente yresult_full_urlcon una URL de descarga prefirmada de vida corta (300s por defecto). Descárgala pronto; repite la llamada para obtener una URL nueva.
En jobs fallidos, error lleva el motivo.
Los efímeros son burn-on-read
Un resultado dry_run se borra atómicamente en el primer GET exitoso, que lo devuelve; toda request siguiente responde 404. Exactamente un llamante observa un resultado efímero, incluso bajo lecturas concurrentes. Las filas efímeras no leídas expiran en 1 día.
curl -X GET "https://intelligence.echosistema.live/api/v1/results/{process_uuid}" \
-H "X-TENANT-KEY: $TENANT_KEY"Uso agregado
Propósito
Conteos, reparto de fallos y percentiles de duración de la empresa autenticada, abiertos por tiempo, por engine y por flota de workers - lo que necesita un panel de uso, en una sola llamada.
Usa esto en vez de paginar el listado para contar. Noventa días de una empresa con movimiento son cientos de peticiones contra la misma cuota por minuto que gasta el resto del producto, repetidas en cada carga del panel.
Parámetros
| Parámetro | Significado |
|---|---|
from, to | RFC 3339, inclusivos. Por defecto, los últimos 30 días. Ventana limitada a 366 días |
bucket | day (por defecto), week o month - granularidad de la serie |
engine | recorta todas las secciones a un engine |
status | recorta todas las secciones a completed o failed |
Respuesta
{
"from": "2026-06-01T00:00:00Z",
"to": "2026-08-27T18:00:00Z",
"bucket": "week",
"totals": { "count": 4821, "completed": 4760, "failed": 61, "p50_ms": 340, "p95_ms": 18200 },
"series": [{ "bucket": "2026-06-01T00:00:00Z", "count": 190, "completed": 188, "failed": 2 }],
"engines": [{ "engine": "rfm", "runtime": "rust", "category": "analytics", "count": 1200, "failed": 3, "p50_ms": 12, "p95_ms": 45 }],
"runtimes": [{ "runtime": "gpu", "count": 310, "failed": 12, "p50_ms": 41000, "p95_ms": 92000 }]
}Cuatro cosas antes de graficar
Todas las secciones salen de una transacción, sobre la misma ventana: los totales nunca discrepan de la suma de los desgloses.
Los percentiles no son aditivos. El p95 de una flota no se arma con el p95 de sus engines - cada uno se mide sobre la ventana que describe.
Los buckets vacíos están ausentes, no en cero. La API informa lo que ocurrió; un gráfico que necesita eje continuo rellena los huecos con el rango que ya pidió.
runtimes puede sumar menos que totals. Un engine que se ejecutó y luego salió del catálogo conserva su uso en engines, sin runtime, pero no se atribuye a una flota que quizá no lo ejecutó.
Errores
| Status | Cuándo |
|---|---|
400 | from posterior a to, o ventana mayor de 366 días |
429 | ventana de peticiones por minuto agotada |
Los resultados dry-run se excluyen, igual que en el listado - un dry-run no es uso. Las duraciones son el tiempo de ejecución del worker, no la espera en cola, que este servicio no observa.
El bloque payload: lo que fue enviado
Las otras secciones describen lo que volvió. Esta describe lo que el cliente envió, medido en el instante en que la submission fue aceptada.
| Campo | Significado |
|---|---|
submissions | Cuántas submissions tiene la ventana, medidas o no |
bytes_total | Suma del input medido |
bytes_avg | Media del input medido |
bytes_p50, bytes_p95 | Mediana y percentil 95. No son aditivos |
Como el registro nace en la aceptación y no en la conclusión, cuenta también los jobs que nunca produjeron resultado: consumieron la subida, la cola y un worker igual, y quitarlos describiría un consumo menor que el real.
El bloque está ausente cuando nada en la ventana fue medido. Eso es distinto de cero bytes: un tenant cuyas submissions son anteriores a la medición, o que solo corrió dry-run, se queda sin respuesta aquí en vez de recibir una respuesta de nada.
curl -X GET "https://intelligence.echosistema.live/api/v1/results/stats?bucket=day" \
-H "X-TENANT-KEY: $TENANT_KEY"En vuelo
Propósito
Lo que esta empresa envió y todavía no recibió de vuelta, del que espera hace más tiempo al que espera hace menos.
Pendiente no es un estado del listado, y no podría serlo. GET /api/v1/results lee la tabla de los resultados, y un resultado guardado es terminal: completed o failed. Un job aceptado y todavía en proceso no tiene fila alguna allí, así que ?status=pending sería pedirle a una tabla que describa lo que deliberadamente no registra. Esta ruta lee otra cosa: el registro del envío, escrito en el instante en que fue aceptado.
Parámetros de query
| Parámetro | Significado |
|---|---|
stale_after | Solo los que esperan hace más que esos segundos. Ausente devuelve todos. Negativo responde 400 |
engine | Un engine, en la grafía actual o en la heredada |
limit | Predeterminado y tope 200 |
Respuesta
200 con {"pending": [...], "count": 1, "queues": [...]}, del que espera hace más tiempo al que espera hace menos.
{
"pending": [
{ "process_uuid": "0c099b32-f000-48d0-92d7-dfe4a6f33def", "engine": "rfm", "mode": "async",
"submitted_at": "2026-08-28T04:10:00Z", "waiting_secs": 3600,
"queue": "ml.customer_intelligence.rfm" }
],
"count": 1,
"queues": [{ "engine": "rfm", "waiting": 4213, "consumers": 6 }]
}| Campo | Qué es |
|---|---|
pending[].process_uuid | El mismo identificador que devolvió el envío, y el que tendrá el resultado |
pending[].engine | El engine, en la grafía pública |
pending[].mode | async o sync |
pending[].submitted_at | Cuándo fue aceptado el envío (RFC 3339) |
pending[].waiting_secs | Hace cuántos segundos espera |
count | Cuántas filas llegaron |
queues[] | El estado de las colas de esos engines, ausente cuando no se pudo obtener |
Tres cosas que se equivocan solas aquí
No existe umbral predeterminado, y es a propósito. El tiempo que vuelve sospechosa una espera depende del engine: un rfm de doce milisegundos y un job pesado de cinco minutos no comparten número. Cada fila trae waiting_secs y quien llama juzga. Un umbral fijo escrito en el cliente y presentado como "atrasado" es una afirmación que esta ruta no hace.
queues describe la operación entera, nunca tu fila. Ahí están cuántos mensajes esperan y cuántos procesos los escuchan, sumando todas las empresas: atribuir un mensaje a un cliente exigiría consumirlo. Esos números responden "este engine está congestionado, hay alguien trabajando en él", y nunca "mi job es el 4213º de la fila".
queues ausente es respuesta no obtenida, y no fila vacía. El contexto tiene un presupuesto de tres segundos y nunca retiene la respuesta: la lista llega igual. consumers: 0 es el único valor accionable, y significa que nada avanza para ese engine mientras no vuelva alguien a procesarlo.
Cuándo una submission sale de esta lista
En el instante en que su resultado se guarda. Ambas escrituras ocurren en la misma transacción, así que nunca hay un resultado guardado que siga apareciendo aquí - no existe ventana en que las dos rutas se contradigan.
La marca vive en la propia submission, y eso es lo que la hace estable en el tiempo: cada plan guarda los resultados por menos tiempo que las submissions (retention_days), y un resultado barrido no hace que su submission reaparezca como pendiente.
Errores
| Status | Cuándo |
|---|---|
400 | stale_after negativo |
429 | ventana de solicitudes por minuto agotada |
Dry-run nunca aparece: su resultado se lee una vez y desaparece, y quedaría aquí para siempre con cara de trabado.
Esta ruta acepta solo el token: una clave de API nombra a una empresa, no a una persona, y se rechaza con 401 aunque sea válida.
curl -X GET "https://intelligence.echosistema.live/api/v1/submissions/pending" \
-H "Authorization: Bearer $ACCESS_TOKEN"Catálogo de engines
Propósito
El catálogo completo de engines aceptadas, agrupado por los once grupos de la plataforma (analytics, customer_intelligence, financial, forecast, inventory, nlp, observability, propensity, recommendations, risk, sales_trends), con un flag allowed calculado desde tu suscripción.
Respuesta
200 con {"groups": [{"group": "...", "engines": [{"engine", "runtime", "active", "allowed"}]}], "total": 105}.
runtime es una clasificación interna del tier de ejecución de la engine; los nombres de cola y la topología interna no se exponen.
curl -X GET "https://intelligence.echosistema.live/api/v1/catalog/engines" \
-H "X-TENANT-KEY: $TENANT_KEY"Engines con resultados
Propósito
Las engines distintas que tienen al menos un resultado (no efímero) para el tenant autenticado: {"engines": ["kmeans", "rfm"]}.
Es el complemento del catálogo: el catálogo dice qué puedes correr; esta ruta dice qué ya corriste.
curl -X GET "https://intelligence.echosistema.live/api/v1/engines" \
-H "X-TENANT-KEY: $TENANT_KEY"Salud de la flota
Propósito
La salud agregada de la flota de ejecución. Dos señales se combinan por tier de ejecución: el heartbeat más fresco de cada worker y la presencia de consumidores en las colas del tier. Los tiers esperados vienen de las filas activas del catálogo de engines, nunca de una lista fija.
| Status | Significado |
|---|---|
healthy | Heartbeat más nuevo que 90s Y al menos un consumidor coincidente |
degraded | Heartbeat de 90-300s, O las dos señales discrepan (heartbeat fresco, cero consumidores) |
down | Heartbeat de más de 300s (o nunca visto) Y ningún consumidor, para un tier esperado |
disabled | El tier no tiene engine activa en el catálogo - informativo, sin alarma |
Una indisponibilidad de la API de gestión de colas nunca tumba el endpoint: la señal de consumidores reporta "unknown" y el status cae a la vista solo de heartbeat.
Forma de acceso (protección de topología)
Una credencial de tenant recibe solo {"runtimes": {"py": {"status": "healthy"}, ...}} - sin nombres de cola, identidades de worker ni conteos. El token de operador recibe además la edad del heartbeat, los consumidores y la lista por worker.
GET /healthz sigue siendo un probe de liveness separado, sin autenticación y sin dependencias.
curl -X GET "https://intelligence.echosistema.live/api/v1/platform/health" \
-H "X-TENANT-KEY: $TENANT_KEY"Límites y cuotas
Cómo funcionan
- Por minuto (
max_requests_per_min): ventana fija de minuto UTC en toda ruta autenticada. Excederla responde429./healthznunca se limita. - Por día (
max_jobs_per_day): ventana fija de día UTC, verificada dentro dePOST /api/v1/{engine}/processantes de cualquier efecto secundario; excederla responde429. NULLen la suscripción desactiva ese límite específico.
Los valores vigentes de tu suscripción aparecen en GET /api/v1/auth.
Registrar dispositivo
Propósito
Registra un dispositivo para recibir notificaciones push del tenant autenticado. Todo usuario del tenant recibe la alerta de todo job terminado del tenant (no hay suscripción por usuario).
Cuerpo
{"platform": "ios", "registration_id": "cX9_f-Yz:APA91bH..."}
platformes uno deandroid,ios,web.registration_ides el token de registro/instalación del servicio de push: 16 a 4096 bytes, solo ASCII gráfico imprimible (sin espacio, sin caracteres de control). Cualquier otra cosa responde400 {"error": "invalid registration_id"}. Los campos desconocidos se rechazan.
El tenant viene de tu credencial y nunca puede enviarse en el cuerpo.
Respuesta
Responde 200 siempre, nunca 201 - el registro es un upsert. Volver a registrar el mismo dispositivo devuelve el mismo id y refresca su último contacto; registrar un dispositivo que hoy pertenece a otro tenant lo mueve al tuyo atómicamente, y el dueño anterior deja de recibir por él de inmediato (un aparato que cambió de manos no puede seguir recibiendo las alertas de la cuenta vieja).
{"id": 42, "platform": "ios", "active": true, "last_seen_at": "2026-08-20T12:00:00Z"}
El registration_id nunca se devuelve: ya lo tienes, y la respuesta sería un lugar más por donde filtrarse. Los dispositivos se direccionan por el id de aquí en adelante.
curl -X POST "https://intelligence.echosistema.live/api/v1/devices" \
-H "X-TENANT-KEY: $TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{ "platform": "ios", "registration_id": "cX9_f-Yz:APA91bH-exemplo" }'Quitar dispositivo
Propósito
Detiene la entrega a un dispositivo, por el id de la respuesta de registro. Responde 204, y 204 de nuevo en una segunda llamada (idempotente). Un dispositivo que no existe y uno de otro tenant responden ambos 404 - la existencia no se filtra.
Es un soft delete: el registro permanece para auditoría y simplemente deja de ser un destino. El servicio hace la misma desactivación por sí solo cuando el proveedor de push reporta el registro como muerto (app desinstalada, token rotado) - un dispositivo abandonado sale del fan-out sin ninguna llamada tuya.
curl -X DELETE "https://intelligence.echosistema.live/api/v1/devices/42" \
-H "X-TENANT-KEY: $TENANT_KEY"Ticket de realtime
Propósito
Emite un ticket de un solo uso para abrir el WebSocket de realtime. Sin cuerpo de request: el tenant viene de la credencial, como en GET /api/v1/auth y /me.
Respuesta
{"ticket": "9f2c... 64 caracteres hex minúsculos ...", "expires_in": 45}
expires_in está en segundos relativos, no es un timestamp absoluto - el ticket es para uso inmediato, sin husos horarios ni clock skew que reconciliar.
Por qué existe
El new WebSocket() de un navegador no puede enviar un header custom, así que ni X-TENANT-KEY ni Bearer viajan en el handshake. El ticket cubre exactamente esa brecha: es opaco, de vida corta, atado a tu tenant y consumido por el primer upgrade exitoso. Presentarlo una segunda vez falla como un ticket que nunca existió.
Trátalo como credencial, no como identificador: viaja en query string, así que asegúrate de que tu reverse proxy no escriba la query string del upgrade en su access log.
Dos comportamientos de la v1 que vale saber: emitir un ticket nuevo no invalida uno que ya tengas, y cada emisión cuenta contra tu cuota de requests por minuto.
curl -X POST "https://intelligence.echosistema.live/api/v1/realtime/tickets" \
-H "X-TENANT-KEY: $TENANT_KEY"WebSocket realtime
Propósito
Hace el upgrade al WebSocket de realtime. No hay otra credencial, porque un handshake de navegador no puede llevar headers.
El socket es unidireccional: el servidor transmite frames y nada de lo que envíes se procesa. Todo frame es JSON y lleva su versión de contrato dentro del frame:
{"version": 1, "event": "result", "process_uuid": "5d1c...", "status": "completed"}{"version": 1, "event": "heartbeat"}
Un frame result lleva identificadores y una etiqueta de status y nada más - sin nombre de engine, sin resumen, sin fragmento del resultado. Busca lo que necesites con GET /api/v1/results/{process_uuid}: esa llamada es autenticada y con alcance de tenant, lo que el payload del socket deliberadamente no intenta ser. Los campos ausentes se omiten, nunca se envían como null.
Rechazos
| Status | Significado |
|---|---|
400 | No es un upgrade WebSocket válido |
401 | Ticket ausente, desconocido, expirado, ya usado, o el tenant ya no está activo. Todos responden de forma idéntica - la respuesta nunca revela cuál |
404 | El canal de realtime está deshabilitado en este deploy |
429 | Tu ventana de requests por minuto se agotó. Espera y pide un ticket nuevo |
503 | Esta réplica está en su techo de conexiones. No es tu límite: reintenta en breve |
Para construir encima
Un rechazo después de que el ticket fue leído ya lo consumió - el reintento es pedir un ticket nuevo, nunca volver a presentar el viejo. Y la entrega es best effort: si la conexión cae, o el cliente deja de leer hasta llenar su buffer, los frames se descartan y nunca se reentregan - recarga la vista y apóyate en el push o en GET /api/v1/results para lo que no puede perderse.
El servidor cierra el socket con 4000 ("session expired, reconnect") cuando alcanza su tiempo máximo de vida, y con 4001 ("authorization revoked") cuando el tenant es suspendido o la suscripción queda inactiva. Trata los dos de forma distinta: 4000 es reconectar, 4001 es detenerse y arreglar la cuenta.
Webhook de finalización
Propósito
Cuando el tenant tiene webhook configurado, el servicio hace un POST de notificación después de que un resultado no efímero se persiste, si el desenlace coincide con webhook_events (completed, failed o both).
La entrega es at-least-once. Deduplica por process_uuid de tu lado - una reentrega del broker puede ocasionalmente producir una segunda notificación del mismo job.
Cuerpo
{"process_uuid": "...", "engine": "rfm", "status": "completed", "execution_ms": 42, "result_url": "/api/v1/results/<process_uuid>", "summary": {"score": 0.87}}
result_url es el endpoint estable de la API (únelo a la base de la API), nunca un link prefirmado de vida corta - vale cuando vayas a buscarlo.
Firma
Header X-Intelligence-Signature: sha256=<hexdigest>, donde el digest es HMAC-SHA256(bytes exactos del cuerpo crudo, webhook_secret). Verifica recomputando el HMAC sobre los bytes crudos recibidos (nunca sobre un parse reserializado) y compara en tiempo constante.
Entrega
Reintentos: 3 intentos en total, 1s antes del 2º, 3s antes del 3º, timeout de 5s por intento. Las URLs de destino deben resolver a direcciones públicas; los rangos privados, loopback, link-local y de metadatos se rechazan.
Push (contrato)
El contrato
El push no lleva datos de negocio, y eso es contrato, no detalle de implementación.
El texto visible de la alerta es uno de tres strings fijos (PT / EN / ES, según el idioma por defecto del tenant). No contiene nombre de engine, número, nombre de archivo ni fragmento de resultado, porque se renderiza fuera de tu app, en una pantalla de bloqueo, por software que no es nuestro y que cualquiera con el teléfono en la mano puede leer.
El bloque data lleva exactamente tres strings:
{"process_uuid": "...", "status": "completed", "result_url": "https://intelligence.echosistema.live/api/v1/results/..."}
result_url se construye desde la configuración del servidor, nunca desde el header Host de request alguna, y es el endpoint estable de la API, no un link de vida corta. Busca el resultado con tu llamada autenticada normal; la notificación es un timbre, no una entrega.
El cuerpo del webhook no es plantilla para el push. Lleva summary (datos reales del resultado) porque viaja a TU infraestructura por HTTPS con firma HMAC. El push pasa por un tercero y aparece en una pantalla de bloqueo - copiar el payload del webhook al push es un defecto de seguridad, no una optimización.
Higiene automática
Los dispositivos se desactivan automáticamente cuando el proveedor de push reporta el registro como muerto (app desinstalada, token rotado) y se depuran tras la ventana de inactividad del deploy. Ninguna de las dos necesita una llamada tuya.
Los tres canales, lado a lado
Tres canales anuncian el mismo evento y ninguno reemplaza a otro. Elige por dónde está el usuario, no por preferencia.
| Situación | Webhook | Push | Realtime |
|---|---|---|---|
| Tu backend necesita el resultado | sí - firmado, con retry, durable | no | no |
| App cerrada, teléfono en el bolsillo | no | sí - la alerta despierta el dispositivo | no |
| App abierta en la pantalla del job | no | (también dispara) | sí - llega en milisegundos |
| El usuario está offline ahora | entregado a tu servidor igual | entregado cuando el dispositivo vuelva | perdido, por diseño |
| Garantía at-least-once | sí | sí, mientras el dispositivo sea alcanzable | no |
Un job enviado con ?sync=true se anuncia como cualquier otro - el modo no deja marca en la fila persistida, lo que mantiene correcto el caso del 504: un envío sync que agotó el tiempo aún necesita su notificación cuando el resultado finalmente llegue. Por eso, deduplica por process_uuid en todos los canales.