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.
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.