Mattermost Bot Setup and OpenClaw Configuration
Set up a self-hosted Mattermost bot with a bot token and WebSocket events, then wire it into OpenClaw. Covers install, base URL, config, and private network notes.
Read this when
- Setting up Mattermost
- Debugging Mattermost routing
Status: plugin available for download (bot token plus WebSocket events). Support covers channels, private channels, group DMs, and DMs. Mattermost is a team messaging platform you can host yourself (mattermost.com).
Install
npm registry
openclaw plugins install @openclaw/mattermost
Local checkout
openclaw plugins install ./path/to/local/mattermost-plugin
See Plugins for details.
Quick setup
Ensure plugin is available
Run the command above to install @openclaw/mattermost. Verify the application result before moving on.
Create a Mattermost bot
Set up a Mattermost bot account, then copy its bot token and invite the bot to whichever teams and channels it needs to read.
Copy the base URL
Grab the Mattermost base URL (for example, https://chat.example.com). Any trailing /api/v4 gets removed for you.
Configure OpenClaw and start the gateway
Smallest working config:
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
For non-interactive setups:
openclaw channels add --channel mattermost --bot-token <token> --http-url https://chat.example.com
Note
When Mattermost is self-hosted on a private, LAN, or tailnet address: outbound Mattermost API calls go through an SSRF guard that blocks private and internal IPs unless you opt in. Enable it with
channels.mattermost.network.dangerouslyAllowPrivateNetwork: true(per account, usechannels.mattermost.accounts.<id>.network.dangerouslyAllowPrivateNetwork).
Native slash commands
Native slash commands must be turned on explicitly. Once active, OpenClaw registers oc_* slash commands across every team the bot belongs to and accepts callback POSTs on the gateway HTTP server.
{
channels: {
mattermost: {
commands: {
native: true,
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL).
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
},
},
}
Commands registered: /oc_status, /oc_model, /oc_models, /oc_new, /oc_help, /oc_think, /oc_reasoning, /oc_verbose, /oc_queue. If nativeSkills: true is set, skill commands get registered as /oc_<skill> too.
Behavior notes
nativeandnativeSkillsboth default to"auto", and for Mattermost that resolves to disabled. Set them explicitly totrue.- The default for
callbackPathis/api/channels/mattermost/command. - When
callbackUrlis left out, OpenClaw works outhttp://<gateway.customBindHost or localhost>:<gateway.port, default 18789><callbackPath>on its own. Wildcard bind hosts (0.0.0.0,::) fall back tolocalhost. - In multi-account deployments,
commandsmay live at the top level or beneathchannels.mattermost.accounts.<id>.commands; account-level values take precedence over top-level fields. - Slash commands already registered by other integrations under the same trigger are not disturbed, since registration skips them. Commands the bot itself created are updated or recreated whenever the callback URL changes.
- Validation of command callbacks uses the per-command tokens Mattermost returns when OpenClaw registers the
oc_*commands. - Before accepting any callback, OpenClaw re-checks the current Mattermost command registration, so tokens left over from deleted or regenerated slash commands are rejected without needing a gateway restart.
- If the Mattermost API cannot confirm a command is still current, callback validation fails closed. Failed validations are cached for a short time, concurrent lookups are merged, and fresh lookups are rate-limited per command to limit replay attempts.
- Slash callbacks also fail closed when registration did not succeed, startup was incomplete, or the callback token does not match the registered token of the resolved command. A token that is valid for one command cannot pass upstream validation for another.
- Accepted callbacks get an ephemeral "Processing..." acknowledgement, and the actual answer follows as an ordinary message.
Reachability requirement
The Mattermost server must be able to reach the callback endpoint.
- Leave
callbackUrlunset rather thanlocalhostunless Mattermost shares a host or network namespace with OpenClaw. - Do not point
callbackUrlat your Mattermost base URL unless that URL reverse-proxies/api/channels/mattermost/commandthrough to OpenClaw. - To check quickly, use
curl https://<gateway-host>/api/channels/mattermost/command; a GET should come back with405 Method Not Allowedfrom OpenClaw, not404.
Mattermost egress allowlist
When your callback targets private, tailnet, or internal addresses, add the callback host or domain to Mattermost's ServiceSettings.AllowedUntrustedInternalConnections.
Entries must be hosts or domains, never full URLs.
- Correct:
gateway.tailnet-name.ts.net - Incorrect:
https://gateway.tailnet-name.ts.net
Environment variables (default account)
On the gateway host, use these env vars if that suits you better:
MATTERMOST_BOT_TOKEN=...MATTERMOST_URL=https://chat.example.com
Note
Env vars take effect for the default account only (
default). Any other account has to be configured through config values.A workspace
.envcannot setMATTERMOST_URL; refer to Workspace .env files.
Chat modes
Mattermost answers DMs on its own. What happens in channels is governed by chatmode:
oncall (default)
Replies in channels happen only when the bot is @mentioned.
onmessage
Every message posted in a channel gets a reply.
onchar
A trigger prefix at the start of a message causes a reply.
Config example:
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"], // default
},
},
}
Notes:
- An explicit @mention always gets a response, even with
onchar. channels.mattermost.requireMentionremains valid, thoughchatmodeis the option to prefer. Settings forgroups.<channelId>.requireMentionon a single channel take priority over either one.- Once the bot has posted a visible reply inside a channel thread, subsequent messages in that thread no longer need a fresh @mention or an
oncharprefix, which keeps multi-turn thread exchanges going. That participation memory lasts 7 days from the bot's last reply in the thread and survives gateway restarts. Threads where the bot has merely been a bystander are not affected; to require an explicit mention again, begin a new top-level message. - To keep participated-thread follow-ups from skipping mention gating, set
channels.mattermost.implicitMentions.threadParticipation: false. For account-level overrides, usechannels.mattermost.accounts.<id>.implicitMentions. Since Mattermost never emitsreplyToBotorquotedBotfacts, neither flag does anything here.
Threading and sessions
Whether channel and group replies remain in the main channel or open a thread beneath the triggering post is decided by channels.mattermost.replyToMode.
off(default): a reply goes into a thread only if the inbound post already sits in one.first: when a top-level channel/group post arrives, a thread opens under it and the conversation moves to a thread-scoped session.allandbatched: for Mattermost these behave the same asfirst, since follow-up chunks and media stay in the same thread once a thread root exists.- Direct messages fall back to
offby default, even whenreplyToModeis configured.
To change the mode for direct, group, or channel chats, use channels.mattermost.replyToModeByChatType. Opt direct messages into threading by setting direct:
off(default): direct messages remain non-threaded within a single rolling session.first,all, orbatched: every top-level direct message opens a Mattermost thread backed by its own fresh session.
{
channels: {
mattermost: {
replyToMode: "all",
replyToModeByChatType: {
direct: "first",
},
},
},
}
Notes:
- A thread-scoped session takes the triggering post id as its thread root.
firstandalldo the same thing, because follow-up chunks and media continue in the same thread once Mattermost has a thread root.- Overrides per chat type win over
replyToMode. Where nodirectoverride exists, deployments already in place keep flat, non-threaded DMs. - Following a restart or session reset, thread-scoped conversations pull recent messages from Mattermost whenever their pending history is cold. DMs with threading turned on are included; flat DMs behave as before. Sender access and context visibility are respected during recovery, the triggering message is left out, and the process is limited by
historyLimit, a 200-post server window, and a five-second deadline. A short-lived server failure will not hold up the new message forever.
Access control (DMs)
- Default:
channels.mattermost.dmPolicy = "pairing"(a pairing code is issued to unknown senders). Other options:allowlist,open,disabled. - Approve through:
openclaw pairing list mattermostopenclaw pairing approve mattermost <CODE>
- Public DMs:
channels.mattermost.dmPolicy="open"together withchannels.mattermost.allowFrom=["*"](the wildcard is enforced by the config schema). channels.mattermost.allowFromtakes user ids (recommended) as well asaccessGroup:<name>entries. See Access groups.
Channels (groups)
- Default:
channels.mattermost.groupPolicy = "allowlist"(mention-gated). - Allowlist senders via
channels.mattermost.groupAllowFrom(user IDs are recommended). channels.mattermost.groupAllowFromtakesaccessGroup:<name>entries. Refer to Access groups.- Per-channel mention overrides are configured under
channels.mattermost.groups.<channelId>.requireMention, orchannels.mattermost.groups["*"].requireMentionfor a default. - Text commands may come after the mention:
@<bot-username> /newexecutes/newwhencommands.textis on (which is the default). A bare/newis run by Mattermost itself as a Mattermost slash command rather than being posted. @usernamematching can change and is active only whenchannels.mattermost.dangerouslyAllowNameMatching: true.- Open channels:
channels.mattermost.groupPolicy="open"(mention-gated). - Order of resolution:
channels.mattermost.groupPolicy, followed bychannels.defaults.groupPolicy, then"allowlist". - Runtime note: when the
channels.mattermostsection is absent entirely, group checks fail closed at runtime togroupPolicy="allowlist"(even ifchannels.defaults.groupPolicyis set), and a one-time warning is logged.
Example:
{
channels: {
mattermost: {
groupPolicy: "open",
groups: {
"*": { requireMention: true },
"team-channel-id": { requireMention: false },
},
},
},
}
Targets for outbound delivery
Pair these target formats with openclaw message send or cron/webhooks:
| Target | Delivers to |
|---|---|
channel:<id> | Channel by id |
channel:<name> or #channel-name | Channel by name, searched across the teams the bot belongs to |
user:<id> or mattermost:<id> | DM with that user |
@username | DM (username resolved via the Mattermost API) |
A single outbound message carries no more than one attachment; to send several files, issue separate sends.
Use channels.mattermost.mediaMaxMb to cap each inbound download and outbound attachment in MiB. accounts.<id>.mediaMaxMb takes precedence over the channel root, and agents.defaults.mediaMaxMb then provides the fallback. When no cap is configured, inbound downloads keep their 8 MiB default and outbound media keeps the shared loader defaults. Outbound images can be optimized. Once a cap is configured, a download or upload failure causes the send to fail rather than posting the unchecked original URL. With no cap configured, the existing URL fallback stays available.
Warning
In Mattermost, bare opaque IDs (such as
64ifufp...) are ambiguous (user ID vs channel ID).OpenClaw resolves them user-first:
- When the ID exists as a user (
GET /api/v4/users/<id>succeeds), OpenClaw sends a DM, resolving the direct channel through/api/v4/channels/direct.- If not, the ID is handled as a channel ID.
For deterministic behavior, always use the explicit prefixes (
user:<id>/channel:<id>).
DM channel retry
When a Mattermost DM target requires OpenClaw to resolve the direct channel first, transient direct-channel creation failures are retried by default.
Tune this behavior globally for the Mattermost plugin with channels.mattermost.dmChannelRetry, or per account with channels.mattermost.accounts.<id>.dmChannelRetry. Defaults:
{
channels: {
mattermost: {
dmChannelRetry: {
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
timeoutMs: 30000,
},
},
},
}
Notes:
- Only DM channel creation (
/api/v4/channels/direct) is affected, not every Mattermost API call. - Retries apply exponential backoff with jitter, covering transient failures like rate limits, 5xx responses, and network or timeout errors.
- Client errors in the 4xx range other than
429are considered permanent and are never retried.
Preview streaming
Mattermost streams thinking, tool activity, and partial reply text into a draft preview post that finalizes in place once the final answer is safe to send. Under partial mode, the preview updates on the same post id rather than flooding the channel with per-chunk messages. Under block mode, the preview rotates between completed text and tool-activity blocks, so earlier blocks remain visible as their own posts instead of being overwritten by the next one. Media/error finals cancel pending preview edits and fall back to normal delivery instead of flushing a throwaway preview post.
Preview streaming is on by default in partial mode. Configure it with channels.mattermost.streaming.mode (legacy scalar/boolean streaming values are migrated by openclaw doctor --fix):
{
channels: {
mattermost: {
streaming: { mode: "partial" }, // off | partial | block | progress
},
},
}
Streaming modes
partial(default): a single preview post that is edited as the reply grows, then finalized with the complete answer.blockrotates the preview between completed text and tool-activity blocks, so each block remains visible as its own post rather than being overwritten in place. Parallel and consecutive tool updates share the current tool-activity post.progressdisplays a status preview while generating and posts only the final answer at completion.offturns preview streaming off. Withstreaming.block.enabled: true, completed assistant blocks are still delivered as normal block replies (separate posts) instead of one coalesced final post.
Streaming behavior notes
- When a stream cannot be closed out in place, such as when the post is deleted partway through, OpenClaw sends a brand new final post instead, so the reply is never dropped.
- Removing a plan clears a preview that would otherwise be empty; if a replacement plan is set, it receives a new preview even when the text has not changed. Once the turn finishes, any failed deletions of older previews are retried one more time, without touching the finalized reply. Should deletion keep failing, the stale preview can linger; the cleanup failure shows up in verbose logs.
- Payloads that contain only thinking are kept out of channel posts, and that includes text arriving as a
> Thinkingblockquote. To see thinking on other surfaces, set/reasoning on; the Mattermost final post carries the answer alone. - The channel-mapping matrix lives in Streaming.
Read channel history (message tool)
Reading posts from a channel the configured Mattermost bot can reach is done with message action=read or the CLI:
openclaw message read --channel mattermost --target channel:<channelId> --limit 5 --json
- Results come back in Mattermost's ordered post list and carry normalized
timestampMsandtimestampUtcfields. limitstarts at 60 and cannot exceed Mattermost's maximum of 200. For pagination, usebefore=<postId>orafter=<postId>, but never both cursors at once.- Direct operator calls depend on Mattermost's channel membership and the
read_channelpermission. A 403 from the provider stays a normal, visible tool error. - The current account may perform delegated reads of the current Mattermost conversation. Reading across channels in a delegated way also demands the destination channel ID under
channels.mattermost.groups, a"*"groups entry, orgroupPolicy: "open". Delegated DM reads across accounts and channels fail closed. - History reads stay off unless enabled. Turn them on with
channels.mattermost.actions.messages: true, and override the setting per account usingchannels.mattermost.accounts.<id>.actions.messages. - The same access rules govern the bundled plugin and the official plugin installed via npm or ClawHub. For delegated reads, the calling run and the plugin registration must both stay active.
Reactions (message tool)
- Pair
message action=reactwithchannel=mattermost. - The Mattermost post id is
messageId. emojitakes names such asthumbsupor:+1:(colons may be omitted).- To take a reaction back, set
remove=true(boolean). - Reaction add and remove events get forwarded to the routed agent session as system events, and the same DM/group policy checks used for messages apply.
Examples:
message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup
message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup remove=true
Config:
channels.mattermost.actions.reactions: turns reaction actions on or off (default true).- Per-account override:
channels.mattermost.accounts.<id>.actions.reactions.
Interactive buttons (message tool)
Messages can carry clickable buttons. A click delivers the user's selection to the agent, which can then respond.
Buttons originate from the semantic presentation payload, both in ordinary agent replies and in message action=send. Value buttons become Mattermost interactive buttons, URL buttons stay visible in the message text, and select menus get downgraded to readable text.
Options offered by an ask_user question are rendered as buttons too, and tapping one answers that question right away. Typing an answer still works, and any option the Gateway does not index remains in the prose: the "Other…" choice, plus any prompt asking more than one question or whose question is multi-select, secret, or lacks two to four distinct options. Any other typed presentation action (command, callback, approval) remains readable text on Mattermost instead of turning into a button: a click there arrives at the agent as a message rather than executing the action, so a control would behave differently from its label.
message action=send channel=mattermost target=channel:<channelId> presentation={"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]}]}
Presentation button fields:
-
label(string, required), Display label (alias:text). -
value(string), Value returned on click, used as the action ID (aliases:callback_data,callbackData). Needed for a clickable button unlessurlis set. -
url(string), Link button; shown aslabel: urltext in the message body rather than as an interactive button. -
style(primary" | "secondary" | "success" | "danger), Button style. Values Mattermost does not support get its default styling.
To announce button support in the agent system prompt, add inlineButtons to the channel capabilities:
{
channels: {
mattermost: {
capabilities: ["inlineButtons"],
},
},
}
When a button is clicked:
Access check
Whoever clicks must clear the same DM/group policy checks as a message sender; clicks that fail get an ephemeral notice and are discarded.
Buttons replaced with confirmation
Every button is swapped for a confirmation line (for example, "✓ Yes selected by @user").
Agent receives the selection
The selection reaches the agent as an inbound message (along with a system event), and the agent responds.
Implementation notes
- Button callbacks are verified with HMAC-SHA256 (automatic, no config required).
- A click replaces the entire attachment block, so all buttons disappear at once; removing only some is not possible.
- Action IDs that contain hyphens or underscores get sanitized automatically (a Mattermost routing limitation).
- If a click's
action_idmatches no action on the original post, it is rejected with403("Unknown action").
Config and reachability
channels.mattermost.capabilities: an array of capability strings. To turn on the buttons tool description inside the agent system prompt, add"inlineButtons".channels.mattermost.interactions.callbackBaseUrl: an optional external base URL used for button callbacks (such ashttps://gateway.example.com). Set this when the gateway's bind host is not directly reachable from Mattermost.- When running several accounts, the same field can be set under
channels.mattermost.accounts.<id>.interactions.callbackBaseUrlas well. - Should
interactions.callbackBaseUrlbe left out, OpenClaw builds the callback URL fromgateway.customBindHost+gateway.port(default 18789), then falls back tohttp://localhost:<port>. The callback path is/mattermost/interactions/<accountId>. - Reachability requirement: the Mattermost server must be able to reach the button callback URL.
localhostonly works when Mattermost and OpenClaw share the same host or network namespace. channels.mattermost.interactions.allowedSourceIps: a source-IP allowlist for button callbacks. With it unset, only loopback sources (127.0.0.1,::1) pass, so any remote Mattermost server has to be allowlisted here, otherwise its clicks come back rejected with403. When a reverse proxy sits in front, setgateway.trustedProxiestoo so the real client IP is taken from forwarded headers.- If the callback target lives on a private, tailnet, or internal network, add its host/domain to Mattermost
ServiceSettings.AllowedUntrustedInternalConnections.
Direct API integration (external scripts)
Rather than routing through the agent's message tool, external scripts and webhooks can publish buttons straight through the Mattermost REST API. OpenClaw's message tool is the preferred route. For direct integrations, import buildButtonAttachments from @openclaw/mattermost/api.js; when posting raw JSON instead, stick to these rules:
Payload structure:
{
channel_id: "<channelId>",
message: "Choose an option:",
props: {
attachments: [
{
actions: [
{
id: "mybutton01", // alphanumeric only - see below
type: "button", // required, or clicks are silently ignored
name: "Approve", // display label
style: "primary", // optional: "default", "primary", "danger"
integration: {
url: "https://gateway.example.com/mattermost/interactions/default",
context: {
action_id: "mybutton01", // must match button id
action: "approve",
// ... any custom fields ...
_token: "<hmac>", // see HMAC section below
},
},
},
],
},
],
},
}
Warning
Critical rules
- Attachments belong in
props.attachments, not at the top levelattachments(which is silently ignored).- Each action requires
type: "button"- omit it and clicks vanish without any error.- Each action requires an
idfield - actions lacking IDs are ignored by Mattermost.- The action
idmust be alphanumeric only ([a-zA-Z0-9]). Underscores and hyphens break Mattermost's server-side action routing (a 404 comes back). Remove them beforehand.context.action_idhas to match the button'sid; any click whoseaction_idis not present on the post gets refused by the gateway.context.action_idis mandatory - without it the interaction handler responds with 400.- The callback source IP must be allowed (see
interactions.allowedSourceIpsabove).
HMAC token generation
Button clicks are verified by the gateway using HMAC-SHA256. Tokens produced by external scripts have to match the gateway's verification logic:
Derive the secret from the bot token
HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken), hex-encoded.
Build the context object
Assemble the context object from every field except _token.
Serialize with sorted keys
Serialize using recursively sorted keys and no spaces (nested objects are canonicalized by the gateway as well, yielding compact JSON).
Sign the payload
HMAC-SHA256(key=secret, data=serializedContext)
Add the token
Place the resulting hex digest into the context as _token.
Python example:
import hmac, hashlib, json
secret = hmac.new(
b"openclaw-mattermost-interactions",
bot_token.encode(), hashlib.sha256
).hexdigest()
ctx = {"action_id": "mybutton01", "action": "approve"}
payload = json.dumps(ctx, sort_keys=True, separators=(",", ":"))
token = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
context = {**ctx, "_token": token}
Common HMAC pitfalls
- By default Python's
json.dumpsinserts spaces ({"key": "val"}). Passseparators=(",", ":")so the output is compact like JavaScript's ({"key":"val"}). - Sign every context field (
_tokenexcluded). The gateway removes_tokenand then signs whatever remains. Signing only some fields leads to a silent verification failure. - Use
sort_keys=True- keys are sorted by the gateway before signing, and Mattermost may reorder context fields while storing the payload. - The secret should be derived from the bot token (deterministically), not from random bytes. Both the process creating buttons and the gateway verifying them must use the same secret.
Directory adapter
A directory adapter ships with the Mattermost plugin, resolving channel and user names through the Mattermost API. Thanks to it, #channel-name and @username targets work in openclaw message send and in cron/webhook deliveries.
Nothing needs configuring - the adapter pulls the bot token from the account config.
Multi-account
Several accounts can be configured under channels.mattermost.accounts:
{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" },
},
},
},
}
Values set on an account take precedence over top-level fields; when no account is named, channels.mattermost.defaultAccount determines which one gets used.
Troubleshooting
No replies in channels
Make sure the bot has joined the channel, then either mention it (oncall), apply a trigger prefix (onchar), or configure chatmode: "onmessage".
Auth or multi-account errors
- Verify the bot token, the base URL, and whether the account is turned on.
- Trouble with multiple accounts: environment variables only take effect for the
defaultaccount. - Mattermost hosts on a private network or LAN require
network.dangerouslyAllowPrivateNetwork: true(by default the SSRF guard rejects private IPs).
Native slash commands fail
Unauthorized: invalid command token.: the callback token was not accepted by OpenClaw. Usual reasons:- slash command registration did not succeed, or finished only partly, during startup
- the callback is reaching the wrong gateway or account
- stale commands in Mattermost still point at an earlier callback target
- the gateway was restarted but slash commands were not reactivated
- HTTP 429 when under load: callbacks that rely on an outgoing OAuth connection, or that pass through a proxy which drops the
Authorizationheader, carry the command token in the body only. Such requests draw from the same limited anonymous pool and may be throttled once it fills up. Keep Mattermost's originalAuthorization: Tokenheader intact through proxies so that recognized command credentials get their own capacity. - Should native slash commands stop responding, search the logs for
mattermost: failed to register slash commandsormattermost: native slash commands enabled but no commands could be registered. - When
callbackUrlis left out and the logs warn that the callback resolved to a loopback URL such ashttp://localhost:18789/..., that address is likely reachable only if Mattermost shares a host or network namespace with OpenClaw. Configure an explicitcommands.callbackUrlthat can be reached from outside instead.
Buttons issues
- Buttons show up as white boxes, or not at all: the button data is not well formed. Every presentation button requires both a
labeland avalue(any button lacking one of them is discarded). - Buttons render, but clicking does nothing: confirm the gateway can be reached from the Mattermost server, that the Mattermost server IP appears in
channels.mattermost.interactions.allowedSourceIps(without it, only loopback is allowed), and thatServiceSettings.AllowedUntrustedInternalConnectionscontains the callback host for private targets. - Buttons give a 404 when clicked: the button
idmost likely has hyphens or underscores in it. Mattermost's action router fails on IDs that are not alphanumeric. Stick to[a-zA-Z0-9]. - Gateway logs
rejected callback source: the click originated from an IP not covered byinteractions.allowedSourceIps. Add the Mattermost server or your ingress to the allowlist, and setgateway.trustedProxieswhen running behind a reverse proxy. - Gateway logs
invalid _token: HMAC mismatch. Make sure every context field is signed (not just some), that keys are sorted, and that the JSON is compact (no spaces). Refer to the HMAC section above. - Gateway logs
missing _token in context: the button's context is missing the_tokenfield. Include it while assembling the integration payload. - Gateway rejects the click with
Unknown action: no actionidon the post matchescontext.action_id. Assign the same sanitized value to both. - Agent offers no buttons: append
capabilities: ["inlineButtons"]to the Mattermost channel config.
Related
- Channel routing - session routing for messages
- Channels Overview - all supported channels
- Groups - group chat behavior and mention gating
- Pairing - DM authentication and pairing flow
- Security - access model and hardening