Saltar al contenido principal
Utiliza esta guía cuando necesites demostrar que el mismo usuario final puede interactuar a través de dos canales y que ambos canales producen registros unificados y auditables. El patrón a continuación utiliza un webhook de WhatsApp como canal uno y una aplicación web como canal dos. Ambos harnesses envían el mismo ID de usuario final X-On-Behalf-Of y ambos escriben eventos de auditoría JSONL asociados al mismo response_id y conversation_id.
Utiliza la misma clave de API MKA1 en ambos harnesses. Dos claves de API diferentes representan dos usuarios diferentes de la API MKA1. Usar el mismo valor de X-On-Behalf-Of no es suficiente si el harness de WhatsApp y el de web se autentican con claves de API distintas. Para este flujo de evidencia, ambos canales deben compartir la misma clave de API MKA1 y el mismo ID de usuario final X-On-Behalf-Of.
Para el patrón de solicitud base, consulta generar una respuesta. Si necesitas crear o gestionar un ID de conversación reutilizable por usuario final, revisa gestionar conversaciones.

Arquitectura

Mensaje del usuario final en WhatsApp -> El webhook de WhatsApp recibe el mensaje -> tu servidor llama a mka1.llm.responses.create con store: true -> tu servidor registra el objeto de respuesta completo almacenado y el response_id -> tu servidor envía la respuesta de vuelta a WhatsApp -> tu aplicación web solicita el mismo response_id desde tu backend -> tu backend llama a mka1.llm.responses.get -> tu backend registra la recuperación web contra el mismo response_id y conversation_id
Mantén tu clave de API en el servidor. El navegador debe llamar a tu ruta de backend, no directamente a la API de MKA1.

Usa un solo ID de usuario final en ambos canales

El valor de X-On-Behalf-Of es la clave que une los canales a nivel de usuario final. Utiliza el mismo ID de usuario final estable en ambos harnesses. También debes usar la misma clave de API MKA1 compartida en ambos harnesses. Ejemplos:
  • El webhook de WhatsApp mapea un número de teléfono a user_123.
  • La sesión web para la misma persona también se resuelve como user_123.
  • Ambos harnesses se autentican con la misma clave de API MKA1.
Si el valor de X-On-Behalf-Of cambia entre canales, los registros ya no son auditables como una sola conversación de usuario final. Si la clave de API cambia entre canales, las solicitudes pertenecen a diferentes usuarios de la API MKA1 incluso cuando X-On-Behalf-Of coincide.

Harness de WhatsApp: almacena la respuesta y regístrala

Comienza con un único wrapper SDK compartido que ambos harnesses utilicen. Esto mantiene la autenticación, la creación de conversaciones, la recuperación y creación de respuestas consistente entre canales.
import { SDK } from "@meetkai/mka1";

const getAuthHeaders = (userId: string) => {
  return {
    "X-On-Behalf-Of": userId,
  };
};

const mka1 = new SDK({
  bearerAuth: `Bearer ${process.env.MKA1_API_KEY}`,
});

const createConversation = async ({ userId }: { userId: string }) => {
  return await mka1.llm.conversations.create({}, { headers: getAuthHeaders(userId) });
};

const getResponse = async ({
  userId,
  responseId,
}: {
  userId: string;
  responseId: string;
}) => {
  return await mka1.llm.responses.get({ responseId }, { headers: getAuthHeaders(userId) });
};

const sendUserMessage = async ({
  conversationId,
  userId,
  message,
  previousResponseId,
  channel,
}: {
  conversationId: string;
  userId: string;
  message: string;
  previousResponseId?: string;
  channel: "whatsapp" | "webapp";
}) => {
  return await mka1.llm.responses.create(
    {
      model: "gpt-5",
      input: message,
      conversation: conversationId,
      previousResponseId,
      store: true,
      stream: false,
      metadata: {
        channel,
      },
    },
    {
      headers: getAuthHeaders(userId),
    }
  );
};
Luego utiliza ese wrapper en tu webhook de WhatsApp. Crea la conversación una vez para el usuario final, reutilízala en cada turno y captura el evento de auditoría desde la respuesta almacenada.
import { Router, Request, Response } from "express";
import { OutputMessage } from "@meetkai/mka1/models/components";
import { sendTextMessage } from "./whatsapp";
import { createConversation, sendUserMessage } from "./mka1";

const router = Router();

router.post("/webhook", async (req: Request, res: Response) => {
  res.sendStatus(200);

  const entry = req.body.entry?.[0];
  const changes = entry?.changes?.[0];
  const value = changes?.value;
  const message = value?.messages?.[0];

  if (!message || message.type !== "text") return;

  const senderPhone = message.from;
  const incomingText = message.text.body.trim();
  const userId = await getEndUserIdForPhone(senderPhone); // e.g. "123"

  let conversationId = await getConversationIdForEndUser(userId);

  if (!conversationId) {
    const conversation = await createConversation({ userId });
    conversationId = conversation.id;
    await saveConversationIdForEndUser(userId, conversationId);
  }

  const previousResponseId = await getPreviousResponseIdForConversation(conversationId);

  const response = await sendUserMessage({
    conversationId,
    userId,
    message: incomingText,
    previousResponseId,
    channel: "whatsapp",
  });

  const assistantMessage = response.output.find(
    (item): item is OutputMessage => item.type === "message" && item.role === "assistant"
  );
  const output = assistantMessage?.content.map((item) => item.text).join("") ?? "";

  const auditEvent = {
    timestamp: new Date().toISOString(),
    channel: "whatsapp",
    action: "responses.create",
    end_user_id: userId,
    response_id: response.id,
    conversation_id: response.conversation.id,
    previous_response_id: response.previousResponseId,
    full_response: response,
  };

  await savePreviousResponseIdForConversation(conversationId, response.id);

  await sendTextMessage(
    {
      accessToken: process.env.WHATSAPP_TOKEN!,
      phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
    },
    senderPhone,
    output
  );
});
Este es el punto de evidencia crítico para el canal uno:
  • WhatsApp es el canal de entrada.
  • La solicitud se almacena con store: true.
  • El objeto de respuesta completo se registra en un formato abierto.
  • El registro incluye response_id, conversation_id y end_user_id.

Harness web: recupera la misma respuesta almacenada

La aplicación web debe llamar a tu propio backend. Ese backend puede recuperar la misma respuesta con mka1.llm.responses.get y registrar la recuperación como un evento de segundo canal.
import express from "express";
import { getResponse } from "./mka1";

const app = express();

app.get("/api/multichannel/:responseId", async (req, res) => {
  const responseId = req.params.responseId;
  const userId = await getEndUserIdForWebSession(req); // mismo valor, por ejemplo "123"
  const response = await getResponse({ userId, responseId });

  const auditEvent = {
    timestamp: new Date().toISOString(),
    channel: "webapp",
    action: "responses.get",
    end_user_id: userId,
    response_id: response.id,
    conversation_id: response.conversation.id,
    previous_response_id: response.previousResponseId,
    full_response: response,
  };

  res.json({
    response_id: response.id,
    conversation_id: response.conversation.id,
    response,
  });
});
Llamada mínima desde el navegador:
const record = await fetch(`/api/multichannel/${responseId}`).then((res) => res.json());

console.log(record.response_id);
console.log(record.conversation_id);
console.log(record.response);
Este es el punto de evidencia crítico para el canal dos:
  • La aplicación web es un canal separado de WhatsApp.
  • Recupera la misma respuesta almacenada de MKA1 por response_id.
  • Registra esa recuperación en el mismo flujo de auditoría JSONL.
  • El objeto recuperado sigue manteniendo el mismo vínculo de usuario final y conversación.

Exportación de auditoría unificada en formato abierto

Ejemplo de exportación:
{"timestamp":"2026-03-30T19:10:14.000Z","channel":"whatsapp","action":"responses.create","end_user_id":"123","response_id":"resp_abc123","conversation_id":"conv_abc123","previous_response_id":null,"full_response":{"id":"resp_abc123","model":"gpt-5","store":true,"status":"completed","conversation":{"id":"conv_abc123"},"metadata":{"channel":"whatsapp"}}}
{"timestamp":"2026-03-30T19:11:02.000Z","channel":"webapp","action":"responses.get","end_user_id":"123","response_id":"resp_abc123","conversation_id":"conv_abc123","previous_response_id":null,"full_response":{"id":"resp_abc123","model":"gpt-5","store":true,"status":"completed","conversation":{"id":"conv_abc123"},"metadata":{"channel":"whatsapp"}}}

Requisito de autenticación para registros compartidos

Para esta demostración multicanal, todo lo siguiente debe coincidir entre los harnesses de WhatsApp y web:
  • la misma clave de API MKA1
  • el mismo ID de usuario final X-On-Behalf-Of
  • el mismo response_id almacenado
  • el mismo conversation_id cuando utilices conversaciones
Si cualquiera de los harnesses utiliza una clave de API diferente, ya no estarás mostrando un contexto de usuario de API MKA1 compartido entre canales.