Skip to main content

Botpress Chat API Agent

This integration creates a Botpress Chat API conversation, sends your input, and returns the next bot reply. It keeps the conversation alive across turns within an evaluation session. Use it when the assistant you want to evaluate is built in Botpress and exposed through the Botpress Chat Integration.

Required secrets

SecretWhat it is
BOTPRESS_WEBHOOK_IDWebhook ID from the Botpress Chat Integration configuration.

Configure Botpress

Complete the Botpress setup first, then use the webhook ID when you configure the copied agent in Spec27.

1. Configure the bot

Create or open the Botpress bot you want to test. Configure its instructions, knowledge bases, workflows, actions, and any external tools in Botpress. Spec27 sends text turns to the bot and records the replies; it does not configure the bot's behavior for you.

2. Enable Chat Integration

In Botpress Studio, open the bot's Integrations Hub, find the Chat Integration, and enable it. This is the integration that exposes the Botpress Chat API endpoints used by this registry agent.

3. Publish the bot

Publish the bot after enabling the Chat Integration and after making behavior changes you want to evaluate. Spec27 sends messages to the bot version exposed by the Chat Integration, so unpublished draft changes may not be included in the test.

4. Copy the webhook ID

In Botpress, open the Chat Integration configuration and copy the webhook ID. Do not use a public share URL or embed snippet. If Botpress shows a webhook URL such as https://webhook.botpress.cloud/<webhook-id>, use only the final webhook ID value.

This webhook ID is the value for BOTPRESS_WEBHOOK_ID in Spec27.

Turn support

Supports both single-turn and multi-turn evaluations. A single-turn evaluation sends one message and returns the next bot reply. In a multi-turn evaluation, the same Botpress user and conversation are preserved across turns within the evaluation session, so it suits multi-turn evaluation.

Limitations and notes

  • The bot's Chat Integration must be enabled for the webhook to accept messages.
  • The Botpress bot must be configured with the instructions, knowledge, workflows, actions, and external tool access it needs before you test it.
  • Evaluations create Botpress users, conversations, and messages. Review your Botpress workspace plan, message limits, AI usage, and charges from any connected integrations or providers before running large evaluations.
  • This integration sends text messages through the Chat API. Channel-specific UI, file uploads, authenticated user context, or custom widget-only behavior may require a custom agent.
  • The agent waits for the bot to reply; very slow bot responses may time out.

How the agent works

The integration you copy is a single JavaScript function. This walkthrough follows the real code so you can adapt the same pattern to connect a custom agent to your own service.

The entry point is an exported process function. Its input is the evaluation turn's text. The body is short because it orchestrates a multi-step flow: read the secret, ensure a session, send the message, then poll for the bot's reply.

export async function process(input) {
const webhookId = requiredEnv("BOTPRESS_WEBHOOK_ID");
const text = parseInput(input);
const activeSession = await getSession(webhookId);
const sentMessage = await sendTextMessage(activeSession, text);
return waitForBotReply(activeSession, sentMessage);
}

The secret is read off the global env object through a requiredEnv helper that throws if the value is missing — this is the Required secret above. Input is normalized by parseInput, which also throws on empty text.

const requiredEnv = (key) => {
const value = String(env[key] ?? "").trim();
if (!value) {
throw new Error(`${key} is required`);
}
return value;
};

const parseInput = (input) => {
const text = String(input ?? "").trim();
if (!text) {
throw new Error("input text is required");
}
return text;
};

All HTTP goes through one requestJson helper that builds the URL from a base and the webhook ID, sets accept/content-type, attaches the per-user x-user-key when present, and enforces a timeout via AbortController. It throws on non-JSON, non-2xx, or unexpected shapes.

response = await fetch(`${CHAT_API_BASE_URL}/${webhookId}${path}`, {
method: options.method ?? "GET",
headers: {
accept: "application/json",
...(options.body === undefined ? {} : { "content-type": "application/json" }),
...(options.userKey === undefined ? {} : { "x-user-key": options.userKey }),
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
signal: controller.signal,
});

A session is built once and reused. createSession first POSTs /users to create a user (capturing user.id and the returned key), then POSTs /conversations with that user key to get a conversation.id.

const createSession = async (webhookId) => {
const userBody = await requestJson(webhookId, "/users", {
method: "POST",
body: { name: "Spec27 Eval User", id: crypto.randomUUID() },
});
const userId = userBody.user?.id;
const userKey = userBody.key;
// ... validate userId and userKey

const conversationBody = await requestJson(webhookId, "/conversations", {
method: "POST",
userKey,
body: { id: crypto.randomUUID() },
});
const conversationId = conversationBody.conversation?.id;
// ... validate conversationId

return { conversationId, userId, userKey, webhookId };
};

Sending a turn POSTs /messages with the conversation id and a text payload, returning the created message (its id and createdAt are used to recognise the reply later).

const sendTextMessage = async ({ conversationId, userKey, webhookId }, text) => {
const body = await requestJson(webhookId, "/messages", {
method: "POST",
userKey,
body: {
conversationId,
payload: { type: "text", text },
},
});
// ... return body.message
};

The bot replies asynchronously, so the agent polls. waitForBotReply lists the conversation's messages on an interval until the deadline, and returns the text of the first message that qualifies as a new bot reply.

const waitForBotReply = async (activeSession, sentMessage) => {
const deadline = Date.now() + POLL_TIMEOUT_MS;
while (Date.now() <= deadline) {
const messages = await listMessages(activeSession);
for (const message of messages) {
const reply = botReplyFromMessage(message, {
sentAt: sentMessage.createdAt,
sentMessageId: sentMessage.id,
userId: activeSession.userId,
});
if (reply) {
returnedMessageIds.add(reply.id);
return reply.text;
}
}
await sleep(POLL_INTERVAL_MS);
}
throw new Error(/* timeout */);
};

The reply's text is read from the message payload (text, then markdown, then title) by payloadText.

Preserving state across turns

Two module-level values live outside process and persist across turns within a session:

let session = null;
const returnedMessageIds = new Set();

getSession reuses the existing session when the webhook id matches, so the same user and conversation carry across turns rather than being recreated.

const getSession = async (webhookId) => {
if (session?.webhookId === webhookId) {
return session;
}
session = await createSession(webhookId);
return session;
};

returnedMessageIds tracks every reply already handed back, and botReplyFromMessage skips the message you just sent, anything from your own user id, anything created before your send, and any id already in the set — so each turn returns only the new bot reply.

const botReplyFromMessage = (message, { sentMessageId, sentAt, userId }) => {
// ...
if (message.id === sentMessageId || returnedMessageIds.has(message.id)) {
return null;
}
if (typeof message.userId === "string" && message.userId === userId) {
return null;
}
// ... skip messages created before sentAt, require non-empty payload text
};

The error-handling convention here is to throw on any failure (missing secret, bad response, timeout) rather than return an error object — process lets those errors propagate.

Adapting it for your own agent

To point this skeleton at a different service, change:

  • The base URL and request paths (here, CHAT_API_BASE_URL plus the webhook id).
  • The auth header (here, the per-user x-user-key).
  • The session-creation steps and request body shapes for your service.
  • The polling step — keep it if replies are asynchronous, or drop waitForBotReply and return directly if your service responds synchronously.
  • The response field your reply text is read from (here, payloadText).
  • The secret keys you declare and read off env.

Set it up

  1. Copy this integration from the registry — see Registry integrations.
  2. Open the copied agent and paste the Botpress webhook ID into the required BOTPRESS_WEBHOOK_ID secret field. Spec27 stores it as a project secret.
  3. Preview the agent to confirm it works. Messages you submit through the preview should appear in Botpress Conversations and be answered by your Botpress bot.