Webhooks
Configure Slack, Discord, Microsoft Teams, Google Chat, and generic webhook channels.
How a webhook channel works
Every webhook channel, whatever the platform, is reachable at one route:
https://YOUR_HOST/webhooks/<project>/<provider>/<slug>
<project>is the project the connection belongs to. Usedefaultfor the default project, or another project’s slug to keep that connection isolated to its own project.<provider>is the platform name:whatsapp,slack,discord,msteams, orgooglechat.<slug>is the unique name you gave the connection.
A GET to that URL answers the provider’s verification handshake (Pepe echoes
back the challenge the platform sends when you first register the URL). A POST
is an inbound event. On a POST, Pepe resolves the connection, verifies the
request signature against your configured secret, parses out the message, runs
the bound agent, and delivers the reply through the provider’s own API. The
agent work runs in the background so the platform gets its acknowledgement
immediately (providers like Meta retry a slow webhook).
There is a single generic route. Adding a new provider never adds a new endpoint.
PEPE_PUBLIC_URL so the callback URLs the CLI prints
are complete. For a quick tunnel while testing, run pepe serve
--tunnel.Slack, Discord, Microsoft Teams, Google Chat
These providers are configured through the guided setup (or the dashboard), which asks for exactly the fields each one needs and prints the callback URL to register:
pepe setup
Choose the channel option, pick the provider and the agent, and enter the
credentials (a ${ENV_VAR} reference is accepted for any secret). Each has its
own page with its provider-specific fields and setup steps:
Slack, Discord, Microsoft Teams,
Google Chat. This page covers what’s shared by all of them
(and by WhatsApp).
Group @mentions
Slack, Microsoft Teams and Google Chat support group/channel conversations,
where the connection answers only when @mentioned by default (a direct
message always reaches the agent regardless). Set require_mention: false on
the connection to answer every message in every channel it’s in. Or, without
touching that connection-wide setting, waive it for a single channel from
inside that channel:
/mention off # this channel only, until /new - no @mention needed to be answered
/mention on # back to requiring an @mention
/mention # show the current setting
Since a channel command still has to be addressed to run in the first place,
the first /mention off needs an actual @mention (@bot /mention off);
after that, the channel no longer needs one until /new. The waiver lives on
that channel’s own conversation, not the connection, so it never leaks into
any other channel. WhatsApp and Discord don’t gate on mentions today (always
answered), so /mention is a no-op there.
Switching models
The /model and /models commands let people check or change which AI model
answers them. They work only on an admin-mode connection with commands
enabled (see the mode comparison in Channels); on support,
they are treated as plain text. /models lists the models available to the
connection’s project; /model shows the current one, or changes it:
/model openrouter # ask whether to switch just this chat or everyone
/model openrouter session # switch for this conversation only
/model openrouter global # switch for everyone this connection talks to
Switching globally, for everyone the connection talks to, is reserved for
trainers (the same trusted list that controls memory); everyone else in an
allowed conversation can only switch their own conversation. Set
model_switch_locked: true on the connection to turn it off entirely for
non-trainers. This is the same mechanism WhatsApp uses; Telegram’s version
adds a tappable picker instead of typed commands.
Under the hood: the provider contract
Every webhook channel is one small module that implements the same contract, so they all behave consistently and a new platform is a new module rather than a new route. The callbacks are:
nameandlabel: the provider’s URL segment and its human name.config_schema: the fields the dashboard renders to configure a connection.verify: answer theGETverification handshake.authenticate: verify the signature on an inboundPOSTagainst the connection’s secret and the raw request body. A request that fails is dropped.parse: normalize the platform’s payload into zero or more plain messages. Status updates and delivery receipts are ignored.respond(optional): produce a synchronous answer when the protocol demands one before any agent work, such as Slack’surl_verificationchallenge or Discord’s ping and deferred acknowledgement.deliver: send a text reply back to the sender.deliver_file(optional): send a file as an attachment.
If you write a plugin that implements this contract, it registers as a new
provider under its own name, reachable at the same /webhooks/... route with
no extra wiring.