Skip to main content
Use this guide when you need to show that the same end user can interact across two channels and that both channels produce unified, auditable logs. The pattern below uses a WhatsApp webhook as channel one and a web app as channel two. Both harnesses send the same X-On-Behalf-Of end-user ID and both write JSONL audit events keyed by the same response_id and conversation_id.
Use the same MKA1 API key in both harnesses. Two different API keys represent two different MKA1 API users. Using the same X-On-Behalf-Of value is not enough if the WhatsApp harness and web harness authenticate with different API keys. For this evidence flow, both channels must share the same MKA1 API key and the same X-On-Behalf-Of end-user ID.
For the base request pattern, see generate a response. If you need to create or manage a reusable conversation ID per end user, see manage conversations.

Architecture

WhatsApp end user message -> WhatsApp webhook receives the message -> your server calls mka1.llm.responses.create with store: true -> your server logs the full stored response object and response_id -> your server sends the reply back to WhatsApp -> your web app requests the same response_id from your backend -> your backend calls mka1.llm.responses.get -> your backend logs the web retrieval against the same response_id and conversation_id
Keep your API key on the server. The browser should call your backend route, not the MKA1 API directly.

Use one end-user ID across both channels

The X-On-Behalf-Of value is the key that ties the channels together at the end-user level. Use the same stable end-user ID in both harnesses. You must also use the same shared MKA1 API key in both harnesses. Examples:
  • WhatsApp webhook maps a phone number to user_123.
  • Web app session for the same person also resolves to user_123.
  • Both harnesses authenticate with the same MKA1 API key.
If the X-On-Behalf-Of value changes between channels, the logs are no longer auditable as one end-user conversation. If the API key changes between channels, the requests belong to different MKA1 API users even when X-On-Behalf-Of matches.

WhatsApp harness: store the response and log it

Start with one shared SDK wrapper that both harnesses use. This keeps authentication, conversation creation, response retrieval, and response creation consistent across channels.
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: "meetkai:functionary-pt",
      input: message,
      conversation: conversationId,
      previousResponseId,
      store: true,
      stream: false,
      metadata: {
        channel,
      },
    },
    {
      headers: getAuthHeaders(userId),
    }
  );
};
Then use that wrapper in your WhatsApp webhook. Create the conversation once for the end user, reuse it across turns, and capture the audit event from the stored response.
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
  );
});
This is the critical evidence point for channel one:
  • WhatsApp is the ingress channel.
  • The request is stored with store: true.
  • The full response object is logged in an open format.
  • The log includes response_id, conversation_id, and end_user_id.

Web harness: retrieve the same stored response

The web app should call your own backend. That backend can retrieve the same response with mka1.llm.responses.get and log the retrieval as a second channel event.
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); // same value, e.g. "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,
  });
});
Minimal browser call:
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);
This is the critical evidence point for channel two:
  • The web app is a separate channel from WhatsApp.
  • It retrieves the same stored MKA1 response by response_id.
  • It logs that retrieval into the same JSONL audit stream.
  • The retrieved object still carries the same end-user and conversation linkage.

Unified audit export in an open format

Example export:
{"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":"meetkai:functionary-pt","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":"meetkai:functionary-pt","store":true,"status":"completed","conversation":{"id":"conv_abc123"},"metadata":{"channel":"whatsapp"}}}

Authentication requirement for shared logs

For this multichannel demonstration, all of the following must match across the WhatsApp and web harnesses:
  • the same MKA1 API key
  • the same X-On-Behalf-Of end-user ID
  • the same stored response_id
  • the same conversation_id when you use conversations
If either harness uses a different API key, you are no longer showing one shared MKA1 API user context across channels.