Changelog · página 10 de 12

Novedades de la API

Cada cambio de la superficie pública queda registrado aquí, del más nuevo al más viejo. Esta página cubre del 17 jul 2026 – 25 jul 2026. También puedes suscribirte al feed.

v2.7.0

Partners API — escritura de oportunidades y proyectos

Si operas tu propio portal, ya puedes registrar y mover oportunidades y proyectos en Meteor desde ahí. Lo que creas por API es tuyo desde el primer momento: una oportunidad nace aceptada y a tu nombre, sin pasar por el pool de Meteor.

  • Oportunidades — POST /partner/leads, PATCH /partner/leads/:id, POST /partner/leads/:id/move, DELETE /partner/leads/:id y el detalle GET /partner/leads/:id, con el scope partner:leads:write. En el SDK: met.partner.leads.create/update/move/delete.
  • Proyectos — POST /partner/projects, PATCH /partner/projects/:id, POST /partner/projects/:id/advance-phase, tareas (/tasks, /project-tasks/:id), hitos y el detalle GET /partner/projects/:id, con el scope partner:projects:write. En el SDK: met.partner.projects.* y el sub-recurso met.partner.projects.tasks.
  • Idempotencia — los POST aceptan Idempotency-Key y el SDK genera uno por request: un reintento por timeout no te deja dos oportunidades iguales.
  • Etapas del pipeline: NUEVO · CONTACTADO · CALIFICADO · PROPUESTA · GANADO · PERDIDO.

Tres acciones se siguen gestionando solo desde el portal de Meteor, porque mueven dinero o atribución: proponer y aprobar el precio, asignar implementador, y convertir una oportunidad en cliente. advancePhase() las respeta — falla si el precio no está aprobado, si quedan hitos pendientes, o si el implementador no aceptó su oferta. Un proyecto creado por API es siempre de cliente.

Los dos scopes son exclusivos de keys de partner: una key de workspace que los pida recibe 403. Ya puedes marcarlos al crear una key desde Ajustes → Desarrolladores en tu portal.

Próximo: tickets de soporte (partner:support:*) y adjuntos y comentarios en oportunidades y proyectos.

v2.6.0

Imágenes, variables, visibilidad de colecciones y las tools de cada Met (SDK 0.5.0)

SDK @meteor.ia/sdk@0.5.0. Esta versión convierte en API de primera clase varias cosas que antes eran rodeos.

Generación de imágenes (met.images, scope integrations:execute, key live). Genera mockups sin resolver ids de integración a mano. Debita energía a tu workspace y devuelve una URL alojada por Meteor.

const img = await met.images.generate({ prompt: 'rótulo azul en fachada', aspect_ratio: '16:9' });
// img → { url, format, size_bytes, model, energy_debited }
await met.images.edit({ prompt: 'ponlo en acrílico', image_base64 });
await met.images.video({ prompt: 'giro 360', image_base64 });
  • POST /workspaces/{ws}/images/generate · /edit · /video.

Variables del workspace por API (met.variables.set/create/update/delete, scope variables:write). Guarda el token que tu Función HTTP resuelve con auth_workspace_variable — antes solo se podía desde el panel.

await met.variables.set('mi_api_token', '<TOKEN>', { encrypted: true });

Visibilidad de colecciones por Met (collections.expose_to_agent). Oculta una colección al LLM sin borrar los datos; aplica en todos los caminos (API, chat, canal).

await met.collections.update(collectionId, { expose_to_agent: false });

Las tools de un Met (met.agents.tools(agentId), scope agents:read). Descubre qué herramientas ve un Met y cuáles están deshabilitadas — el diagnóstico para saber por qué una Función no aflora, y qué nombres pasar a agents.update({ disabled_tools }).

const tools = await met.agents.tools(agentId);
// [{ name, source: 'collection'|'skill'|'function'|'internal', description, disabled }]
  • GET /workspaces/{ws}/agents/{agentId}/tools.

Bind de Met por conversación. En un Run, met (nombre o id) ejecuta ESE Met directo con sus Funciones. Con conversation_id, el primer met recibido queda fijado como default de la conversación: los runs siguientes que omitan met corren el mismo Met. Un met explícito siempre gana.

Errores de tool más claros. Cuando una Función HTTP falla, la respuesta indica la causa concreta (variable de auth ausente, 401 del endpoint, timeout) en vez de un error genérico. Nunca se exponen tokens ni secretos.

Mejoras de documentación del SDK: el handler HTTP directo de una Función quedó documentado como camino de primera clase (no necesitas un flujo para llamar a tu endpoint), y items.search aclara que es búsqueda por texto (léxica).

v2.5.0

Ejecuta un Met específico por Run, imágenes de entrada, y control fino de tools

Varias mejoras para construir el loop agéntico con precisión, pensadas con el feedback de los primeros integradores.

met en un Run ahora ejecuta ESE Met (bind, no hint). Antes el campo met era informativo y todo pasaba por el orquestador —que no cargaba las Funciones IA del Met—. Ahora, si pasas met (id o nombre), el Run ejecuta ese Met directamente con sus Funciones y tools:

const fn = await met.functions.create({ name: 'calcular_cotizacion', http: { url: '.../api/cotizar' } });
await met.functions.link(agentId, fn.id);

// Antes: la función no aparecía (el orquestador no la cargaba).
// Ahora: corre el Met "kami" con calcular_cotizacion disponible.
for await (const ev of met.runs.stream({ met: 'kami', input: 'Cotiza 3 piezas de acero' })) {
  console.log(ev.type, ev.data);
}

Si omites met (o no resuelve a un Met del workspace), el orquestador auto-rutea como antes. El metering es correcto en ambos casos (live cobra Energía, test es gratis).

Imágenes de entrada (visión). runs.create/stream acepta attachments para que el Met "vea" una imagen —clasificar una foto, leer un documento, validar un diseño—:

await met.runs.create({
  met: 'kami',
  input: '¿Este acabado coincide con el aprobado?',
  attachments: [{ url: 'https://.../foto-instalada.jpg', mime_type: 'image/jpeg' }],
});

Apaga tools por Met. met.agents.update({ disabled_tools }) desactiva tools nativas para un Met —por ejemplo, quitarle el acceso a las colecciones sin archivarlas—. Las tools deshabilitadas se ocultan del catálogo del modelo (ya no las ve ni las intenta) además de rechazarse en runtime:

await met.agents.update(agentId, {
  disabled_tools: [
    'mcp_meteor_list_collections', 'mcp_meteor_get_collection', 'mcp_meteor_list_items',
    'mcp_meteor_get_item', 'mcp_meteor_search_items', 'mcp_meteor_create_item',
    'mcp_meteor_update_item', 'mcp_meteor_set_item_name',
  ],
});

met.events — escucha el workspace en vivo. El SDK ahora expone el feed de eventos (met listen): una suscripción SSE efímera, sin montar un endpoint.

for await (const ev of met.events.stream({ types: ['contact.message.received', 'run.completed'] })) {
  console.log(ev.type, ev.data);
}

Más eventos que sí disparan. Se cablearon los emisores de contact.created, contact.updated, task.completed y conversation.handoff (antes estaban en el catálogo pero no llegaban). Suscríbete por webhook (met.webhooks.subscriptions) o escúchalos con met.events.stream.

v2.4.0

Construye el loop agéntico por SDK — Funciones que llaman tu endpoint, flujos y autopilot

El SDK 0.3.0 (npm i @meteor.ia/sdk@0.3.0) expone la superficie para armar todo el proceso agéntico desde tu backend, sin panel.

Funciones IA con handler HTTP directo. Una Función (una herramienta que tu Met puede llamar) ahora puede apuntar directo a tu endpoint HTTP — sin tener que armar un flujo:

const fn = await met.functions.create({
  name: 'lookup_price',
  description: 'Consulta un precio en el backend del cliente',
  parameters: [{ name: 'sku', description: 'Código de producto', required: true }],
  http: {
    url: 'https://tu-servidor.com/api/price',
    method: 'POST',
    sign_secret_variable: 'PRICE_SECRET', // firma cada request con X-Met-Signature
  },
});
await met.functions.link(agentId, fn.id);

Meteor llama tu endpoint con los argumentos que el Met recolectó (body JSON), lo firma con HMAC (X-Met-Signature) para que verifiques el origen con met.tools.createHandler, y devuelve tu respuesta al Met como resultado de la herramienta. Los secretos nunca se guardan en crudo: se referencian por nombre de variable del workspace (sign_secret_variable / auth_workspace_variable). También sigue disponible el handler por flujo (flow_id) con un nodo http.request, ahora creable por SDK con met.flows.*.

Nuevo en el SDK:

  • met.functions.* — crear/listar/actualizar/borrar Funciones IA + link/unlink a un Met (scopes functions:read/functions:write).
  • met.flows.* — crear un flujo, editar su lienzo (draft_definition), publicar, duplicar, archivar, ejecutar (flows:write / automations:*).
  • met.contacts.setAutopilot(id, enabled?) y pauseAutopilot(id, minutes) — prende/apaga o pausa el autopilot de un contacto para el handoff a un humano (scope handoff:manage).

Con esto el patrón completo es: crear el Met (met.agents.create), definir la Función que llama a tu API, vincularla, y —en canales— el Met responde solo cuando entra un mensaje.

v2.3.0

Registra tu propio servidor MCP HTTP por API (self-service)

Nuevo endpoint público: POST /workspaces/{workspaceId}/integrations (scope integrations:manage). Conecta tu propio servidor MCP HTTP a un workspace directamente por API/SDK — antes el alta de un MCP era solo interna (admin).

Al registrar, Meteor le hace tools/list a tu endpoint para validar que responde y cachear sus tools; queda activo de una vez. Los secretos van en credentials (write-only) y se rellenan en la plantilla auth_header — nunca quedan en el catálogo.

En el SDK: met.integrations.register(...).

const integ = await met.integrations.register({
  name: 'mi-conector',
  endpoint: 'https://tu-servidor.com/mcp',
  auth_header: 'Authorization: Bearer {token}',
  credentials: { token: process.env.MCP_TOKEN },
});
// integ.id → úsalo como `key` en integrations.tools / execute / deactivate

El endpoint se llama con safeFetch (guard anti-SSRF: se rechazan hosts loopback, IPs privadas y metadata). Las tools quedan disponibles para tus Mets y para met.integrations.execute(id, tool, input).

¿Cuándo usar esto vs una Función HTTP? Si solo quieres exponer un endpoint como herramienta, una Función con handler http (met.functions.create({ http: {...} })) es más simple. Registra un MCP cuando ya corres un servidor MCP con varias tools y quieres exponerlas todas de una.

v2.2.0

Gestiona tus webhooks salientes por API (registra tu endpoint sin el panel)

Nuevos endpoints públicos bajo /webhook-endpoints (scope webhooks:manage). Ahora registras el endpoint que recibe los eventos salientes de Meteor directamente por API o SDK — antes solo se podía desde el panel de Desarrolladores.

  • POST /webhook-endpoints — registra tu URL y los eventos que quieres escuchar (enabled_events, o ["*"] para todos). Devuelve el secreto de firma una sola vez: guárdalo para verificar cada entrega con met.webhooks.constructEvent(...).
  • GET /webhook-endpoints y GET /webhook-endpoints/event-types — lista tus endpoints y el catálogo de eventos disponibles.
  • DELETE /webhook-endpoints/{id} — elimina un endpoint.
  • POST /webhook-endpoints/{id}/test — envía una entrega de prueba firmada.
  • GET /webhook-endpoints/{id}/deliveries y POST /webhook-endpoints/{id}/deliveries/{deliveryId}/retry — inspecciona el log de intentos y reintenta una entrega puntual.

En el SDK: met.webhooks.subscriptions.*.

const sub = await met.webhooks.subscriptions.create({
  url: 'https://tu-servidor.com/hooks/met',
  enabled_events: ['contact.message.received', 'channel.run.completed'],
});
// sub.secret llega una sola vez — guárdalo.

Eventos nuevos en el catálogo:

  • contact.message.received (scope conversations:read) — entró un mensaje de un contacto por un canal (WhatsApp, etc.). Es el evento para construir productos conversacionales encima de Meteor sin hacer polling.
  • channel.run.completed / channel.run.failed (scope runs:read) — el Met respondió (o falló al responder) un mensaje de canal. Son distintos de run.completed/run.failed, que son Runs de la API con id consultable; un run de canal no tiene fila en Runs.

Cada entrega llega firmada con X-Met-Signature (HMAC-SHA256, con anti-replay). Verifícala con met.webhooks.constructEvent(rawBody, signature, secret) antes de procesar: si la firma no valida, responde 400 y no proceses.

v2.1.0

Import masivo de items entra a la API pública (upsert + insert-only)

Nuevo endpoint público: POST /collections/{collectionId}/items/import (scope items:write). Carga o actualiza muchos items de una colección en una sola llamada (hasta 5.000 filas por request), keyed por el nombre de cada field.

Tiene dos modos según mandes o no keyField:

  • Upsert (con keyField): hace match por data->>keyField. Si existe un item con ese valor lo actualiza (rellena/pisa con los valores no vacíos del archivo); si no, lo crea. Deduplica por clave dentro del mismo request. Es el modo para catálogos con clave natural — SKU, email, código de producto.
  • Insert-only (sin keyField): inserta todas las filas sin deduplicar. Es para datos que no tienen una clave única por fila —por ejemplo líneas de venta que comparten la misma fecha—, donde el upsert las colapsaría en un solo registro. Es también el camino de la carga inicial cuando creas una colección desde un spreadsheet.

La respuesta trae el balance del lote: { created, updated, skipped, errors }, con errors[] detallando la fila y el motivo (sin clave, clave duplicada en el archivo, o fila sin columnas conocidas) para que puedas reintentar solo lo que falló.

Nota de compatibilidad. keyField es opcional. Si ya venías llamando este endpoint con keyField, tu integración sigue funcionando igual (modo upsert). Omitirlo es lo único que activa el nuevo modo insert-only.

Las columnas del archivo que no correspondan a un field de la colección se descartan (no ensucian items.data). El import no dispara automatizaciones ni eventos por fila: es una carga de datos, no una secuencia de acciones de usuario.

v2.0.0

RAG sale de la API pública hasta que esté terminado

Los 4 endpoints de /rag/* dejan de ser públicos. Una API key ya no los alcanza (403 endpoint_not_public) y desaparecen del spec, del SDK y de esta referencia:

  • POST /workspaces/{workspaceId}/rag/search
  • POST /workspaces/{workspaceId}/rag/search-collections
  • POST /workspaces/{workspaceId}/rag/reindex
  • GET /rag/status

Los scopes rag:read y rag:write salen del selector al emitir una key. Si tienes una key que ya los tiene, la key sigue siendo válida — esos scopes simplemente no habilitan nada.

Por qué. Estábamos ofreciendo una capacidad que no funciona de punta a punta. Auditamos antes de retirarla:

  • Nada indexa. Los items no se indexan al crearse ni al editarse. El índice solo se llena con un /rag/reindex manual, así que queda viejo apenas alguien crea un item. Para que te sirviera, tendrías que reindexar todo el tiempo.
  • Nada consulta. Ningún Met usa la búsqueda semántica para responder — usan la búsqueda léxica con scoring de items. RAG era una isla.

Preferimos retirarla a dejarla publicada como si anduviera.

Sobre nuestra propia política. Nuestra convención dice que un endpoint público nunca se elimina sin un ciclo de deprecación completo (headers Deprecation/Sunset y un período de convivencia). Aquí lo saltamos, y queremos que sepas exactamente por qué en vez de que lo descubras solo:

Esos endpoints fueron públicos 7 días (del 10 al 17 de julio de 2026) y no por una decisión de producto: entraron barridos en una pasada masiva que decoró los 68 controllers de una. Sumado a que la feature no funcionaba completa, la probabilidad de que exista una integración real es prácticamente nula. La regla existe para no romperle el código a nadie; concluimos que no hay nadie a quien romperle.

Si nos equivocamos y esto te rompió algo, escríbenos — es un error nuestro y lo resolvemos contigo.

Qué haría falta para que vuelvan. Indexado incremental (que un item se indexe al crearse y al editarse) y que los Mets usen la búsqueda semántica. Con eso, volver a publicarlos es aditivo y no rompe a nadie. No hay fecha.

v1.8.0

La extracción con IA también completa tus campos personalizados

POST /contacts/{id}/extract ya no se limita a los campos de sistema ($name, $email, $city, $country): ahora también completa los campos personalizados de tu workspace, con las mismas reglas de siempre — fill-only, una sola llamada, un solo cobro.

Lo que necesitas saber si integras:

  • Solo campos de tipo text. El modelo devuelve texto, así que los select, currency, date y number quedan afuera por diseño. De los campos del pipeline sembrado, eso deja afuera a etapa, monto, fecha_cierre_estimada y lead_score. etapa además es el juicio del humano y la IA no lo toca nunca.
  • La descripción del campo es la pista que recibe el modelo. Si tu campo presupuesto_estimado tiene descripción *"cuánto dijo que puede gastar"*, eso es literalmente lo que se le pide extraer. Sin descripción se usa el label. Vale la pena escribir buenas descripciones: son el prompt.
  • Tope de 10 campos por llamada, con los de sistema primero. Cada campo alarga el prompt y el prompt se cobra; si tienes 40 campos personalizados, no se piden los 40.
  • lead_score_motivo no entra aunque sea text: lo escribe la IA en el mismo JSON, junto con el score.

Todo lo demás del endpoint sigue igual: filled mantiene su forma y significado (los campos que se completaron), el score se sigue devolviendo en lead_score, y con crm_extraction_enabled apagado no se llama al modelo ni se cobra.

Descubre tus campos y sus tipos con met.variables.fieldDefinitions().

v1.7.0

Enriquecimiento web del contacto, con las fuentes a la vista

  • POST /contacts/{id}/enrich (contacts:write): busca al contacto en Google por su nombre —más el dominio de su correo corporativo, si lo tiene— lee las páginas que encuentra y completa solo los campos vacíos: empresa, cargo, $city, $country.

Devuelve filled con lo que completó y sources con las URLs de donde salió. Las mismas fuentes quedan en una nota del contacto.

Tres cosas que importan si integras:

  • Es bajo demanda, no automático. Sale a internet, cuesta bastante más que analizar una conversación y puede encontrar a un homónimo. Que lo dispare alguien que mira el resultado es parte del diseño.
  • Las fuentes no son decoración. El modelo tiene instrucción de omitir cualquier dato que no pueda confirmar que es de esta persona —un {} vacío es una respuesta correcta y preferible a una equivocada— pero los nombres se repiten. Por eso se citan: para que un humano pueda auditar de dónde salió cada dato.
  • Fill-only, y lo que dijo el contacto gana. Si él dijo que trabaja en X, gana X aunque una página diga otra cosa. La web nunca pisa la conversación.

Un contacto sin $name no se busca (un teléfono suelto no es una consulta). Los dominios de correo personales —gmail, hotmail, outlook…— no entran en la búsqueda: no dicen dónde trabaja alguien y solo traen homónimos.

Consume energía (crm_enrichment, una tarea ejecutada por uso). Requiere que el workspace tenga el enriquecimiento habilitado (crm_enrichment_enabled, apagado por defecto); apagado devuelve filled: [] sin buscar ni cobrar.

  • Campos sembrados: empresa y cargo (text) se suman a los del pipeline en contact_field_definitions, con la misma regla fill-only que el resto: si tu workspace ya tenía un campo con ese field_key, se respeta el tuyo. Descubrilos con met.variables.fieldDefinitions().

En la app aparece como el botón «Enriquecer desde la web» en el perfil del contacto, y un interruptor en Ajustes → Conversaciones que decide si ese botón existe — la palanca del admin para controlar cuánta energía se va en enriquecer a mano.

v1.6.0

Calificación de leads con IA, en la misma llamada que la extracción

  • POST /contacts/{id}/extract (contacts:write) ahora devuelve lead_score además de filled. Es un entero de 0 a 100: qué tan cerca de comprar está el contacto según lo que se dijo en la conversación. El motivo queda en el campo lead_score_motivo del contacto — un número sin justificación no es accionable.

No es un endpoint nuevo ni una ejecución nueva. Es la misma lectura del transcript y el mismo débito de energía que ya hacía la extracción: el modelo lee la conversación una vez y devuelve las dos cosas. Si venías llamando a este endpoint, ya estás recibiendo el score — filled sigue significando exactamente lo mismo y no cambió su forma.

Dos diferencias con los campos de ficha que importan si integras:

  • El score no es fill-only. Los campos ($name, $email, $city, $country) solo se llenan si están vacíos: el humano y el webhook mandan sobre el modelo. El score se recalcula en cada llamada, porque es un juicio sobre la conversación, no un hecho — si no se recalculara quedaría congelado en la foto del primer cierre.
  • La IA nunca toca etapa. Ese campo es el juicio del humano sobre el embudo y le pertenece. lead_score es el juicio de la IA. Cada uno manda en el suyo, y por eso son campos distintos.

lead_score viaja siempre en la respuesta, incluso como null (análisis apagado, sin saldo, o conversación sin mensajes): no hace falta distinguir "no hay score" de "la clave no vino".

  • Campos sembrados: lead_score (number) y lead_score_motivo (text) se suman a los del pipeline en contact_field_definitions. Como el resto del seed son fill-only: si tu workspace ya tenía un campo con ese field_key, se respeta el tuyo y no se toca. Descubrilos con met.variables.fieldDefinitions(); se ordenan y agrupan en el tablero como cualquier campo personalizado.

En la app, el interruptor de Ajustes → Conversaciones pasó de «Completar datos del contacto con IA» a «Analizar el contacto con IA»: es el mismo flag (crm_extraction_enabled), la misma llamada y el mismo cobro, ahora haciendo las dos cosas. No se agregó un interruptor aparte para la calificación porque no hay una decisión aparte que tomar.

v1.5.0

Detección de duplicados, fusión de contactos e import con mapeo de columnas

  • GET /contacts/{id}/duplicates (contacts:read): posibles duplicados por teléfono (tolerante al formato: con o sin código de país) o email (ignorando mayúsculas). Solo lectura. Cada candidato trae can_merge y, cuando es false, el blocked_reason.
  • POST /contacts/{id}/merge (interno por ahora — es destructivo y el contrato está en maduración): el contacto de la URL sobrevive, el de loser_id se fusiona y se borra. dry_run es true por defecto: la llamada te devuelve qué movería (mensajes, tareas, recordatorios, roles, envíos, items vinculados) sin tocar nada; hay que mandar dry_run: false explícito para que ocurra. Es atómico y fill-only: el superviviente conserva todos sus valores y del duplicado solo toma lo que le falte.

Una restricción que importa si integras: no se pueden fusionar dos contactos que tengan conversación propia. El webhook entrante identifica al contacto por $user_ns + $channel_id, y un contacto solo puede tener un $user_ns por canal — fusionarlos perdería una identidad y el próximo mensaje entrante desharía la fusión. El endpoint lo rechaza con el motivo en vez de hacerlo a medias.

  • Import con campos personalizados: POST /contacts/import acepta custom por fila, con el field_key pelado como clave ({ "custom": { "etapa": "Nuevo", "monto": "6300000" } }). Solo se aceptan claves que ya existan como field-definition del workspace — descubrilas con met.variables.fieldDefinitions(). Las demás se descartan en silencio: una clave inventada quedaría en data sin ninguna vista que la muestre.

En la app, el modal de importar ahora mapea las columnas: subes el archivo como lo tengas, Met adivina qué es cada columna por el encabezado (incluye sinónimos frecuentes de otros CRMs) y tú corriges lo que haga falta. Antes exigía que el archivo tuviera columnas llamadas exactamente name y phone.