Changelog · página 7 de 9

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 14 jul 2026 – 21 jul 2026. También puedes suscribirte al feed.

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.

v1.4.0

Línea de tiempo del contacto, tareas vinculadas y extracción de datos con IA

Tres cosas nuevas en el dominio de contactos, todas aditivas:

  • GET /contacts/{id}/timeline (contacts:read, met.contacts.timeline()): la historia del contacto en una sola lista — cambios de campo, eventos de sistema (estado, asignación), notas internas y tareas de seguimiento. Cada entrada trae un type discriminador (field_change, system_event, note, task, contact_created), así que no hay que inferir nada de la forma del dato. No incluye los mensajes de la conversación: para eso está messages(). Cursor: before = created_at de la última entrada recibida.
  • Tareas vinculadas al contacto: POST /workspaces/{ws}/tasks acepta contact_id, y GET /workspaces/{ws}/tasks?contact_id= devuelve las de un contacto. La respuesta sigue siendo un array plano — no cambiamos la forma de un endpoint ya publicado. Las tareas traen responsable, fecha límite, pasos y comentarios del módulo de Tareas: no hay entidad nueva.
  • POST /contacts/{id}/extract (contacts:write): completa con IA los campos vacíos del contacto con lo que se dijo en la conversación. Dos cosas importantes: consume energía (aparece en tu ledger como crm_extraction) y es fill-only — nunca pisa un dato existente. Está apagada por defecto: si el workspace no la habilitó en Ajustes, el endpoint devuelve filled: [] sin llamar al modelo ni cobrarte.

También, si construiste automatizaciones: se retiró del catálogo el disparador contact.status_changed. Estaba deshabilitado y ningún evento lo emitía nunca — no se podía usar, así que no hay nada que migrar. Para reaccionar a un cambio de estado o de etapa, usa contact.field_updated con el campo correspondiente ($status o etapa), que funciona hoy.

v1.3.0

Embudo comercial en contactos, totales agregados y catálogo de campos personalizados

El CRM estrena pipeline. La decisión de diseño importa para quien integra: no hay una entidad "oportunidad" nueva. El embudo son campos personalizados del contacto, así que se leen y escriben con las tools de contactos que ya usas, y todo lo que construiste encima sigue funcionando igual.

  • Tres campos sembrados en cada workspace: etapa (lista: Nuevo, Calificado, Propuesta, Negociación, Ganado, Perdido), monto (moneda) y fecha_cierre_estimada (fecha). Se siembran solo si no existían: si tu workspace ya tenía un campo con ese field_key, se respeta el tuyo — ni sus opciones ni su etiqueta se tocan. Mover una etapa es un PATCH /contacts/{id} normal, y dispara los mismos disparadores de contact.field_updated de siempre.
  • GET /contacts/pipeline-summary (scope contacts:read, met.contacts.pipelineSummary()): conteo y suma por etapa, agregados en la base sobre todos tus contactos, no sobre una página. Los contactos sin etapa se agrupan en __no_value__. Acepta stage_field/amount_field si tu workspace usa otros nombres.
  • met.variables (scopes variables:read/variables:write): los endpoints de campos personalizados ya eran públicos pero el SDK no los exponía. met.variables.fieldDefinitions() es la única forma de descubrir qué campos tiene un workspace y qué valores acepta cada lista — imprescindible para escribir en ellos sin adivinar.
  • Tipo de campo currency: nuevo tipo para las definiciones de campos de contacto. El código ISO de la moneda va en options (['COP']) y el valor del contacto es solo el número, para que sume limpio.
  • Documentada la convención de claves, que era invisible y es la causa más común de errores al escribir: los campos de sistema llevan $ ($name, $status); los personalizados, su field_key pelado (etapa, monto). Mandar $etapa no falla — guarda una clave que ninguna vista lee, y el dato queda invisible. Está en la guía de contactos.
v1.2.0

Paginado por cursor en contactos (arregla el barrido completo) y vistas de tareas en el contrato

Si barrías tu lista de contactos con contacts.iterate() del SDK, solo recibías la primera página y el barrido terminaba declarando éxito. Este release lo arregla y deja el recurso alineado con el paginado por cursor del resto de la API (§3.9):

  • GET /contacts devuelve has_more. El campo faltaba, así que el auto-paginado del SDK lo leía como undefined y cortaba en la página 1. total y page siguen intactos: si ya consumes la respuesta actual, nada cambia para ti.
  • Cursor estable con starting_after. GET /contacts?order=id&starting_after=<id del último contacto> pagina por cursor sobre un orden inmutable. El orden por defecto (recencia de la conversación) se reordena con cada mensaje entrante: un barrido sobre él se saltea contactos que saltan a la primera página a mitad del recorrido. Por eso el cursor exige order=id, y hay que pedirlo desde la primera página.
  • contacts.iterate() ya lo hace solo. El SDK pide order=id y encadena el cursor por ti; actualiza a la última versión y el barrido recorre todos tus contactos. Si paginabas a mano con page, sigue haciéndolo — pero para exports completos prefiere el cursor.

También, sin cambios de código: el contrato publicado estaba desactualizado y ya incluye las vistas de tareas (/workspaces/{ws}/task-views, /task-views/{viewId}, …/task-views/reorder, con scopes tasks:read / tasks:write). Están vivas desde el 15 de julio; lo que faltaba era su documentación en el spec.

v1.1.0

Autoría de flujos por API key, filtros de items, PATCH parcial y adjuntar por URL

Un paquete de mejoras nacido de integrar cotizadores y pipelines comerciales contra la API pública. Reduce fricción en automatización-como-código y en la gestión de datos no-code:

  • Autoría de flujos y automatizaciones con la key de servicio. Las mutaciones de flujos (POST/PATCH/DELETE/duplicar en /workspaces/{ws}/flows[/{id}]) y de disparadores (POST/PATCH/DELETE en /workspaces/{ws}/automations) ya no exigen el JWT de sesión: se emiten con los scopes nuevos flows:write y automations:write. Permite versionar la automatización como código y editarla desde CI.
  • Filtro y orden de items en el servidor. GET /collections/{id}/items acepta filter[campo]=valor (o filter[campo][op]=valor, con opeq, neq, gt, gte, lt, lte, like, ilike, in) y sort=-updated_at,campo. Ya no hace falta bajar la colección entera y filtrar en el cliente. Sobre claves de data, las comparaciones son de texto.
  • PATCH parcial de un item. PATCH /collections/{id}/items/{itemId} fusiona (merge) las claves recibidas en data sin reemplazar el objeto completo — evita las carreras del PUT. Complementa el PATCH …/field (un solo campo) existente.
  • Adjuntar un archivo por URL. POST /items/{itemId}/files acepta además un cuerpo JSON { url, filename }: Met descarga el archivo (con protección anti-SSRF) y lo guarda, sin streamear los bytes por multipart. La respuesta ahora incluye download_url (URL firmada de descarga directa).
  • dry_run en envíos de plantilla. POST /channels/{id}/whatsapp/send-template?dry_run=true valida plantilla y parámetros y devuelve el mensaje resuelto sin entregarlo — útil para verificar el payload en desarrollo.
  • Firma HMAC saliente en el nodo _Petición HTTP_. El nodo http.request puede firmar su cuerpo con un secreto del workspace y enviar la firma en X-Met-Signature (HMAC-SHA256), verificable en el destino — reemplaza el pasar una clave en la query.
  • Contrato más rico. Los DTOs de envíos de WhatsApp y de disparadores traen properties y examples; el POST con Idempotency-Key queda documentado en el contrato (reintento seguro sin duplicados).