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
| Secret | What it is |
|---|---|
BOTPRESS_WEBHOOK_ID | Webhook 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_URLplus 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
waitForBotReplyand 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
- Copy this integration from the registry — see Registry integrations.
- Open the copied agent and paste the Botpress webhook ID into the required
BOTPRESS_WEBHOOK_IDsecret field. Spec27 stores it as a project secret. - 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.