Overview & concepts
Coviqo bots are programs that run on your server and talk to Coviqo over HTTPS: they post messages, answer slash commands, react to buttons and forms, receive events, join voice rooms and read meeting transcripts – always within the permissions a space admin granted them.
| Who sets it up | Where the logic runs | Account | |
|---|---|---|---|
| Developer bots | Anyone signed in, in Settings → Developer | Your server: token API + signed HTTPS interaction URL and / or event delivery | Own BOT account with a token |
| Incoming webhooks | Space admins with bots.manage | The sender (CI, monitoring …) – posts into one room only | No bot account; the creator is the author |
| Sign in with Coviqo | Anyone signed in, in Settings → Developer → Login apps | Your website or app – OAuth 2.0 / OpenID Connect, identity only (details) | No account – people sign in with theirs |
| Coviqo bots | Space admins with bots.create | Inside Coviqo (welcome, role picker, polls, RSS, AutoMod …) – no code | Own BOT account, no token |
Bot accounts and the managed role
- Every developer bot has its own BOT account: it cannot sign in, has no password, is not discoverable and shows a “BOT” badge. Bots do not count as members or seats; the plan limit
botsPerSpaceapplies instead. - Installing a bot into a space creates exactly one managed role “Bot: <name>” right above the default role. Everything the bot does runs with the permissions of that role in the space and room – including room overrides, encryption, mentions, realtime, activity and audit. A bot can never do more than its role.
- The role cannot be given to people and cannot be edited in the role editor; admins change it in Space settings → Bots (tab “Permissions”, per room in tab “Rooms”). Bot members cannot be kicked, banned or timed out – admins pause or remove the bot instead.
Permissions
You request permissions in the developer area (default for a new bot: room.view, chat.view, chat.send, chat.react). When installing, the admin sees each requested permission as a checkbox and may grant fewer. Rules:
- Nobody grants a bot more than they hold themselves.
- Never grantable to bots:
administrator,space.manage,space.manage_billing,space.manage_roles,space.view_audit,bots.manage,bots.create,automations.manage,voice.listen_in,voice.radio.broadcast,voice.priority_speaker,room.moderated.manage,voice.bypass_moderation,voice.grant_speak,voice.effects.manage,map.edit,map.pull_members,map.megaphone,emojis.create,emojis.manage,voice.soundboard,voice.soundboard.manage,webpages.manage,feeds.manage(400BOT_PERMISSION_FORBIDDEN). - Moderation-like permissions get an extra warning in the consent dialog:
moderate.kick,moderate.ban,moderate.timeout,moderate.reports,chat.manage,chat.mention_everyone,space.manage_members,space.invite,teams.manage,room.manage,room.create,voice.mute_others,voice.move_members,files.manage,tasks.manage,events.manage,meetings.manage,tickets.view,tickets.handle,tickets.manage. - A missing permission answers 403
MISSING_PERMISSIONwithdetails.permission; a room the bot cannot see answersMISSING_PERMISSIONroom.view. - The installations list in the developer area shows when a space granted fewer permissions than requested.
Data access (intents)
Besides permissions, a bot needs data access for privileged data. Standard intents are always granted; privileged ones only when you request them and an admin approves each one per installation. Plan-gated intents only work while the space’s plan includes them (approvals stay stored after a downgrade). Enforced on every event delivery and every REST read (403 BOT_INTENT_REQUIRED with details.intent).
| Intent | Level | Gives access to |
|---|---|---|
COMMANDS | standard | Slash commands, buttons, forms, own messages |
MEMBERS | standard | Member list (names, roles) and member events |
MESSAGE_CONTENT | privileged | Text of all messages in rooms the bot can read (events and message history) – the bot’s own messages are readable without it (author=self, since 1.6.0) |
CONTENT_READ | privileged | Read files, notes and tasks (events and REST reads) |
PRESENCE | privileged | Presence of members (presence.updated, presence=true) |
VOICE_LISTEN | privileged, plan voiceBots | Hear people in voice rooms – shown to everybody in the room |
TRANSCRIPTS | privileged, plan botTranscriptAccess | Finished meeting transcripts and summaries of rooms an admin released |
TICKETS_READ | privileged | Read tickets, replies and ticket events (with the ticket permissions of the bot role) |
TICKETS_WRITE | privileged | Open tickets, reply, internal notes, change status / priority / category / assignee / fields / description, hand own tickets over to a member |
ROLES_ASSIGN | privileged | |
VOICE_ACTIVITY | privileged | |
LINKED_ACCOUNTS | privileged | |
ROOMS_MANAGE | privileged | |
EVENT_RSVPS | privileged | |
TICKETS_CREATE_ON_BEHALF | privileged | |
TEAMS_MANAGE | privileged | Change team settings – name, join policy, application question (with teams.manage, since 1.4.0) |
TEAM_APPLY_ON_BEHALF | privileged | Apply to teams on behalf of any active member – shown “via” the bot (since 1.4.0); withdraw applications the bot created (since 1.5.0) |
EVENT_RSVP_ON_BEHALF | privileged | Answer events on behalf of any active member – shown “via” the bot (since 1.4.0); remove their answer (since 1.5.0) |
TICKETS_REPLY_ON_BEHALF | privileged | Reply to tickets on behalf of their reporter or a participant – shown “via” the bot (since 1.7.0, together with TICKETS_WRITE) |
Members see which bots read what: the room panel “Bots in this room” lists e.g. “This bot reads messages in #support”.
Quickstart
- Open the developer area: Settings → Developer. The section appears once you have a bot; before that, switch on Developer mode in Settings → Appearance, or open
/settings/developerdirectly. - Create a bot (name 2–32 characters, at most 25 bots per account). The token (
cqb_…) is shown exactly once – store it as a secret. Copy the public key (64 hex characters) from the bot’s page. - Build the interaction endpoint: an HTTPS endpoint that verifies the signature of every request, answers invalid signatures with 401 and answers
PINGwith{ "type": "PONG" }. The easiest way iscreateInteractionHandlerof the SDK. - Enter the interaction URL in the developer area. Coviqo checks it right away: a correctly signed PING must be answered with status 200 and
{ "type": "PONG" }, then a PING with a wrong signature must be answered with 401 – otherwise 400BOT_INTERACTION_URL_VERIFICATION_FAILED(details.steppongorsignature). Bots that only use the WebSocket gateway can skip this. - Register slash commands with PUT /bot/commands (or in the developer area).
- Fill in requested permissions and data access, operator, server location, privacy policy URL and “Uses AI”; optionally make the bot publicly installable.
- Share the install link
https://app.coviqo.com/bots/install/<applicationId>. A space admin withbots.managepicks a space, the permissions and the data access – done.
curl -H "Authorization: Bot $COVIQO_BOT_TOKEN" https://app.coviqo.com/api/v1/bot/meimport { createServer } from 'node:http';
import { createInteractionHandler, reply } from './coviqo-bot.mjs';
const { COVIQO_PUBLIC_KEY } = process.env; // 64 hex characters from the developer area
createServer(
createInteractionHandler({
publicKey: COVIQO_PUBLIC_KEY,
onCommand: (i) => (i.command.name === 'ping' ? reply('Pong', { ephemeral: true }) : reply('Unknown command')),
}),
).listen(8787);cloudflared tunnel --url http://localhost:8787 or ngrok http 8787).Finding IDs & endpoints
Every object is addressed by an opaque string ID. Switch on Developer mode (Settings → Appearance; saved with your account, off by default) and the app shows them:
| ID | Where in the app |
|---|---|
| Application ID, bot user ID | Settings → Developer → your bot → IDs and endpoints (always visible) – also GET /bot/me |
| Space ID, installation ID | Same page under Installations – also GET /bot/spaces and the gateway's READY |
| API base URL, gateway, OpenAPI file | Settings → Developer → This instance – computed from the app's own address, so self-hosted instances show their domain |
| Space, category, room, message, member, team, event, meeting, task, note, file, folder, ticket, DM conversation | Developer mode → Copy ID in the entity's menu (right-click or “…”) |
| Role | Developer mode → Space settings → Roles: right-click a role, or the For developers box (with its mention) – also GET /bot/spaces/:spaceId/roles (isDefault: true marks the default role every member has, since 1.6.0) |
| Invite | Developer mode → Space settings → Invites: fingerprint button – also GET /bot/spaces/:spaceId/invites |
| Incoming webhook | Developer mode → Space settings → Apps & integrations → Bots → Webhooks: webhook ID, URL without the secret, room ID |
| Installed bot (admin view) | Developer mode → bot detail page → Overview: application, installation, bot user and managed role ID |
Endpoints of this instance
REST https://app.coviqo.com/api/v1
Gateway wss://app.coviqo.com/api/v1/bot/gateway
Webhooks https://app.coviqo.com/api/v1/hooks/<webhookId>Syntax that is easy to miss
| What | Syntax |
|---|---|
| Mention a person | <@userId> |
| Mention a role | <@&roleId> |
| Mention a team (and its sub-teams) | <@team:teamId> |
| Everyone / online members | @everyone / @here – needs chat.mention_everyone |
| Link to a room | There is no room mention – link the room URL /s/<spaceSlug>/r/<roomId> |
| Reaction emoji | Unicode, URL-encoded (%F0%9F%91%8D), or a custom emoji of the space as :name: |
POST /api/v1/me/bots/:id/token); the old one stops working at once.Authentication
- Every call to the token API carries
Authorization: Bot <token>. Tokens look likecqb_+ 43 base64url characters (256 bit). Coviqo stores only a SHA-256 hash and shows the last four characters afterwards. - Rotate the token in the developer area (“Generate new token”) – the old token stops working immediately.
- Bot tokens only work under
/api/v1/bot/*(and the gateway); every other API route refuses them. - Order of checks: token valid (401
BOT_AUTH_REQUIRED/BOT_TOKEN_INVALID) → bot not stopped by Coviqo (403BOT_DISABLEDwithdetails.reason) → bot platform switched on (503BOTS_DISABLED) → rate limit per bot (429). In a space where an admin paused the bot, calls answer 403BOT_PAUSED; in a space where it is not installed 404SPACE_NOT_FOUND.
GET /api/v1/bot/spaces HTTP/1.1
Host: app.coviqo.com
Authorization: Bot cqb_…Token API reference
Base URL https://app.coviqo.com/api/v1. JSON in, JSON out. Click an endpoint for parameters, request body, response and errors. Unknown request fields are ignored; clients should ignore unknown response fields. Every endpoint can also answer 401 / 403 BOT_DISABLED / 429 / 503 BOTS_DISABLED. The same description is available as OpenAPI 3.1.
Profile & spaces
Who the bot is, where it is installed, rooms, members and roles.
GET/bot/meBot profile
Profile of the bot plus its public key and the requested permissions / intents. Good first call to check a token.
Response
200 Profile – BotMe
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spacesSpaces the bot is installed in
Response
200 Installations (paused ones included, enabled: false) – BotSpace[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/roomsRooms the bot can see
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 Rooms by position – BotRoom[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/membersMember list
Active members (standard intent MEMBERS: names, roles), ordered by member id. Space identities (per-space names) are applied. joinSource is always present (null without space.invite / space.manage_members or for joins before invite tracking); linkedAccounts only with LINKED_ACCOUNTS and the member’s consent.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
cursor | query | nextCursor of the previous page (a member id) |
limit | query | Page size (default 100, at most 1000) · 1–1000 |
presence | query | true adds presence – needs the PRESENCE intent (403 BOT_INTENT_REQUIRED) |
joinedViaInviteId | query | Only members that joined through this invite – needs space.invite or space.manage_members (403 MISSING_PERMISSION) |
joinedVia | query | Only members that joined this way – needs space.invite or space.manage_members |
Response
200 One page – BotMemberPage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/members/{userId}One member
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
presence | query | true adds presence – needs the PRESENCE intent (403 BOT_INTENT_REQUIRED) |
Response
200 The member – BotMember
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND,MEMBER_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/rolesRoles of the space
All roles, highest first (position 0 = highest). assignable = the bot may assign / remove the role right now (ROLES_ASSIGN, allow-list, below the bot role; admin-level roles only with “allow roles with admin rights”).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 Roles by position – BotRole[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/members/by-discord/{discordUserId}Member by linked Discord account
The active member who linked this Discord account AND released it for the bots of this space (consent "BOTS_LINKED_DISCORD", revocable). Unknown ids and members without consent both answer 404 MEMBER_NOT_FOUND.
Data access: LINKED_ACCOUNTS.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
discordUserId · required | path | Discord user id (snowflake) of a linked account |
Response
200 The member incl. linkedAccounts – BotMember
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND,MEMBER_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/linked-accountsMembers who share a linked account
Active members with a linked account of provider who released it for the bots of this space, ordered by user id. Changes arrive as member.linked_account_updated.
Data access: LINKED_ACCOUNTS.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
provider | query | Provider of the linked account (default discord) |
cursor | query | nextCursor of the previous page (a user id) |
limit | query | Page size (default 100, at most 1000) · 1–1000 |
Response
200 One page – LinkedAccountPage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Messages
Post, edit, delete, react, read the history.
GET/bot/rooms/{roomId}/messagesMessage history
Newest first, system messages left out. author=self (Since 1.6.0) returns only the bot's own messages and works without the MESSAGE_CONTENT intent (room.view + chat.view still needed); everything else needs MESSAGE_CONTENT. Since 1.7.0 every item carries components (buttons / select menus as stored, null without) – customId only on the bot's own messages, other bots' components come without it. Use it to find your own boards again (e.g. delete duplicates by custom id).
Permissions of the bot role: chat.view.
Data access: MESSAGE_CONTENT.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
before | query | Message id – returns older messages (nextCursor of the previous page) |
limit | query | Page size (default 50, at most 100) · 1–100 |
author | query | self = only messages of the bot itself – no MESSAGE_CONTENT intent needed. Since 1.6.0 |
Response
200 One page – HistoryMessagePage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/messagesPost a message
Posts as the bot (encrypted with the space key, mentions, realtime and activity as for people). Optional buttons / select menus.
Permissions of the bot role: chat.view, chat.send.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body BotMessageRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Message to reply to |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
{
"content": "Build **#128** finished.",
"components": [
{
"type": "row",
"components": [
{
"type": "button",
"style": "success",
"label": "Deploy",
"customId": "deploy:128"
},
{
"type": "button",
"style": "link",
"label": "Log",
"url": "https://ci.example.com/128"
}
]
}
]
}Response
201 The new message – Message
Errors
400VALIDATION_FAILED,MESSAGE_EMPTY,BOT_COMPONENTS_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/messages/{messageId}Edit an own message
Permissions of the bot role: chat.view.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
messageId · required | path | Message id |
Request body BotEditMessageRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | null |
Response
200 The edited message – Message
Errors
400VALIDATION_FAILED,BOT_COMPONENTS_INVALID,MESSAGE_EMPTY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,MESSAGE_NOT_AUTHOR404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/messages/{messageId}Delete a message
Own messages always; messages of others need chat.manage (moderation feed + audit with the bot as actor).
Permissions of the bot role: chat.manage (others’ messages).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
messageId · required | path | Message id |
Response
204 Deleted
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/messages/{messageId}/reactions/{emoji}Add a reaction
Permissions of the bot role: chat.view, chat.react.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
messageId · required | path | Message id |
emoji · required | path | Emoji (URL-encoded), e.g. %F0%9F%91%8D or a custom emoji :name: |
Response
200 Reactions of the message – ReactionList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/messages/{messageId}/reactions/{emoji}Remove the bot’s reaction
Permissions of the bot role: chat.view.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
messageId · required | path | Message id |
emoji · required | path | Emoji (URL-encoded), e.g. %F0%9F%91%8D or a custom emoji :name: |
Response
200 Reactions of the message – ReactionList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Slash commands
Register the commands people see after typing "/".
GET/bot/commandsRegistered slash commands
Response
200 Commands – Command[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/commandsReplace all slash commands
The list becomes the complete command set of the bot (bulk overwrite). Spaces see the change at once.
Request body ReplaceCommandsRequest
| Field | Type | Description |
|---|---|---|
commands · required | CommandInput[] | The complete list (replaces all commands), at most 50 · max 50 items |
{
"commands": [
{
"name": "ping",
"description": "Answers with Pong"
},
{
"name": "remind",
"description": "Set a reminder",
"options": [
{
"name": "text",
"description": "What about?",
"type": "string",
"required": true
},
{
"name": "minutes",
"description": "In minutes",
"type": "integer",
"choices": [
{
"name": "5",
"value": 5
},
{
"name": "60",
"value": 60
}
]
}
]
}
]
}Response
200 The new command set – Command[]
Errors
400VALIDATION_FAILED,BOT_COMMANDS_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Interactions
Follow-ups and edits of interaction answers (header X-Coviqo-Interaction-Token).
POST/bot/interactions/{interactionId}/followupsSend a follow-up message
Another answer to an interaction (within 15 minutes). The first follow-up after DEFERRED_MESSAGE ends "<Bot> is thinking …" and becomes the original answer. Note: an empty message answers 502 BOT_INTERACTION_INVALID_RESPONSE and invalid components 502 BOT_COMPONENTS_INVALID on this endpoint (the same check as for interaction responses).
Permissions of the bot role: chat.view, chat.send (public answers).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body BotFollowupRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
ephemeral | boolean | Only the person who used the interaction sees it (not stored). Default: the visibility chosen with DEFERRED_MESSAGE, else false |
expiresAfterSeconds | integer | Ephemeral messages only (400 VALIDATION_FAILED otherwise): the message disappears from the person’s screen after this many seconds (3–3600). Default for the first answer of a deferred interaction: the value given with DEFERRED_MESSAGE. Since 1.7.0 · 3–3600 |
{
"content": "Done."
}Response
201 Id of the new message – FollowupResult
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404BOT_INTERACTION_EXPIRED429RATE_LIMITED,TOO_MANY_REQUESTS502BOT_INTERACTION_INVALID_RESPONSE,BOT_COMPONENTS_INVALID503BOTS_DISABLED
PATCH/bot/interactions/{interactionId}/originalEdit the original answer
Edits the first answer – for COMPONENT interactions without an own answer the message carrying the component. After DEFERRED_MESSAGE without an answer the edit becomes the answer.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body BotEditMessageRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | null |
Response
200 Id of the edited message – EditOriginalResult
Errors
400VALIDATION_FAILED,BOT_COMPONENTS_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,MESSAGE_NOT_AUTHOR404BOT_INTERACTION_EXPIRED,BOT_INTERACTION_NO_ORIGINAL429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/interactions/{interactionId}/originalDelete the original answer
A public answer is deleted like DELETE /bot/messages/{messageId} (realtime message.deleted to the room). An ephemeral answer is removed live from the clients of the person (realtime bot.ephemeral_deleted).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Response
204 Deleted
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED404BOT_INTERACTION_EXPIRED,BOT_INTERACTION_NO_ORIGINAL429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/interactions/{interactionId}/modalOpen a form (modal) for the person
For bots answering through the WebSocket gateway (every interaction arrives deferred) or after DEFERRED_MESSAGE / DEFERRED_UPDATE: opens the form for the person who used the command / component – like a MODAL response. Rules: not for MODAL_SUBMIT interactions (409 BOT_MODAL_NOT_ALLOWED), once per interaction, not after an answer was posted, and only within 60 s of the interaction (409 BOT_MODAL_TOO_LATE). The form replaces "<Bot> is thinking …"; the submission arrives as a new MODAL_SUBMIT interaction (interaction.created on the gateway, or the interaction URL).
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body Modal
| Field | Type | Description |
|---|---|---|
customId · required | string | 1–100 printable ASCII characters, unique within the message / form · 1–100 chars |
title · required | string | 1–45 chars |
fields · required | TextInput | FileInput[] | Text inputs and (since 1.6.0) at most 2 file upload fields · 1–5 items |
{
"customId": "report",
"title": "Report a problem",
"fields": [
{
"type": "text",
"customId": "details",
"label": "What happened?",
"style": "paragraph",
"required": true,
"minLength": 10,
"maxLength": 1000
}
]
}Response
200 The form was sent to the person – OpenModalResult
Errors
400VALIDATION_FAILED,BOT_MODAL_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_NO_ACCESS404BOT_INTERACTION_EXPIRED409BOT_MODAL_NOT_ALLOWED,BOT_MODAL_TOO_LATE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/interactions/{interactionId}/autocompleteAnswer an AUTOCOMPLETE interaction (gateway bots)
For bots without interaction URL that receive AUTOCOMPLETE interactions as interaction.created over the WebSocket gateway: send the suggestions for command.focused – once, within 3 s of the interaction (afterwards the person already got an empty list: 410 BOT_INTERACTION_EXPIRED; a second answer: 409 BOT_AUTOCOMPLETE_ANSWERED). At most 25 choices are passed on; entries with an invalid name or a value that does not match the option type / limits are dropped (listed in dropped). HTTP bots answer AUTOCOMPLETE_RESULT in the response instead. Since 1.4.0.
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body AutocompleteResultData
| Field | Type | Description |
|---|---|---|
choices · required | AutocompleteChoice[] | At most 25 (more are cut off) · max 100 items |
{
"choices": [
{
"name": "Berlin",
"value": "berlin"
},
{
"name": "Bern",
"value": "bern"
}
]
}Response
200 The suggestions were passed on to the person – AutocompleteAnswerResult
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED404BOT_INTERACTION_EXPIRED409BOT_AUTOCOMPLETE_ANSWERED410BOT_INTERACTION_EXPIRED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/interactions/{interactionId}/files/{fileId}/downloadDownload URL of a file submitted with a form
A fresh signed URL (valid 10 minutes, absolute, no bot token needed) for a file the person uploaded in a file upload field and submitted to THIS bot in THIS MODAL_SUBMIT interaction – the payload already carries one per file. No interaction token needed; available for 72 hours after the submit, then Coviqo deletes the files (keep your own copy). Every other file, another interaction, a paused or removed installation or a room the bot no longer sees: 404 BOT_FORM_FILE_NOT_FOUND. End-to-end encrypted files are never delivered (409 FILE_E2EE, defensive). Counts against the normal request limits; audited (BOT_FORM_FILES downloaded, file id – no name). Since 1.6.0.
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
fileId · required | path | File id |
Response
200 Signed URL – FileDownload
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,FILE_INFECTED404BOT_FORM_FILE_NOT_FOUND409FILE_NOT_READY,FILE_E2EE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Events
Event subscriptions per space (event webhook and WebSocket gateway).
GET/bot/spaces/{spaceId}/subscriptionsEvent subscriptions in a space
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 Subscribed event types – BotSubscriptions
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/spaces/{spaceId}/subscriptionsSet the event subscriptions in a space
Replaces the list (event webhook and gateway). Intents and room permissions are still checked for every event.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Request body SubscriptionsRequest
| Field | Type | Description |
|---|---|---|
events · required | string[] | Event types (400 BOT_EVENTS_INVALID with details.events for unknown ones) · max 96 items |
{
"events": [
"message.created",
"member.joined"
]
}Response
200 Subscribed event types – BotSubscriptions
Errors
400VALIDATION_FAILED,BOT_EVENTS_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Direct messages
1:1 conversations of people with the bot.
POST/bot/interactions/{interactionId}/dmAnswer an interaction in a direct message
Opens (or reuses) the 1:1 conversation with the person who used the interaction and posts there – within the 15 minutes of the interaction token. Link buttons only. replyToId is ignored here.
Parameters
| Name | In | Description |
|---|---|---|
interactionId · required | path | id of the interaction payload |
X-Coviqo-Interaction-Token · required | header | token of the interaction payload (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body BotMessageRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Message to reply to |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
Response
201 Conversation and message – InteractionDmResult
Errors
400VALIDATION_FAILED,BOT_COMPONENTS_INVALID,BOT_DM_COMPONENTS_LINK_ONLY,MESSAGE_EMPTY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_DM_BLOCKED404BOT_INTERACTION_EXPIRED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/dmsConversations of the bot
1:1 conversations with the bot, newest activity first.
Parameters
| Name | In | Description |
|---|---|---|
before | query | ISO date – nextCursor of the previous page |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – DmConversationPage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/dms/{conversationId}/messagesMessages of a conversation
Always with text (people wrote to the bot directly) – no intent needed.
Parameters
| Name | In | Description |
|---|---|---|
conversationId · required | path | Conversation id (conversationId of dm.message_created / GET /bot/dms) |
before | query | Message id – older messages |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – MessagePage
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED404DM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/dms/{conversationId}/messagesReply in a conversation
Only after the person wrote in this conversation – never unsolicited – and never while a block exists. Link buttons only.
Parameters
| Name | In | Description |
|---|---|---|
conversationId · required | path | Conversation id (conversationId of dm.message_created / GET /bot/dms) |
Request body BotMessageRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Message to reply to |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
Response
201 The new message – Message
Errors
400VALIDATION_FAILED,BOT_COMPONENTS_INVALID,BOT_DM_COMPONENTS_LINK_ONLY,MESSAGE_EMPTY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_DM_NOT_ALLOWED,BOT_DM_BLOCKED,DM_USER_UNAVAILABLE404DM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Tasks
Tasks of rooms.
GET/bot/rooms/{roomId}/tasksTasks of a room
Permissions of the bot role: tasks.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
status | query | Filter |
Response
200 Tasks – Task[]
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/tasksCreate a task
Same rules as for people (tasks.create).
Permissions of the bot role: tasks.create.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body CreateTaskRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
description | string | null | max 10000 chars |
assigneeId | string | null | User id of a member |
dueDate | string | null | date-time |
priority | "LOW" | "MEDIUM" | "HIGH" | "URGENT" | |
tabId | string | null | Task list ("tab") of the room – omitted / null = the default tab |
categoryId | string | null | Category (task type) of the room – omitted / null = none |
{
"title": "Review release notes",
"priority": "HIGH",
"dueDate": "2026-10-09T12:00:00.000Z"
}Response
201 The new task – Task
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/tasks/{taskId}One task
Permissions of the bot role: tasks.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
taskId · required | path | Task id |
Response
200 The task – Task
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TASK_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/tasks/{taskId}Change a task
Same rules as for people. Tasks the bot did not create need CONTENT_READ (the answer contains the task).
Data access: CONTENT_READ (for objects the bot did not create).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
taskId · required | path | Task id |
Request body UpdateTaskRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
description | string | null | max 10000 chars |
status | "TODO" | "IN_PROGRESS" | "DONE" | "CANCELED" | |
priority | "LOW" | "MEDIUM" | "HIGH" | "URGENT" | |
dueDate | string | null | date-time |
assigneeId | string | null | |
tabId | string | null | Move the task to another task list of its room – null = the default tab |
categoryId | string | null | Category (task type) of the room – null = none |
Response
200 The task – Task
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TASK_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/tasks/{taskId}/completeComplete a task
Shortcut for PATCH { status: "DONE" }.
Data access: CONTENT_READ (for objects the bot did not create).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
taskId · required | path | Task id |
Response
200 The task – Task
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TASK_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Calendar
Calendar events.
GET/bot/spaces/{spaceId}/eventsCalendar events
Permissions of the bot role: events.view.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
from | query | ISO date – start of the range · date-time |
to | query | ISO date – end of the range · date-time |
roomId | query | Only events of this room |
Response
200 Events – CalendarEvent[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND,ROOM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/eventsCreate a calendar event
Same rules as for people (events.create).
Permissions of the bot role: events.create.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body CreateEventRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
startAt · required | string | date-time |
endAt · required | string | date-time |
description | string | null | max 5000 chars |
allDay | boolean | |
timezone | string | IANA time zone, e.g. Europe/Berlin · max 64 chars |
recurrenceRule | string | null | RFC 5545 RRULE · max 500 chars |
location | string | null | max 500 chars |
requiresRegistration | boolean | |
capacity | integer | null | 1–1000000 |
reminderMinutes | integer[] | Reminders before the start (minutes) · max 5 items |
{
"title": "Release review",
"startAt": "2026-10-09T14:00:00.000Z",
"endAt": "2026-10-09T15:00:00.000Z",
"timezone": "Europe/Berlin"
}Response
201 The new event – CalendarEvent
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/events/{eventId}Change a calendar event
Own events, others’ with events.manage. Since 1.6.0 roomId moves the event to another room of the same space (move, not copy – the id stays): the bot needs events.manage in the old AND the new room (own events: events.create in the new room), must see the new room with events.view (else 404 ROOM_NOT_FOUND), the Events module must be on there and the room not archived; neither room end-to-end encrypted. Recurring series move as a whole. Calendar entries of online meetings move their meeting along (the new room needs "Meetings"). RSVPs, tickets and reminders stay; people who cannot see the new room keep their answer but no longer see the event. At most 10 moves per minute per bot and space (on top of the write limit; 429 RATE_LIMITED, scope botevmove). Audited as EVENT_MOVED with the bot as actor; event.updated carries previousRoomId.
Permissions of the bot role: events.manage (others’ events; for moves in the old and the new room).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
Request body UpdateEventRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
startAt | string | date-time |
endAt | string | date-time |
description | string | null | max 5000 chars |
allDay | boolean | |
timezone | string | IANA time zone, e.g. Europe/Berlin · max 64 chars |
recurrenceRule | string | null | RFC 5545 RRULE · max 500 chars |
location | string | null | max 500 chars |
requiresRegistration | boolean | |
capacity | integer | null | 1–1000000 |
reminderMinutes | integer[] | Reminders before the start (minutes) · max 5 items |
roomId | string | Move the event (a recurring series as a whole) to another room of the same space – RSVPs, tickets and reminders stay. Needs events.manage in the old and the new room (own events: events.create in the new room), the Events module on there, not archived. Rooms only. Since 1.6.0 · max 64 chars |
{
"roomId": "cm0room0000000000000000002"
}Response
200 The event – CalendarEvent
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,E2EE_FEATURE_DISABLED,SPACE_MEETINGS_DISABLED,MEETING_NOT_ORGANIZER404SPACE_NOT_FOUND,EVENT_NOT_FOUND,ROOM_NOT_FOUND409EVENT_CANCELED,ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/events/{eventId}Delete (cancel) a calendar event
Removes the event from all calendars – like DELETE for people this cancels the event (Coviqo never hard-deletes calendar events: RSVPs and tickets stay for the record). Same rules and errors as POST /bot/events/{eventId}/cancel; idempotent.
Permissions of the bot role: events.manage (others’ events).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
Response
204 Canceled
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,MEETING_NOT_ORGANIZER404SPACE_NOT_FOUND,EVENT_NOT_FOUND409ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/events/{eventId}/cancelCancel a calendar event
Same rules as for people: own events (created by the bot), others’ with events.manage. A recurring event is canceled as a whole series. The entry disappears from all calendars (external calendar copies are removed); the calendar entry of an online meeting also cancels the meeting (only meeting organizers / moderators – 403 MEETING_NOT_ORGANIZER). Idempotent – an already canceled event is returned unchanged. Sends event.canceled.
Permissions of the bot role: events.manage (others’ events).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
Response
200 The canceled event (canceledAt set) – CalendarEvent
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,MEETING_NOT_ORGANIZER404SPACE_NOT_FOUND,EVENT_NOT_FOUND409ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/events/{eventId}/rsvpsRSVPs of a calendar event
Who accepted, declined or answered maybe – the same list people with events.view see on the event, never more: the bot must see the room (or hold events.view in the space for space-wide events), entries of online meetings only when the bot may see the meeting, no RSVPs from end-to-end encrypted rooms. Users with their space identity, no e-mail. Coviqo tracks answers, not actual attendance. Changes arrive as event.rsvp_updated.
Permissions of the bot role: events.view.
Data access: EVENT_RSVPS.
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
status | query | Only this answer (default: all) |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 100, at most 500) · 1–500 |
Response
200 One page – EventRsvpPage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,EVENT_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/events/{eventId}/rsvps/{userId}Answer an event on behalf of a member
Since 1.4.0. Going / maybe / declined for a member – the answer shows "via <bot>" until the person answers themselves. The member is named by (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space by exactly this person, no extra data access – or (2) the privileged data access EVENT_RSVP_ON_BEHALF (any active member). The bot must see the event (as for RSVP lists, no end-to-end encrypted rooms); the member must be an active member (no guests, bots, suspended accounts) who sees the event – then every rule of a personal answer applies (canceled events, capacity, tickets, webinar registration). Sends event.rsvp_updated. Extra limit of 10 per minute and space (scope botonbehalf); audited as BOT_MEMBER_ACTION event_rsvp.
Permissions of the bot role: events.view.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
userId · required | path | User id of the member |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body PutEventRsvpRequest
| Field | Type | Description |
|---|---|---|
status · required | "GOING" | "MAYBE" | "DECLINED" | GOING becomes REGISTERED for events with registration |
interactionId | string | Interaction proof: an interaction of this bot by this person – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
{
"status": "GOING"
}Response
200 The answer – EventRsvpResult
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,BOT_ON_BEHALF_INVALID,BOT_INTERACTION_MISMATCH404SPACE_NOT_FOUND,EVENT_NOT_FOUND,BOT_INTERACTION_EXPIRED409E2EE_FEATURE_DISABLED,EVENT_CANCELED,EVENT_FULL,EVENT_TICKET_REQUIRED,WEBINAR_USE_REGISTRATION429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/events/{eventId}/rsvps/{userId}Remove a member’s answer on behalf of the member
Since 1.5.0. Removes the person’s answer entirely – back to “no answer” (meeting invitees stay invited). Same gate as PUT: (1) interaction proof – interactionId (query) plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space by exactly this person – or (2) EVENT_RSVP_ON_BEHALF: then the bot may remove the person’s answer whatever its origin (given by the person, this bot or another bot) – the website is the source of truth for the person’s choice. Answers tied to a ticket (paid or free, VALID / PENDING) are refused with 409 EVENT_RSVP_TICKET_HELD { ticketStatus, paid } – bots never cancel or refund tickets. Canceled events 409 EVENT_CANCELED, webinars with registration 409 WEBINAR_USE_REGISTRATION. Idempotent: without an answer nothing happens (204). Sends event.rsvp_updated with status: null, source: "bot". Shares the extra limit of 10 per minute and space with PUT (scope botonbehalf); audited as BOT_MEMBER_ACTION event_rsvp_removed (with the previous status and origin).
Permissions of the bot role: events.view.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
eventId · required | path | Calendar event id |
userId · required | path | User id of the member |
interactionId | query | Interaction proof: an interaction of this bot by this person – only together with the header X-Coviqo-Interaction-Token |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Response
204 Removed (or there was no answer)
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,BOT_ON_BEHALF_INVALID,BOT_INTERACTION_MISMATCH404SPACE_NOT_FOUND,EVENT_NOT_FOUND,BOT_INTERACTION_EXPIRED409E2EE_FEATURE_DISABLED,EVENT_CANCELED,EVENT_RSVP_TICKET_HELD,WEBINAR_USE_REGISTRATION429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Files
Files: list, metadata, download, three-step upload.
GET/bot/rooms/{roomId}/filesFiles and folders of a room
Permissions of the bot role: files.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
folderId | query | Folder (default: root) |
q | query | Search by name |
Response
200 Folders and files – FileList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/filesStart a file upload
Step 1 of 3: announce the file. Step 2: PUT the raw bytes to uploadUrl with headers (no bot token). Step 3: POST /bot/files/{fileId}/complete – the malware scan starts. Upload size and storage quota as in the web app.
Permissions of the bot role: files.upload.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body UploadFileRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–255 chars |
mimeType · required | string | e.g. application/pdf · max 255 chars, pattern ^[\w.+-]+/[\w.+-]+$ |
size · required | integer | Exact size in bytes (the upload URL accepts at most this many) · min 0 |
folderId | string |
{
"name": "report.pdf",
"mimeType": "application/pdf",
"size": 48213
}Response
201 Upload URL – FileUpload
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID402PLAN_LIMIT_STORAGE403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED413FILE_TOO_LARGE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/files/{fileId}/completeComplete a file upload
Parameters
| Name | In | Description |
|---|---|---|
fileId · required | path | File id |
Response
200 The file (status SCANNING or READY) – File
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND,FILE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/files/{fileId}File metadata
Permissions of the bot role: files.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
fileId · required | path | File id |
Response
200 The file – File
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,FILE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/files/{fileId}/downloadDownload URL
Short-lived signed URL. Files in the malware scan (409 FILE_NOT_READY) or infected (403 FILE_INFECTED) cannot be downloaded.
Permissions of the bot role: files.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
fileId · required | path | File id |
Response
200 Signed URL – FileDownload
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,FILE_INFECTED404SPACE_NOT_FOUND,FILE_NOT_FOUND409FILE_NOT_READY429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Notes
Notes as text with light Markdown.
GET/bot/rooms/{roomId}/notesNotes of a room
Permissions of the bot role: notes.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Notes (content: null) – Note[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/notesCreate a note
Same rules as for people (notes.edit).
Permissions of the bot role: notes.edit.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body CreateNoteRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
content | string | Text with light Markdown (# headings, - lists, paragraphs) · max 100000 chars |
{
"title": "Standup 2026-10-02",
"content": "# Done\n- Release notes\n# Next\n- Review"
}Response
201 The new note – Note
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/notes/{noteId}One note with its text
Permissions of the bot role: notes.view.
Data access: CONTENT_READ.
Parameters
| Name | In | Description |
|---|---|---|
noteId · required | path | Note id |
Response
200 The note – Note
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,NOTE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/notes/{noteId}Change a note
Same rules as for people. Notes the bot did not create need CONTENT_READ. People editing live receive the new text as a change.
Data access: CONTENT_READ (for objects the bot did not create).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
noteId · required | path | Note id |
Request body UpdateNoteRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
content | string | Replaces the whole text as a new version · max 100000 chars |
baseVersion | integer | Optimistic concurrency (409 NOTE_VERSION_CONFLICT); default: the current version · min 1 |
Response
200 The note – Note
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,NOTE_NOT_FOUND409NOTE_VERSION_CONFLICT429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Moderation
Timeouts, kicks and bans with the bot as moderator (role hierarchy applies).
POST/bot/spaces/{spaceId}/members/{userId}/timeoutTime out a member
Permissions of the bot role: moderate.timeout.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Request body TimeoutRequest
| Field | Type | Description |
|---|---|---|
minutes · required | integer | 1 minute … 28 days · 1–40320 |
reason | string | max 1000 chars |
{
"minutes": 10,
"reason": "Spam"
}Response
200 The member – BotMember
Errors
400VALIDATION_FAILED,MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MODERATION_HIERARCHY404SPACE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/spaces/{spaceId}/members/{userId}/timeoutEnd a timeout
Permissions of the bot role: moderate.timeout.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Response
200 The member – BotMember
Errors
400MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MODERATION_HIERARCHY404SPACE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/spaces/{spaceId}/members/{userId}/kickKick a member
Permissions of the bot role: moderate.kick.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Request body KickRequest (optional)
| Field | Type | Description |
|---|---|---|
reason | string | max 1000 chars |
Response
204 Kicked
Errors
400VALIDATION_FAILED,MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MODERATION_HIERARCHY404SPACE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/spaces/{spaceId}/bans/{userId}Ban a person
Permissions of the bot role: moderate.ban.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Request body BanRequest (optional)
| Field | Type | Description |
|---|---|---|
reason | string | max 1000 chars |
durationHours | integer | Temporary ban; omit for a permanent ban · 1–87600 |
deleteMessageHours | integer | Delete the member’s messages of the last hours · 0–168 |
Response
204 Banned
Errors
400VALIDATION_FAILED,MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MODERATION_HIERARCHY404SPACE_NOT_FOUND409BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/spaces/{spaceId}/bans/{userId}Lift a ban
Permissions of the bot role: moderate.ban.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Response
204 Ban lifted
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND,BAN_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/bansActive bans
Bans that have not expired, ordered by user id. Expired bans are lifted every minute (member.unbanned with expired: true).
Permissions of the bot role: moderate.ban.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
cursor | query | nextCursor of the previous page (a user id) |
limit | query | Page size (default 100, at most 1000) · 1–1000 |
Response
200 One page – BanPage
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/moderation-actionsModeration log
Warnings, kicks, bans, unbans and timeouts, newest first.
Permissions of the bot role: moderate.kick | moderate.ban | moderate.timeout | moderate.reports.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId | query | Only actions against this person |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – ModerationActionPage
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Voice
Voice bots (plan voiceBots).
POST/bot/rooms/{roomId}/voice/joinJoin a voice room
Returns LiveKit credentials for the bot identity. The bot is a visible participant with the BOT badge; while it listens everybody in the room sees "Bot is listening". Never in meetings, direct messages or lobby rounds; rooms with external guests only with the room setting "Bots may join despite external guests".
Permissions of the bot role: room.join, voice.speak (publish).
Data access: VOICE_LISTEN.
Plan: voiceBots.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body VoiceJoinRequest
| Field | Type | Description |
|---|---|---|
listen | boolean | Hear the room – needs VOICE_LISTEN |
publish | boolean | Speak (microphone only) – needs voice.speak |
{
"listen": true,
"publish": false
}Response
200 LiveKit URL and token – VoiceJoinResult
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID402PLAN_LIMIT_VOICE_BOTS403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,BOT_VOICE_NOT_ALLOWED,BOT_VOICE_GUESTS,BOT_VOICE_CANNOT_SPEAK404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED,ROOM_FULL429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/voice/leaveLeave a voice room
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
204 Left
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED404ROOM_NOT_FOUND409VOICE_NOT_CONNECTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/voiceVoice state of a room
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Participants the bot may see and its own session – VoiceRoom
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Transcripts
Finished meeting transcripts and summaries (plan botTranscriptAccess).
GET/bot/spaces/{spaceId}/meetingsFinished meetings with transcript / summary
Meetings of the rooms a space admin released to the bot (transcript rooms), newest first.
Data access: TRANSCRIPTS.
Plan: botTranscriptAccess.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
roomId | query | Only this transcript room |
cursor | query | nextCursor of the previous page (a meeting id) |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – MeetingList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID402PLAN_LIMIT_BOT_TRANSCRIPTS403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_TRANSCRIPT_POLICY,BOT_TRANSCRIPT_ROOM_NOT_APPROVED,MISSING_PERMISSION404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/meetings/{meetingId}/transcriptTranscript and summary of a meeting
Every read is audited (BOT_TRANSCRIPT_ACCESSED). The summary is AI-generated (ai: true).
Data access: TRANSCRIPTS.
Plan: botTranscriptAccess.
Parameters
| Name | In | Description |
|---|---|---|
meetingId · required | path | Meeting id |
Response
200 Transcript – MeetingTranscript
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID402PLAN_LIMIT_BOT_TRANSCRIPTS403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_TRANSCRIPT_POLICY,BOT_TRANSCRIPT_ROOM_NOT_APPROVED,MISSING_PERMISSION404SPACE_NOT_FOUND,MEETING_NOT_FOUND409MEETING_STILL_LIVE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Tickets
Private support tickets (room module TICKETS): data access TICKETS_READ / TICKETS_WRITE plus the ticket permissions of the bot role.
GET/bot/rooms/{roomId}/ticketsTickets of a room
Newest change first (updatedAt). tickets.view / handle / manage: every ticket of the room; tickets.create only: the tickets the bot opened itself. Sync: keep the newest updatedAt and pass it as updatedSince.
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create (own tickets).
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
status | query | all (default), open (NEW, IN_PROGRESS, WAITING_FOR_REPLY) or one status |
priority | query | Only this priority |
categoryId | query | Category id or none |
assignee | query | User id, me (the bot) or none |
overdue | query | true: only tickets with an overdue SLA clock |
updatedSince | query | ISO date – only tickets changed since then · date-time |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page (body: null) – TicketPage
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/ticketsOpen a ticket
By default the bot becomes the creator (notifications to the team, SLA, history as for people); the answer is the bot’s own ticket – TICKETS_READ is not needed for it. On behalf of a member the member becomes the creator (reporter) and the ticket shows "created by <bot> on behalf of <member>": (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space (the room may differ), no extra data access; (2) onBehalfOfUserId – any active member of the space, only with the privileged data access TICKETS_CREATE_ON_BEHALF (on top of TICKETS_WRITE), e.g. for ticket forms on an external website. The member must be able to open the ticket as a person (sees the room, tickets.create, not timed out) – never guests, bots or people who left; both given must name the same person. On-behalf tickets count against an extra limit of 10 per minute and space (scope botticket) and are audited with the member. Attachments: upload them first (POST /bot/rooms/{roomId}/tickets/attachments) and pass the ids in attachmentIds.
Permissions of the bot role: tickets.create, tickets.handle (priority).
Data access: TICKETS_WRITE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body CreateTicketRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 3–200 chars |
body | string | Description (Markdown, <@userId> mentions) · max 20000 chars |
categoryId | string | null | |
priority | "LOW" | "NORMAL" | "HIGH" | "URGENT" | Only with tickets.handle – otherwise NORMAL |
fields | object | Custom field values (field id → value); required fields of the category are enforced |
attachmentIds | string[] | Ticket uploads of the bot in this room (POST /bot/rooms/{roomId}/tickets/attachments), not attached yet – at most 10 · max 10 items |
interactionId | string | On behalf of the person of this interaction (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed |
onBehalfOfUserId | string | On behalf of this member – needs the approved data access TICKETS_CREATE_ON_BEHALF (or an interaction of the same person) |
{
"title": "Server not reachable",
"body": "Monitoring reports `api-2` down since 09:12.",
"categoryId": null,
"fields": {}
}Response
201 The new ticket – TicketDetail
Errors
400VALIDATION_FAILED,TICKET_TITLE_INVALID,TICKET_CATEGORY_NOT_FOUND,TICKET_FIELDS_INVALID,TICKET_ATTACHMENT_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY,TICKET_REPORTER_INVALID,TICKET_REPORTER_NO_ACCESS,TICKET_INTERACTION_MISMATCH404ROOM_NOT_FOUND,SPACE_NOT_FOUND,BOT_INTERACTION_EXPIRED409E2EE_FEATURE_DISABLED,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/tickets/attachmentsStart a ticket attachment upload
Step 1 of 3 – the same pipeline as people: upload size and storage quota of the plan, blocked file types, malware scan, encryption at rest. Step 2: PUT the raw bytes to uploadUrl with headers (no bot token). Step 3: POST /bot/files/{fileId}/complete. Then pass the file id in attachmentIds of a new ticket or a reply. Ticket files are private (only who may see the ticket); not in end-to-end encrypted rooms.
Permissions of the bot role: tickets.create | tickets.handle.
Data access: TICKETS_WRITE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body UploadTicketAttachmentRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–255 chars |
mimeType · required | string | e.g. image/png · max 255 chars, pattern ^[\w.+-]+/[\w.+-]+$ |
size · required | integer | Exact size in bytes (the upload URL accepts at most this many) · min 0 |
{
"name": "screenshot.png",
"mimeType": "image/png",
"size": 48213
}Response
201 Upload URL – FileUpload
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID402PLAN_LIMIT_STORAGE403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED,TICKETS_E2EE_UNSUPPORTED413FILE_TOO_LARGE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/tickets/categoriesTicket categories
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create.
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Categories by position – TicketCategory[]
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/tickets/fieldsCustom ticket fields
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create.
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Fields by position – TicketField[]
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/tickets/canned-responsesCanned responses
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage.
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Canned responses by position – TicketCannedResponse[]
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/tickets/statsTicket statistics
Permissions of the bot role: tickets.handle | tickets.manage.
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Statistics of the room – TicketStats
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/tickets/ratingsTicket ratings
Stars and comments of the creators, newest first.
Permissions of the bot role: tickets.handle | tickets.manage.
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
cursor | query | nextCursor of the previous page (a ticket id) |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – TicketRatingPage
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TICKETS_MEMBERS_ONLY404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/tickets/{ticketId}One ticket with its replies
Public replies; internal notes only with tickets.handle. Tickets the bot may not see answer 404 (no existence leak).
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create (own tickets).
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
Response
200 The ticket – TicketDetail
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND,TICKET_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/tickets/{ticketId}Change status, priority, category, assignee, fields or the description
Same workflow as for people: tickets.handle for everything; the bot’s own ticket may be closed (and reopened within the reopen window) without it. Tickets the bot did not open need TICKETS_READ too (the answer contains the ticket). body (description, since 1.6.0): tickets.handle, or the bot’s own ticket – opened as itself or on behalf of the creator (createdByBotId = this bot); same limits as on create, closed tickets only with tickets.handle (409 TICKET_CLOSED). The permission is checked before anything is written. The history gets a description change without the text, people with access get the realtime update, newly mentioned people a mention; ticket.updated carries { field: "description" }.
Permissions of the bot role: tickets.handle.
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
Request body UpdateTicketRequest
| Field | Type | Description |
|---|---|---|
title | string | 3–200 chars |
body | string | Description (Markdown, <@userId> mentions; may be empty) – tickets.handle, or the bot’s own ticket (opened as itself or on behalf of the creator). Since 1.6.0. · max 20000 chars |
status | "NEW" | "IN_PROGRESS" | "WAITING_FOR_REPLY" | "RESOLVED" | "CLOSED" | Workflow: tickets.handle any allowed transition; the bot’s own ticket: close / reopen within the window |
priority | "LOW" | "NORMAL" | "HIGH" | "URGENT" | |
categoryId | string | null | |
assigneeId | string | null | A person with tickets.handle in the room (never a bot) |
assigneeRoleId | string | null | Role of the space |
fields | object | Replaces the custom field values |
{
"status": "IN_PROGRESS",
"priority": "HIGH",
"assigneeId": "cm1user0000000000000000"
}Response
200 The ticket – TicketDetail
Errors
400VALIDATION_FAILED,TICKET_TITLE_INVALID,TICKET_CATEGORY_NOT_FOUND,TICKET_FIELDS_INVALID,TICKET_ASSIGNEE_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,TICKET_NOT_FOUND409TICKET_TRANSITION_INVALID,TICKET_REOPEN_EXPIRED,TICKET_CLOSED,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/tickets/{ticketId}/attachmentsAttachments of a ticket
Every attachment the bot may see: files of the ticket description (messageId null) and of the replies; files of internal notes only with tickets.handle. Deleted files are left out; files still in the malware scan or blocked as infected are listed with their status but cannot be downloaded. Tickets the bot may not see answer 404 (no existence leak). Since 1.4.0.
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create (own tickets).
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
Response
200 Attachments, oldest first – TicketAttachmentList
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND,TICKET_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/tickets/{ticketId}/attachments/{fileId}/downloadDownload URL of a ticket attachment
Short-lived signed URL (like GET /bot/files/{fileId}/download) – fetch it without the bot token; the bytes are streamed (HTTP Range supported), the size is bounded by the upload limit of the plan. The file must be an attachment of this ticket that the bot may see (internal notes: tickets.handle) – otherwise 404 TICKET_ATTACHMENT_NOT_FOUND. Files in the malware scan (409 FILE_NOT_READY) or infected (403 FILE_INFECTED) cannot be downloaded; end-to-end encrypted files are refused (409 FILE_E2EE – the server cannot decrypt them). The download policy of the organisation applies. Counts against the normal request limits; audited (BOT_TICKET_ACTION attachment_downloaded, no file name). Since 1.4.0; since 1.6.0 url is always absolute (built from the public base URL of the instance).
Permissions of the bot role: tickets.view | tickets.handle | tickets.manage | tickets.create (own tickets).
Data access: TICKETS_READ.
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
fileId · required | path | File id |
Response
200 Signed URL – FileDownload
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,FILE_INFECTED404SPACE_NOT_FOUND,TICKET_NOT_FOUND,TICKET_ATTACHMENT_NOT_FOUND409FILE_NOT_READY,FILE_E2EE429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/tickets/{ticketId}/reporterHand a ticket of the bot over to a member
For tickets the bot opened as itself (creatorId = the bot), e.g. before it knew who the person was: the member becomes the reporter and the ticket is then exactly a ticket opened on behalf (createdByBotId = this bot, shown as "created by <bot> on behalf of <member>"). Same gates as POST /bot/rooms/{roomId}/tickets on behalf: interaction proof (interactionId + header X-Coviqo-Interaction-Token, no extra data access) or onBehalfOfUserId with the privileged data access TICKETS_CREATE_ON_BEHALF – neither → 400 BOT_ON_BEHALF_REQUIRED. The member must be able to open the ticket as a person (sees the room, tickets.create, not timed out) – never guests, bots or people who left. Tickets already handed over by this bot → 409 TICKET_REPORTER_ALREADY_SET (details.sameReporter true when the same member was named – safe to treat a retry as done); tickets of other reporters → 409 TICKET_REPORTER_NOT_BOT. Write limits plus the on-behalf ticket limit (10 per minute and space, scope botticket – shared with tickets opened on behalf). History reporter change, realtime update to the member, ticket.updated; audited (BOT_TICKET_ACTION reporter_reassigned). The answer contains the ticket once – afterwards the bot sees it only with tickets.view / handle / manage. Since 1.6.0.
Permissions of the bot role: tickets.create (own tickets).
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body ReassignTicketReporterRequest
| Field | Type | Description |
|---|---|---|
interactionId | string | The person of this interaction becomes the reporter (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed |
onBehalfOfUserId | string | This member becomes the reporter – needs the approved data access TICKETS_CREATE_ON_BEHALF (or an interaction of the same person) |
{
"onBehalfOfUserId": "cm1user0000000000000000"
}Response
200 The ticket (now with the member as creator) – TicketDetail
Errors
400VALIDATION_FAILED,BOT_ON_BEHALF_REQUIRED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,TICKET_REPORTER_INVALID,TICKET_REPORTER_NO_ACCESS,TICKET_INTERACTION_MISMATCH,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,TICKET_NOT_FOUND,BOT_INTERACTION_EXPIRED409TICKET_REPORTER_ALREADY_SET,TICKET_REPORTER_NOT_BOT,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/tickets/{ticketId}/messagesReply or add an internal note
PUBLIC: reply the creator sees (tickets.handle, or the bot’s own ticket) – the first team reply sets the first response time. INTERNAL: note only the team sees (tickets.handle). Closed tickets take internal notes only. Attachments: attachmentIds of ticket uploads (POST /bot/rooms/{roomId}/tickets/attachments). Messages (here, in GET /bot/tickets/{ticketId} and in ticket.message.created) carry author { id, displayName, isBot } – the display name inside the space (since 1.6.0). On behalf of a member (since 1.7.0) the reply becomes the member’s own reply, marked “via <bot>” (viaBotId): (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space by exactly this person, no extra data access; (2) onBehalfOfUserId – only with the privileged data access TICKETS_REPLY_ON_BEHALF (on top of TICKETS_WRITE), e.g. to relay e-mail or chat replies. The member must be the reporter or a participant of the ticket, an active member (no guests, bots, people who left) who still sees the room and is not timed out (403 TICKET_REPLIER_INVALID { reason }); public replies only. Everything else runs as the member (status rules – a reply can reopen a resolved ticket within the reopen window –, notifications, mentions). The bot must see the ticket (TICKETS_READ unless it is its own ticket). Replies on behalf count against an extra limit of 30 per minute and space (scope botticketreply) and are audited with the member (BOT_TICKET_ACTION replied_on_behalf).
Permissions of the bot role: tickets.handle (internal notes, others’ tickets).
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body CreateTicketMessageRequest
| Field | Type | Description |
|---|---|---|
body · required | string | Markdown, <@userId> mentions · 1–20000 chars |
kind | "PUBLIC" | "INTERNAL" | Default PUBLIC; INTERNAL needs tickets.handle |
attachmentIds | string[] | Ticket uploads of the bot in the ticket’s room, not attached yet – at most 10 · max 10 items |
interactionId | string | Reply on behalf of the person of this interaction (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed. Since 1.7.0 |
onBehalfOfUserId | string | Reply on behalf of this member – needs the approved data access TICKETS_REPLY_ON_BEHALF (or an interaction of the same person). Since 1.7.0 |
{
"body": "Thanks – we are on it.",
"kind": "PUBLIC"
}Response
201 The new message – TicketMessage
Errors
400VALIDATION_FAILED,TICKET_ATTACHMENT_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT,TICKET_REPLIER_INVALID,TICKET_INTERACTION_MISMATCH404SPACE_NOT_FOUND,TICKET_NOT_FOUND,BOT_INTERACTION_EXPIRED409TICKET_CLOSED,TICKET_REOPEN_EXPIRED,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/tickets/{ticketId}/messages/{messageId}Edit a reply or internal note
Since 1.7.0. Messages the bot wrote itself or posted on behalf of a member (viaBotId = this bot) – others: 403 TICKET_MESSAGE_NOT_AUTHOR. Same limits as a new message; public replies of closed tickets only with tickets.handle (409 TICKET_CLOSED). Sets editedAt (no history entry), sends ticket.message.updated, notifies people mentioned for the first time. Same text: nothing happens. Audited (BOT_TICKET_ACTION message_edited, no content).
Permissions of the bot role: the bot sees the ticket.
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
messageId · required | path | Message id |
Request body UpdateTicketMessageRequest
| Field | Type | Description |
|---|---|---|
body · required | string | The new text (Markdown, <@userId> mentions) – may be empty only while the message has attachments · max 20000 chars |
{
"body": "Thanks – fixed in 2.4.1."
}Response
200 The message after the edit – TicketMessage
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,TICKET_MESSAGE_NOT_AUTHOR,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,TICKET_NOT_FOUND,TICKET_MESSAGE_NOT_FOUND409TICKET_CLOSED,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/tickets/{ticketId}/messages/{messageId}Delete a reply or internal note
Since 1.7.0. Messages the bot wrote itself or posted on behalf of a member; with tickets.manage also messages of others (moderation). Public replies of closed tickets only with tickets.handle. Attachments of the message are deleted with it. Sends ticket.message.deleted (ids + kind). Audited (BOT_TICKET_ACTION message_deleted, no content).
Permissions of the bot role: tickets.manage (messages of others).
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
messageId · required | path | Message id |
Response
204 Deleted
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,TICKET_NOT_FOUND,TICKET_MESSAGE_NOT_FOUND409TICKET_CLOSED,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/tickets/{ticketId}/participants/{userId}Add a participant
A member who can see the room (no guests, no bots). Idempotent.
Permissions of the bot role: tickets.handle (others’ tickets).
Data access: TICKETS_WRITE (+ TICKETS_READ for tickets the bot did not open).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
ticketId · required | path | Ticket id |
userId · required | path | User id of the member |
Response
200 The ticket – TicketDetail
Errors
400TICKET_PARTICIPANT_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,TICKET_NOT_FOUND409TICKET_PARTICIPANTS_LIMIT,TICKETS_E2EE_UNSUPPORTED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Teams
Teams, applications and announcements (data access MEMBERS; changes need teams.manage).
GET/bot/spaces/{spaceId}/teamsList teams
Space teams plus the organisation teams usable in the space (read-only for bots), ordered by position.
Data access: MEMBERS.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 The teams – TeamList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/teams/{teamId}Change team settings
Since 1.4.0. Name, join mode (OPEN / APPLICATION / INVITE_ONLY / PASSWORD since 1.6.0) and the application question of a space team – with the rules for people (teams.manage of the bot role; bots are never team leads). The team tree, IdP group, position and room grants stay with people. Switching to OPEN is refused with 409 TEAM_GRANTS_ABOVE_BOT { permissions } while the team or a parent team grants room / category permissions the bot does not hold itself. Audited as TEAM_UPDATED with the bot. Since 1.6.0: joinPolicy PASSWORD with password (write-only, stored hashed, never returned – the team only shows hasJoinPassword). Switching to PASSWORD needs a password (400 TEAM_JOIN_PASSWORD_REQUIRED); password alone changes it. Setting a password gets the same 409 TEAM_GRANTS_ABOVE_BOT check as OPEN (the bot knows the password, so it controls who joins) – unless the team is OPEN already. The audit names the change joinPassword, never the password.
Permissions of the bot role: teams.manage.
Data access: TEAMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
Request body UpdateTeamRequest
| Field | Type | Description |
|---|---|---|
name | string | 1–60 chars |
joinPolicy | "OPEN" | "INVITE_ONLY" | "APPLICATION" | "PASSWORD" | OPEN is refused (409 TEAM_GRANTS_ABOVE_BOT) while the team or a parent team grants room permissions the bot does not hold |
applicationPrompt | string | null | Question of the application dialog (joinPolicy APPLICATION) · max 300 chars |
password | string | joinPolicy PASSWORD: the join password (6–128 characters, write-only – never returned). Required when switching to PASSWORD; send it alone to change the password of a PASSWORD team; refused (400 TEAM_JOIN_PASSWORD_NOT_APPLICABLE) with any other join policy. Switching away clears it. Since 1.6.0 · 6–128 chars, password |
{
"joinPolicy": "APPLICATION",
"applicationPrompt": "Which games do you play?"
}Response
200 The changed team – Team
Errors
400VALIDATION_FAILED,TEAM_SPACE_ONLY,TEAM_APPLICATION_SPACE_ONLY,TEAM_JOIN_PASSWORD_REQUIRED,TEAM_JOIN_PASSWORD_NOT_APPLICABLE401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,FORBIDDEN,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_NOT_FOUND409TEAM_GRANTS_ABOVE_BOT429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/teams/{teamId}/membersTeam members
Direct members (members of sub-teams are listed at their own team).
Data access: MEMBERS.
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
Response
200 The members – TeamMemberList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND,TEAM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/teams/{teamId}/members/{userId}Add a team member / set the lead flag
Idempotent. Only active non-guest members of the space. Sends team.member.added for new members (via manual).
Permissions of the bot role: teams.manage.
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
userId · required | path | User id of the member |
Request body PutTeamMemberRequest (optional)
| Field | Type | Description |
|---|---|---|
isLead | boolean | Make / unmake a team lead (omitted: unchanged for members, false for new ones) |
{
"isLead": false
}Response
200 The member – TeamMember
Errors
400VALIDATION_FAILED,TEAM_MEMBER_INVALID,TEAM_SPACE_ONLY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_NOT_FOUND,TEAM_MEMBER_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/teams/{teamId}/members/{userId}Remove a team member
Memberships synced from the identity provider cannot be removed (409 TEAM_IDP_MANAGED). Sends team.member.removed (via removed).
Permissions of the bot role: teams.manage.
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
userId · required | path | User id of the member |
Response
204 Removed
Errors
400TEAM_SPACE_ONLY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_NOT_FOUND,TEAM_MEMBER_NOT_FOUND409TEAM_IDP_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/teams/{teamId}/applicationsApplications to a team
Teams with joinPolicy APPLICATION. The bot must be able to decide (teams.manage). New ones arrive as team.application.created.
Permissions of the bot role: teams.manage.
Data access: MEMBERS.
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
status | query | PENDING | ACCEPTED | DECLINED | WITHDRAWN (default: all) |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – TeamApplicationPage
Errors
400TEAM_SPACE_ONLY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/teams/{teamId}/applicationsApply on behalf of a member
Since 1.4.0. Bots never apply for themselves – the member is named by (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space, no extra data access – or (2) onBehalfOfUserId with the privileged data access TEAM_APPLY_ON_BEHALF (any active member, e.g. a recruitment form on a clan website); both given must name the same person. All rules of a personal application apply (joinPolicy APPLICATION, one open application per team, open limit, cooldown – 429 TEAM_APPLICATION_COOLDOWN { retryAfter }); the deciders are notified as usual, team.application.created is sent and the application shows "via <bot>" (createdByBotId). The member can withdraw it – and so can the bot (POST /bot/team-applications/{applicationId}/withdraw, since 1.5.0, no cooldown). Extra limit of 10 per minute and space (scope botonbehalf); audited as BOT_MEMBER_ACTION team_applied (no message content).
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body CreateTeamApplicationRequest
| Field | Type | Description |
|---|---|---|
message · required | string | 1–1000 characters · 1–1000 chars |
onBehalfOfUserId | string | Apply for this member – needs the approved data access TEAM_APPLY_ON_BEHALF (or an interaction of the same person) |
interactionId | string | Interaction proof: apply for the person of this interaction – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
{
"message": "Hi, I would like to join the raid team.",
"onBehalfOfUserId": "clx…"
}Response
201 The new application – TeamApplication
Errors
400VALIDATION_FAILED,BOT_ON_BEHALF_REQUIRED,TEAM_SPACE_ONLY,TEAM_MEMBER_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_ON_BEHALF_INVALID,BOT_INTERACTION_MISMATCH,TEAM_NOT_APPLICATION404SPACE_NOT_FOUND,TEAM_NOT_FOUND,BOT_INTERACTION_EXPIRED409TEAM_APPLICATION_PENDING,TEAM_ALREADY_MEMBER,TEAM_APPLICATION_LIMIT,BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/team-applications/{applicationId}/withdrawWithdraw an application on behalf of the applicant
Since 1.5.0. Withdraws a PENDING application that THIS bot created on behalf of the person (createdByBotId) – the same as the person withdrawing it, but without the re-application cooldown (declining it would block the person for 24 h). Applications the person wrote themselves or another bot created: 403 TEAM_APPLICATION_NOT_BY_BOT. Gate like applying: (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space by the applicant, no extra data access – or (2) TEAM_APPLY_ON_BEHALF. The applicant must still be an active member. Sends team.application.decided with status WITHDRAWN and withdrawnByBotId. Shares the extra limit of 10 per minute and space with applying (scope botonbehalf); audited as BOT_MEMBER_ACTION team_application_withdrawn.
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
applicationId · required | path | applicationId |
X-Coviqo-Interaction-Token | header | Interaction proof: token of the interaction named in interactionId (valid 15 minutes). Wrong / expired → 404 BOT_INTERACTION_EXPIRED. |
Request body WithdrawTeamApplicationRequest (optional)
| Field | Type | Description |
|---|---|---|
interactionId | string | Interaction proof: an interaction of this bot by the applicant – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
{}Response
200 The withdrawn application – TeamApplication
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_ON_BEHALF_INVALID,BOT_INTERACTION_MISMATCH,TEAM_APPLICATION_NOT_BY_BOT404SPACE_NOT_FOUND,TEAM_APPLICATION_NOT_FOUND,BOT_INTERACTION_EXPIRED409TEAM_APPLICATION_NOT_PENDING429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/team-applications/{applicationId}/decisionAccept or decline an application
ACCEPTED adds the person as a normal member (team.member.added via application). The applicant is notified; the note is shown to them. Sends team.application.decided.
Permissions of the bot role: teams.manage.
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
applicationId · required | path | applicationId |
Request body TeamApplicationDecisionRequest
| Field | Type | Description |
|---|---|---|
decision · required | "ACCEPTED" | "DECLINED" | |
note | string | Shown to the applicant (≤ 500 characters) · max 500 chars |
{
"decision": "ACCEPTED",
"note": "Welcome aboard!"
}Response
200 The decided application – TeamApplication
Errors
400VALIDATION_FAILED,TEAM_MEMBER_INVALID401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_APPLICATION_NOT_FOUND409TEAM_APPLICATION_NOT_PENDING429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/teams/{teamId}/announcementsTeam announcements
Data access: MEMBERS.
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 20, at most 50) · 1–50 |
Response
200 One page – TeamAnnouncementPage
Errors
400TEAM_SPACE_ONLY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,TEAM_NOT_MEMBER404SPACE_NOT_FOUND,TEAM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/teams/{teamId}/announcementsPost a team announcement
Notifies the members of the team and its sub-teams; optionally also posts it into a room. At most 10 per team and hour (429 TEAM_ANNOUNCEMENT_RATE_LIMITED).
Permissions of the bot role: teams.manage, chat.send (roomId).
Data access: MEMBERS.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
Request body CreateTeamAnnouncementRequest
| Field | Type | Description |
|---|---|---|
title | string | null | ≤ 120 characters · max 120 chars |
content · required | string | 1–4000 characters · 1–4000 chars |
roomId | string | null | Also post it as a message into this room of the team’s space (the bot needs chat.send there) |
{
"title": "Raid tonight",
"content": "Meet at 20:00 in Squad 1.",
"roomId": null
}Response
201 The announcement – TeamAnnouncement
Errors
400VALIDATION_FAILED,TEAM_SPACE_ONLY401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION404SPACE_NOT_FOUND,TEAM_NOT_FOUND,ROOM_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Forum
Forum rooms: posts (title + first message, tags), replies, solved / pinned / locked. Reading posts needs MESSAGE_CONTENT.
GET/bot/rooms/{roomId}/forumForum tags and rules
Tags (set up by moderators – standard data access), whether a tag is required, and what the bot may do here.
Permissions of the bot role: chat.view.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 The forum – Forum
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/forum/postsList posts
Pinned posts first, then by sort (default: the forum’s sort). Titles and excerpts are content.
Permissions of the bot role: chat.view.
Data access: MESSAGE_CONTENT.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
sort | query | Order after the pinned posts (default: the forum’s default sort) |
status | query | all (default), open or solved |
tag | query | Only posts with this tag id |
q | query | Search words in titles, first messages and replies (at least 2 characters) |
cursor | query | nextCursor of the previous page |
limit | query | Page size (default 25, at most 50) · 1–50 |
Response
200 One page – ForumPostPage
Errors
400ROOM_MODULE_DISABLED,VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/forum/postsCreate a post
Title plus an optional first message (with buttons / select menus like a bot message) and tags. The forum’s rules apply: required tag, moderator-only tags (forum.manage). Sends forum.post_created; the bot follows its post.
Permissions of the bot role: chat.view, chat.send.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body CreateForumPostRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 3–200 characters (whitespace is collapsed) · max 250 chars |
content | string | First message (optional – the title is the post). Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
tagIds | string[] | At most 5 tags of this forum · max 5 items |
components | Components | |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
{
"title": "Server maintenance on Friday",
"content": "The game server is down **20:00–22:00**.",
"tagIds": [
"cm1tag00000000000000000"
]
}Response
201 The new post – ForumPost
Errors
400VALIDATION_FAILED,FORUM_TITLE_INVALID,FORUM_TAG_REQUIRED,FORUM_TAGS_TOO_MANY,FORUM_TAG_INVALID,BOT_COMPONENTS_INVALID,ROOM_MODULE_DISABLED,ROOM_ARCHIVED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,FORUM_TAG_RESTRICTED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/forum/posts/{postId}One post
The post, its first message and the accepted answer.
Permissions of the bot role: chat.view.
Data access: MESSAGE_CONTENT.
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Response
200 The post – ForumPostDetail
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/forum/posts/{postId}Change title / tags
Own posts (chat.send) or any post with forum.manage. The first message is edited with PATCH /bot/messages/{messageId} (message id = post id).
Permissions of the bot role: chat.view, chat.send (own post) or forum.manage.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Request body UpdateForumPostRequest
| Field | Type | Description |
|---|---|---|
title | string | 3–200 characters · max 250 chars |
tagIds | string[] | Replaces the tags (at most 5) · max 5 items |
{
"title": "Server maintenance moved to Saturday"
}Response
200 The post (title / excerpt null for posts of others without MESSAGE_CONTENT) – ForumPost
Errors
400VALIDATION_FAILED,FORUM_TITLE_INVALID,FORUM_TAG_REQUIRED,FORUM_TAGS_TOO_MANY,FORUM_TAG_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,FORUM_NOT_AUTHOR,FORUM_TAG_RESTRICTED404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/forum/posts/{postId}/repliesReplies of a post
Newest first. New replies arrive as message.created.
Permissions of the bot role: chat.view.
Data access: MESSAGE_CONTENT.
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
before | query | Reply id – returns older replies (nextCursor of the previous page) |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – EventMessagePage
Errors
400ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/forum/posts/{postId}/repliesReply to a post
Posts into the post’s discussion as the bot (optional buttons / select menus). Locked posts only with forum.manage (403 FORUM_POST_LOCKED).
Permissions of the bot role: chat.view, chat.send.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Request body ForumReplyRequest
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Quote a message of this post (its first message or a reply) · max 64 chars |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
{
"content": "Done – the server is back online."
}Response
201 The new reply – Message
Errors
400VALIDATION_FAILED,MESSAGE_EMPTY,BOT_COMPONENTS_INVALID,ROOM_MODULE_DISABLED,ROOM_ARCHIVED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,FORUM_POST_LOCKED404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND,MESSAGE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/forum/posts/{postId}/solvedMark solved / pick the answer
Own posts or any post with forum.manage. solved: false clears the answer.
Permissions of the bot role: chat.view, forum.manage (posts of others).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Request body ForumSolvedRequest
| Field | Type | Description |
|---|---|---|
solved · required | boolean | |
answerId | string | null | A reply of this post (400 FORUM_ANSWER_INVALID otherwise) · max 64 chars |
{
"solved": true,
"answerId": "cm1msg00000000000000000"
}Response
200 The post (title / excerpt null for posts of others without MESSAGE_CONTENT) – ForumPost
Errors
400VALIDATION_FAILED,FORUM_ANSWER_INVALID,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,FORUM_NOT_AUTHOR404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/forum/posts/{postId}/pinnedPin / unpin a post
Permissions of the bot role: chat.view, forum.manage.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Request body ForumFlagRequest
| Field | Type | Description |
|---|---|---|
value · required | boolean |
{
"value": true
}Response
200 The post (title / excerpt null for posts of others without MESSAGE_CONTENT) – ForumPost
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/forum/posts/{postId}/lockedLock / unlock a post
Locked posts take replies only from people / bots with forum.manage.
Permissions of the bot role: chat.view, forum.manage.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
postId · required | path | Forum post id (= id of its first message) |
Request body ForumFlagRequest
| Field | Type | Description |
|---|---|---|
value · required | boolean |
{
"value": true
}Response
200 The post (title / excerpt null for posts of others without MESSAGE_CONTENT) – ForumPost
Errors
400VALIDATION_FAILED,ROOM_MODULE_DISABLED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND,FORUM_POST_NOT_FOUND409E2EE_FEATURE_DISABLED,ROOM_ARCHIVED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Roles
Assign / remove roles of members (data access ROLES_ASSIGN, allow-list per installation).
PUT/bot/spaces/{spaceId}/members/{userId}/roles/{roleId}Assign a role
Only roles in the installation’s allow-list (bot detail → “Assign roles”), strictly below the bot’s own role; roles with moderation-like permissions only with “assign elevated roles”, roles with admin-level permissions only with “allow roles with admin rights” (set by the space owner or an administrator). Any active human member – the member’s own position does not matter (Discord semantics), the owner included (ownership itself is not a role and never changes); never bots, guests never get elevated roles. Idempotent. Sends member.roles_updated with added, removed, actorId.
Data access: ROLES_ASSIGN.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
roleId · required | path | Role id (GET /bot/spaces/{spaceId}/roles) |
Request body RoleAssignRequest (optional)
| Field | Type | Description |
|---|---|---|
reason | string | Audit reason (≤ 512 characters) · max 512 chars |
{
"reason": "Discord role sync"
}Response
200 The member after the change – BotMember
Errors
400VALIDATION_FAILED,BOT_ROLE_NOT_ALLOWED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_ROLE_NOT_ASSIGNABLE,ROLE_HIERARCHY404SPACE_NOT_FOUND,ROLE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/spaces/{spaceId}/members/{userId}/roles/{roleId}Remove a role
Same rules as assigning (allow-list, below the bot role …). Idempotent.
Data access: ROLES_ASSIGN.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
roleId · required | path | Role id (GET /bot/spaces/{spaceId}/roles) |
Request body RoleAssignRequest (optional)
| Field | Type | Description |
|---|---|---|
reason | string | Audit reason (≤ 512 characters) · max 512 chars |
{
"reason": "Left the clan"
}Response
200 The member after the change – BotMember
Errors
400VALIDATION_FAILED,BOT_ROLE_NOT_ALLOWED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_ROLE_NOT_ASSIGNABLE,ROLE_HIERARCHY404SPACE_NOT_FOUND,ROLE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/spaces/{spaceId}/members/{userId}/rolesAdd and remove roles at once
Atomic: every role is checked first (same rules as PUT); one failing role fails the whole request and nothing changes. At most 25 ids in add and in remove; a role must not be in both.
Data access: ROLES_ASSIGN.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
userId · required | path | User id of the member |
Request body RolesPatchRequest
| Field | Type | Description |
|---|---|---|
add | string[] | Roles to add (≤ 25) · max 25 items |
remove | string[] | Roles to remove (≤ 25) · max 25 items |
reason | string | Audit reason (≤ 512 characters) · max 512 chars |
{
"add": [
"role_member"
],
"remove": [
"role_recruit"
],
"reason": "Promotion"
}Response
200 The member after the change – BotMember
Errors
400VALIDATION_FAILED,BOT_ROLE_NOT_ALLOWED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,BOT_ROLE_NOT_ASSIGNABLE,ROLE_HIERARCHY404SPACE_NOT_FOUND,ROLE_NOT_FOUND,MEMBER_NOT_FOUND409BOT_MEMBER_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Rooms
Create, edit, archive and delete rooms and categories, room permission overrides (data access ROOMS_MANAGE + room.create / room.manage), move / disconnect people in voice (voice.move_members).
POST/bot/spaces/{spaceId}/roomsCreate a room
Like a person with room.create: in any category (default: the first one); the room starts synced with the category’s permissions. Types: STANDARD, VOICE, PROJECT, EVENT, COMMUNITY, STAGE, FORUM (no meeting, lobby or map rooms). Modules: not WEBPAGES / FEED. PRIVATE / INVITE_ONLY rooms get the bot as room member. At most 50 rooms + categories per bot, space and day (429 RATE_LIMITED, scope botrooms).
Permissions of the bot role: room.create.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Request body CreateRoomRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–64 characters · 1–64 chars |
type · required | "STANDARD" | "VOICE" | "PROJECT" | "EVENT" | "COMMUNITY" | "STAGE" | "FORUM" | Not MEETING, LOBBY or MAP |
visibility | "PUBLIC" | "PRIVATE" | "RESTRICTED" | "INVITE_ONLY" | Default PUBLIC; PRIVATE / INVITE_ONLY add the bot as room member |
description | string | max 500 chars |
categoryId | string | Create inside this category – needs room.manage in the space; the room starts synced with the category’s permissions |
modules | "VOICE" | "CHAT" | "NOTES" | "FILES" | "TASKS" | "EVENTS" | "WHITEBOARD" | "FORUM" | "TICKETS" | "DECISIONS" | "MEETINGS"[] | Default: the modules of the room type. Not WEBPAGES / FEED (configured by people only) |
tags | string[] | max 10 items |
icon | string | max 64 chars |
color | "blue" | "cyan" | "green" | "yellow" | "orange" | "red" | "pink" | "purple" | |
userLimit | integer | Voice user limit · 1–500 |
{
"name": "Squad 3",
"type": "VOICE",
"userLimit": 5
}Response
201 The room – BotRoomDetail
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,CATEGORY_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/categoriesRoom categories
With room.create: all categories of the space; otherwise only categories with rooms the bot can see.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 Categories in sidebar order – RoomCategory[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/spaces/{spaceId}/categoriesCreate a category
Counts to the daily cap of 50 new rooms + categories per bot and space (429 RATE_LIMITED, scope botrooms).
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Request body CategoryRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–48 characters · 1–48 chars |
{
"name": "Clan wars"
}Response
201 The category – RoomCategory
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/categories/{categoryId}Rename a category
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
categoryId · required | path | Room category id (GET /bot/spaces/{spaceId}/categories) |
Request body CategoryRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–48 characters · 1–48 chars |
{
"name": "Season 4"
}Response
200 The category – RoomCategory
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,CATEGORY_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/categories/{categoryId}Delete a category
Its rooms stay (without a category, with their current permissions).
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
categoryId · required | path | Room category id (GET /bot/spaces/{spaceId}/categories) |
Response
204 Deleted
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED,MISSING_PERMISSION,MEMBER_TIMED_OUT404SPACE_NOT_FOUND,CATEGORY_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PATCH/bot/rooms/{roomId}Edit a room
Omitted fields stay. Moving to another category (categoryId) also needs room.manage in the space; permissions: "category" applies the override rules (only permissions the bot holds, only entries below the bot role). Meeting rooms and Runden are managed elsewhere (409).
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Request body UpdateRoomRequest
| Field | Type | Description |
|---|---|---|
name | string | 1–64 chars |
description | string | null | max 500 chars |
visibility | "PUBLIC" | "PRIVATE" | "RESTRICTED" | "INVITE_ONLY" | |
tags | string[] | max 10 items |
icon | string | null | max 64 chars |
color | "blue" | "cyan" | "green" | "yellow" | "orange" | "red" | "pink" | "purple" | |
userLimit | integer | null | 1–500 |
type | "STANDARD" | "VOICE" | "PROJECT" | "EVENT" | "COMMUNITY" | "STAGE" | "FORUM" | Lobby and map rooms keep their type |
categoryId | string | null | Move to this category (null = no category) – needs room.manage in the space |
permissions | "category" | "keep" | With a category move: category takes over the category’s permissions, keep keeps the room’s own (default rules when omitted) |
{
"name": "Squad 3 (closed)",
"userLimit": 4
}Response
200 The room – BotRoomDetail
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,PERMISSION_ESCALATION,ROLE_HIERARCHY404ROOM_NOT_FOUND,SPACE_NOT_FOUND,CATEGORY_NOT_FOUND409E2EE_FEATURE_DISABLED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED,LOBBY_TYPE_FIXED,MAP_TYPE_FIXED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/rooms/{roomId}Delete a room
Permanent – messages, notes, files and tasks of the room go with it, a running call ends. The default room and the AFK room of the space cannot be deleted by bots (409 BOT_ROOM_PROTECTED). Prefer archiving.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
204 Deleted
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_ROOM_PROTECTED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/archiveArchive a room
Read-only, hidden from the sidebar; can be restored. Not the default room or the AFK room (409 BOT_ROOM_PROTECTED).
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 The room – BotRoomDetail
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_ROOM_PROTECTED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/unarchiveRestore an archived room
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 The room – BotRoomDetail
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/rooms/{roomId}/overridesPermission overrides of a room
Role, member and team entries (team entries can be changed since 1.6.0).
Permissions of the bot role: room.manage.
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
Response
200 Overrides – RoomOverride[]
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT404ROOM_NOT_FOUND,SPACE_NOT_FOUND409E2EE_FEATURE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/rooms/{roomId}/overrides/roles/{roleId}Set the room permissions of a role
Replaces the role’s entry (allow / deny, room-scoped permissions only). The role must be strictly below the bot role; never the owner role, a bot role or a role with admin-level permissions (the default role is fine). The bot only adds or removes permission bits it holds itself in this room (403 PERMISSION_ESCALATION) and never permissions bots cannot hold (400 BOT_PERMISSION_FORBIDDEN). Guests never get elevated permissions (409 GUEST_ROLE_ELEVATED). Changing a synced room ends the sync.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
roleId · required | path | Role id (GET /bot/spaces/{spaceId}/roles) |
Request body PutOverrideRequest
| Field | Type | Description |
|---|---|---|
allow · required | string[] | Room-scoped permissions (≤ 64) · max 64 items |
deny · required | string[] | Room-scoped permissions (≤ 64); deny wins over allow · max 64 items |
{
"allow": [
"room.view",
"room.join"
],
"deny": []
}Response
200 The entry – RoomOverride
Errors
400VALIDATION_FAILED,BOT_OVERRIDE_NOT_ALLOWED,INVALID_PERMISSION,BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,ROLE_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,ROLE_NOT_FOUND409E2EE_FEATURE_DISABLED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/rooms/{roomId}/overrides/roles/{roleId}Remove the room permissions of a role
Same target rules as setting; only entries whose permissions the bot holds itself in this room.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
roleId · required | path | Role id (GET /bot/spaces/{spaceId}/roles) |
Response
204 Removed
Errors
400BOT_OVERRIDE_NOT_ALLOWED,BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,ROLE_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,ROLE_NOT_FOUND,OVERRIDE_NOT_FOUND409E2EE_FEATURE_DISABLED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/rooms/{roomId}/overrides/members/{userId}Set the room permissions of a member
Replaces the member’s entry (allow / deny, room-scoped permissions only). The member’s highest role must be strictly below the bot role; never the owner, administrators or bots. The bot only adds or removes permission bits it holds itself in this room (403 PERMISSION_ESCALATION) and never permissions bots cannot hold (400 BOT_PERMISSION_FORBIDDEN). Guests never get elevated permissions (409 GUEST_ROLE_ELEVATED). Changing a synced room ends the sync.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
userId · required | path | User id of the member |
Request body PutOverrideRequest
| Field | Type | Description |
|---|---|---|
allow · required | string[] | Room-scoped permissions (≤ 64) · max 64 items |
deny · required | string[] | Room-scoped permissions (≤ 64); deny wins over allow · max 64 items |
{
"allow": [
"room.view",
"room.join"
],
"deny": []
}Response
200 The entry – RoomOverride
Errors
400VALIDATION_FAILED,INVALID_PERMISSION,BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,MEMBER_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MEMBER_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/rooms/{roomId}/overrides/members/{userId}Remove the room permissions of a member
Same target rules as setting; only entries whose permissions the bot holds itself in this room.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
userId · required | path | User id of the member |
Response
204 Removed
Errors
400BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,MEMBER_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,MEMBER_NOT_FOUND,OVERRIDE_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
PUT/bot/rooms/{roomId}/overrides/teams/{teamId}Set the room permissions of a team
Since 1.6.0. Replaces the team’s entry (allow / deny, room-scoped permissions only). The team must be a team of this space or an organisation team of its organisation (like in the room settings). Members of the team and of its sub-teams get the entry. Never a team with a bot account among its (sub-team) members (409 BOT_MEMBER_MANAGED). Deny entries – set, kept or lifted – need every member of the team and its sub-teams in this space strictly below the bot role: never the owner, administrators or members with an admin-level role (403 TEAM_HIERARCHY { reason, userId }). The bot only adds or removes permission bits it holds itself in this room (403 PERMISSION_ESCALATION) and never permissions bots cannot hold or admin-level permissions (400 BOT_PERMISSION_FORBIDDEN) – so a team stays openable for the bot (no 409 TEAM_GRANTS_ABOVE_BOT because of its own entries); an OPEN team gets at most what the bot could give the default role. Guests in the team never get elevated permissions (409 GUEST_ROLE_ELEVATED). Changing a synced room ends the sync.
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
Request body PutOverrideRequest
| Field | Type | Description |
|---|---|---|
allow · required | string[] | Room-scoped permissions (≤ 64) · max 64 items |
deny · required | string[] | Room-scoped permissions (≤ 64); deny wins over allow · max 64 items |
{
"allow": [
"room.view",
"room.join",
"voice.speak"
],
"deny": []
}Response
200 The entry – RoomOverride
Errors
400VALIDATION_FAILED,INVALID_PERMISSION,BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TEAM_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,TEAM_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED,GUEST_ROLE_ELEVATED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/rooms/{roomId}/overrides/teams/{teamId}Remove the room permissions of a team
Since 1.6.0. Same team rules as setting; only entries whose permissions the bot holds itself in this room (and, for entries with deny bits, only when every team member is below the bot role).
Permissions of the bot role: room.manage.
Data access: ROOMS_MANAGE.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
teamId · required | path | Team id (GET /bot/spaces/{spaceId}/teams) |
Response
204 Removed
Errors
400BOT_PERMISSION_FORBIDDEN401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,BOT_INTENT_REQUIRED,TEAM_HIERARCHY,PERMISSION_ESCALATION404ROOM_NOT_FOUND,SPACE_NOT_FOUND,TEAM_NOT_FOUND,OVERRIDE_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED,MEETING_ROOM_MANAGED,LOBBY_SESSION_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
POST/bot/rooms/{roomId}/voice/members/{userId}/moveMove a person to another voice room
The person must be in the voice call of roomId. Needs voice.move_members in both rooms and room.join in the target room. Only people strictly below the bot role – never the owner, administrators or bots. Moves that would need a confirmation (full room, person cannot join the target) are refused (409 VOICE_MOVE_CONFIRM_REQUIRED). Audit VOICE_MEMBER_MOVED with the bot as mover; event voice.member_moved.
Permissions of the bot role: voice.move_members, room.join (target room).
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
userId · required | path | User id of the member |
Request body VoiceMoveRequest
| Field | Type | Description |
|---|---|---|
targetRoomId · required | string | Voice room to move the person to (same space) |
{
"targetRoomId": "room_squad3"
}Response
204 Moved
Errors
400VALIDATION_FAILED,VOICE_SAME_ROOM,MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,MODERATION_HIERARCHY404ROOM_NOT_FOUND,SPACE_NOT_FOUND,VOICE_PARTICIPANT_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED,VOICE_MOVE_CONFIRM_REQUIRED,VOICE_TARGET_NO_ACCESS,ROOM_MODULE_DISABLED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
DELETE/bot/rooms/{roomId}/voice/members/{userId}Disconnect a person from voice
Ends the person’s voice connection in this room (they can rejoin). Same hierarchy as moving. Audit VOICE_MEMBER_DISCONNECTED with the bot as actor.
Permissions of the bot role: voice.move_members.
Counts against the write limits (30 / min per bot and space, 120 / min per space).
Parameters
| Name | In | Description |
|---|---|---|
roomId · required | path | Room id |
userId · required | path | User id of the member |
Response
204 Disconnected
Errors
400MODERATION_SELF401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION,MEMBER_TIMED_OUT,MODERATION_HIERARCHY404ROOM_NOT_FOUND,SPACE_NOT_FOUND,VOICE_PARTICIPANT_NOT_FOUND409E2EE_FEATURE_DISABLED,BOT_MEMBER_MANAGED429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Invites
Invite links and who joined through them (space.invite or space.manage_members).
GET/bot/spaces/{spaceId}/invitesInvites with join counts
Invite links of the space, newest first, with joinedMembers (active members that joined through each). Expired / used-up invites are included; revoked ones only with includeRevoked=true. No e-mail addresses.
Permissions of the bot role: space.invite | space.manage_members.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
includeRevoked | query | true lists revoked invites too |
cursor | query | nextCursor of the previous page (an invite id) |
limit | query | Page size (default 50, at most 100) · 1–100 |
Response
200 One page – InviteList
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/invites/{inviteId}/membersMembers who joined through an invite
Active members whose last (re-)join used this invite, ordered by member id (incl. joinSource).
Permissions of the bot role: space.invite | space.manage_members.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
inviteId · required | path | Invite id (GET /bot/spaces/{spaceId}/invites) |
cursor | query | nextCursor of the previous page (a member id) |
limit | query | Page size (default 100, at most 1000) · 1–1000 |
Response
200 One page – BotMemberPage
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,MISSING_PERMISSION404SPACE_NOT_FOUND,INVITE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Voice data
Voice states, voice activity and sessions (activity / sessions: data access VOICE_ACTIVITY).
GET/bot/spaces/{spaceId}/voice/statesWho is in voice right now
Everybody in the voice rooms the bot can see with mute / deafen / video / screen share / stage state (never speaking). Changes arrive as voice.state_updated.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
Response
200 Snapshot – VoiceStates
Errors
401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/voice/activityVoice activity report
Per person: time in voice, muted / deafened / AFK time within [from, to) (default: the last 30 days; at most 92 days). Open sessions count until now. Only rooms the bot can see. 10 requests per bot and minute together with /voice/sessions (429 RATE_LIMITED, scope botvoice).
Data access: VOICE_ACTIVITY.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
from | query | Start (ISO 8601, inclusive) · date-time |
to | query | End (ISO 8601, exclusive; default now) · date-time |
userIds | query | Comma-separated user ids (≤ 200) |
roomIds | query | Comma-separated room ids (≤ 200) |
includeAfkRoom | query | true counts the AFK room into totalSec (afkRoomSec is always reported) |
mutedGraceMinutes | query | 0–60: the first minutes of every muted interval do not count as muted · 0–60 |
cursor | query | nextCursor of the previous page (a user id) |
limit | query | Page size (default 100, at most 200) · 1–200 |
Response
200 One page – VoiceActivity
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
GET/bot/spaces/{spaceId}/voice/sessionsVoice sessions
Raw sessions overlapping [from, to) (default: the last 30 days; at most 92 days) with their muted / deafened intervals, oldest first. Only rooms the bot can see.
Data access: VOICE_ACTIVITY.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
from | query | Start (ISO 8601, inclusive) · date-time |
to | query | End (ISO 8601, exclusive; default now) · date-time |
userId | query | Only this person |
cursor | query | nextCursor of the previous page (a session id) |
limit | query | Page size (default 100, at most 500) · 1–500 |
Response
200 One page – VoiceSessionPage
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED,BOT_INTENT_REQUIRED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Statistics
Member growth (daily snapshots).
GET/bot/spaces/{spaceId}/stats/membersMember growth
Daily member snapshots between two UTC days (both inclusive; default: the last 30 days up to yesterday; at most 730 days). History starts with the first day after the feature was deployed – no backfill, no estimates.
Parameters
| Name | In | Description |
|---|---|---|
spaceId · required | path | Space id (the bot must be installed there) |
from | query | First day (YYYY-MM-DD) · date |
to | query | Last day (YYYY-MM-DD) · date |
Response
200 Snapshots – MemberStats
Errors
400VALIDATION_FAILED401BOT_AUTH_REQUIRED,BOT_TOKEN_INVALID403BOT_DISABLED,BOT_PAUSED404SPACE_NOT_FOUND429RATE_LIMITED,TOO_MANY_REQUESTS503BOTS_DISABLED
Incoming webhooks
Post into a room from CI, monitoring … – no bot account.
POST/hooks/{webhookId}Post through an incoming webhook
No bot account: the message is authored by the person who created the webhook and shows the webhook’s name / avatar. The secret goes into the secret query parameter or the X-Coviqo-Webhook-Secret header – never into the path.
Parameters
| Name | In | Description |
|---|---|---|
webhookId · required | path | Webhook id (from the webhook URL) |
Request body IncomingWebhookPayload
| Field | Type | Description |
|---|---|---|
content · required | string | 1–4000 chars |
username | string | Display name for this one message |
avatarUrl | string | https avatar for this one message (shown through the image proxy) · uri |
{
"content": "Deploy **v2.3.1** succeeded",
"username": "CI"
}Response
204 Posted
Errors
400MESSAGE_EMPTY,VALIDATION_FAILED403WEBHOOK_DISABLED404WEBHOOK_INVALID409WEBHOOK_OWNER_INVALID429RATE_LIMITED,TOO_MANY_REQUESTS503WEBHOOKS_DISABLED
Slash commands
Register with PUT /bot/commands – the list replaces the previous one, spaces see the change immediately. People type “/” in the composer: commands appear when they have bots.use in the room and the bot can see the room and write there (developer bots additionally need an interaction URL or an open gateway connection). Options are typed as name:value (values with spaces in quotes), e.g. /remind text:"Stand-up" minutes:5.
{
"commands": [
{ "name": "ping", "description": "Answers with Pong" },
{ "name": "remind", "description": "Set a reminder", "options": [
{ "name": "text", "description": "What about?", "type": "string", "required": true },
{ "name": "minutes", "description": "In minutes", "type": "integer", "choices": [
{ "name": "5", "value": 5 }, { "name": "60", "value": 60 } ] },
{ "name": "who", "description": "For whom", "type": "user" }
] }
]
}| Limit | Value |
|---|---|
| Commands per bot | 50 |
| Name (command and option) | 1–32 characters a-z 0-9 _ -, lower case, unique |
| Description | 1–100 characters |
| Options per command | 10; required options before optional ones |
| Option types | string, integer, number, boolean, user, room, role |
| choices | up to 25, only for string / integer / number; values must respect the limits below |
| Field | Types | Rules |
|---|---|---|
required | all | required options come before optional ones |
choices | string, integer, number | fixed list of { name, value } (up to 25); not together with autocomplete |
minValue | integer, number | smallest allowed value, inclusive; finite, an integer for integer (since 1.4.0) |
maxValue | integer, number | largest allowed value, inclusive, ≥ minValue (since 1.4.0) |
minLength | string | at least this many characters after trimming, 0–4000 (since 1.4.0) |
maxLength | string | at most this many characters, 1–4000, ≥ minLength (since 1.4.0) |
autocomplete | string, integer, number | true: Coviqo asks your bot for suggestions while the person types (see below, since 1.4.0) |
channelTypes | room | non-empty list of text / voice – only these rooms can be chosen (alias channel_types, since 1.6.0, see below) |
Limits are camelCase like the rest of the API; the spellings min_value, max_value, min_length and max_length are accepted on registration as well and stored as camelCase.
{ "name": "roll", "description": "Roll a die", "options": [
{ "name": "sides", "description": "Sides", "type": "integer", "required": true, "minValue": 2, "maxValue": 100 },
{ "name": "note", "description": "Note", "type": "string", "maxLength": 50 }
] }Coviqo checks every invocation before calling your bot: types (booleans also accept yes / no, 1 / 0), choices, limits (e.g. sides: at least 2, note: at most 50 characters), required options, unknown options (400 BOT_COMMAND_OPTIONS_INVALID with details.errors for the person – the composer already checks the same limits and shows the allowed range next to the option). user, room and role arrive as ids and belong to the space. Invalid definitions answer 400 BOT_COMMANDS_INVALID with details.errors.
Room options and channel types
Since 1.6.0 a room option can be limited with "channelTypes": ["voice"] (or text, or both): text = rooms with chat, voice = rooms with their own voice channel (not the backing room of a meeting); a room with chat and voice fits either. While the caret is in a room option the composer offers the rooms of the space the person can see – only matching ones – and inserts option:<#roomId>. Other rooms are refused in the composer and again before delivery (400 BOT_COMMAND_OPTIONS_INVALID, e.g. to: only voice rooms), so your bot always gets a room of the requested kind. SDK: roomOption(name, description, { channelTypes }).
{ "name": "move", "description": "Move the squad", "options": [
{ "name": "to", "description": "Voice room", "type": "room", "required": true, "channelTypes": ["voice"] }
] }Autocomplete
Options with "autocomplete": true get suggestions from your bot while the person types. When the caret is in the value of such an option (city:Ber, or the position that option fills), the composer waits for a short typing pause and Coviqo sends an interaction of type AUTOCOMPLETE: command.options holds the other options typed so far (only valid ones), command.focused the option being typed and its raw text (at most 200 characters, may be empty).
{
"id": "cm1ia…",
"type": "AUTOCOMPLETE",
"applicationId": "cm1app…",
"token": "q2V…",
"roomId": "cm1room…",
"user": { "id": "cm1user…", "username": "anna", "displayName": "Anna", "locale": "en" },
"command": { "name": "weather", "options": { "days": 3 }, "focused": { "name": "city", "value": "Ber" } },
"createdAt": "2026-10-05T09:00:00.000Z"
}- HTTP bots answer within 3 seconds with
{ "type": "AUTOCOMPLETE_RESULT", "data": { "choices": [{ "name": "Berlin", "value": "berlin" }] } }. - Gateway bots (no interaction URL) receive it as
interaction.createdwithinteraction.type = "AUTOCOMPLETE"and answer with POST /bot/interactions/{id}/autocomplete (headerX-Coviqo-Interaction-Token, body{ choices }) – once (409BOT_AUTOCOMPLETE_ANSWERED) and within 3 seconds (410BOT_INTERACTION_EXPIREDafterwards). - At most 25 choices are shown (more are cut off); names 1–100 characters, values of the option type within its limits – invalid entries are dropped. Too slow, an error or another response type: the person simply sees no suggestions. Picking one inserts
option:value. - Since 1.6.0 a choice can carry a short secondary line
description, shown under the name (whitespace collapsed, longer than 100 characters is cut off, non-strings are ignored):{ "name": "Berlin", "value": "berlin", "description": "Capital, 3.7 M" }. SDK:choice(name, value, { description }). - Autocomplete has no follow-ups, forms or
interaction.createdwebhook events, and its own rate limit of 120 requests per person and minute.
// HTTP: createInteractionHandler({ publicKey, onCommand, onAutocomplete })
const onAutocomplete = (i) => {
const q = i.command.focused.value.toLowerCase();
return CITIES.filter((c) => c.toLowerCase().startsWith(q)).slice(0, 25).map((c) => ({ name: c, value: c }));
};
// gateway bot
gateway.on('interaction.created', async (ev) => {
const i = ev.data.interaction;
if (i.type === 'AUTOCOMPLETE') await bot.answerAutocomplete(i, onAutocomplete(i));
});Interactions
When a person uses a command, clicks a button, picks from a select menu or submits a form, Coviqo checks their permissions (chat.view + bots.use), the rate limit and that the bot is installed, not paused and can see the room, and then sends a signed POST to your interaction URL. Your HTTP answer is applied right away. Bots without interaction URL but with an open gateway connection receive the interaction as event interaction.created instead.
Payload
Content-Type: application/json, User-Agent: Coviqo-Bots/1.0 (+https://coviqo.com/developers), schema InteractionPayload:
{
"id": "cm1ia…",
"type": "COMMAND",
"applicationId": "cm1app…",
"token": "q2V…",
"spaceId": "cm1space…",
"roomId": "cm1room…",
"user": { "id": "cm1user…", "username": "anna", "displayName": "Anna", "locale": "en" },
"memberRoleIds": ["cm1role…"],
"command": { "name": "remind", "options": { "text": "Stand-up", "minutes": 5 } },
"locale": "en",
"createdAt": "2026-10-02T09:00:00.000Z"
}| type | Extra fields |
|---|---|
PING | only id, applicationId, token, createdAt – URL check, answer { "type": "PONG" } |
COMMAND | command { name, options } |
COMPONENT | customId, values (select menus), message { id, content } (null for “only visible to you” messages) |
MODAL_SUBMIT | customId of the form, fields { <fieldCustomId>: value }; with file upload fields (since 1.6.0) files { <fieldCustomId>: [{ id, name, size, mimeType, url, urlExpiresAt }] } – see File uploads in forms |
AUTOCOMPLETE | command { name, options, focused { name, value } } – see Autocomplete (since 1.4.0) |
id + token identify the interaction for follow-ups; the token is valid for 15 minutes.
Responses
Answer with HTTP 2xx and JSON { "type": …, "data": … } (InteractionResponse) within 3 seconds, at most 64 KB, no redirects.
| type | Allowed for | Effect |
|---|---|---|
PONG | PING | URL check |
MESSAGE | all | post a message; data: { content?, components?, ephemeral? } |
DEFERRED_MESSAGE | all | shows “<Bot> is thinking …”; answer later with a follow-up. data.ephemeral sets the visibility of that later answer |
UPDATE_MESSAGE | COMPONENT, MODAL_SUBMIT opened from a message | replace the message carrying the component (data as for MESSAGE) |
DEFERRED_UPDATE | all (typically COMPONENT) | acknowledge only; edit later with PATCH …/original |
MODAL | COMMAND, COMPONENT | open a form; data = Modal (not in reply to a form) |
AUTOCOMPLETE_RESULT | AUTOCOMPLETE | suggestions; data: { choices: [{ name, value }] } (at most 25) |
What the person sees when your answer fails: 504 BOT_INTERACTION_TIMEOUT (slower than 3 s), 502 BOT_INTERACTION_FAILED (status not 2xx, unreachable), 502 BOT_INTERACTION_INVALID_RESPONSE (no valid JSON, unknown type, empty message, UPDATE_MESSAGE outside a component, form in reply to a form, more than 64 KB), 502 BOT_COMPONENTS_INVALID / BOT_MODAL_INVALID. Need more time? Answer DEFERRED_MESSAGE and send a follow-up.
Follow-ups, editing and deleting the answer
Use payload.id in the path, payload.token in the header X-Coviqo-Interaction-Token (secrets never go into URL paths) and your bot token. A token that does not belong to this interaction and bot (or expired) answers 404 BOT_INTERACTION_EXPIRED.
- POST /bot/interactions/{id}/followups – another message. Without
ephemeralthe visibility chosen withDEFERRED_MESSAGEapplies. The first follow-up afterDEFERRED_MESSAGEends “is thinking …” and becomes the original answer. Ephemeral follow-ups may carryexpiresAfterSeconds(since 1.7.0, see below). - PATCH /bot/interactions/{id}/original – edits the first answer; for
COMPONENTinteractions without an own answer, the message carrying the component. AfterDEFERRED_MESSAGEwithout an answer the edit becomes the answer. - DELETE /bot/interactions/{id}/original – deletes the first answer; an ephemeral answer also disappears live from the person's screen (e.g. delete it 5 s after answering).
- POST /bot/interactions/{id}/dm – answers in the person’s direct conversation with the bot.
curl -X POST "https://app.coviqo.com/api/v1/bot/interactions/$IA_ID/followups" \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" \
-H "X-Coviqo-Interaction-Token: $IA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"Done."}'Only visible to you (ephemeral)
ephemeral: true delivers the answer only to the person who triggered the interaction. It is never stored, does not appear for anybody else and disappears on reload. Buttons on it stay clickable for 15 minutes, then 410 BOT_INTERACTION_EXPIRED.
Let it disappear by itself (1.7.0): add expiresAfterSeconds (3–3600) to an ephemeral MESSAGE / UPDATE_MESSAGE, to DEFERRED_MESSAGE with ephemeral (applies to the later answer) or to an ephemeral follow-up – e.g. “Saved ✓” for 5 seconds. The person’s app removes the message at that time (with a short fade, none with reduced motion), also in other tabs and devices; its buttons stop working. The message carries an absolute expiresAt. Edits keep the expiry; a new expiresAfterSeconds on UPDATE_MESSAGE sets a new one. Editing an expired or deleted ephemeral message answers 410 BOT_EPHEMERAL_EXPIRED. On public messages the field is refused (502 BOT_INTERACTION_INVALID_RESPONSE as interaction response, 400 VALIDATION_FAILED on follow-ups).
{ "type": "MESSAGE", "data": { "content": "Saved ✓", "ephemeral": true, "expiresAfterSeconds": 5 } }Signature verification
Every request Coviqo sends to your interaction URL and event URL is signed with the bot’s Ed25519 key:
| Header | Content |
|---|---|
X-Coviqo-Signature-Ed25519 | Ed25519 signature, hex (128 characters) |
X-Coviqo-Signature-Timestamp | Unix time in seconds (string) |
- The signed message is timestamp + raw body (UTF-8) – the unchanged bytes of the request body; do not re-serialise parsed JSON.
- Verify with the public key of the bot (32 bytes raw, hex) from the developer area or
GET /bot/me. - Reject requests older than 300 seconds (replay protection).
- Answer invalid signatures with 401 – Coviqo tests this when the URL is saved.
- The private key is generated per bot, stored encrypted and never leaves Coviqo.
import { createPublicKey, verify } from 'node:crypto';
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); // Ed25519 SPKI header (RFC 8410)
export function verifyCoviqoSignature(publicKeyHex, signatureHex, timestamp, rawBody) {
if (!/^[0-9a-f]{128}$/i.test(signatureHex ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = createPublicKey({ key: Buffer.concat([SPKI_PREFIX, Buffer.from(publicKeyHex, 'hex')]), format: 'der', type: 'spki' });
return verify(null, Buffer.from(timestamp + rawBody, 'utf8'), key, Buffer.from(signatureHex, 'hex'));
}# pip install pynacl
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
def verify_coviqo_signature(public_key_hex: str, signature_hex: str, timestamp: str, raw_body: bytes) -> bool:
try:
VerifyKey(bytes.fromhex(public_key_hex)).verify(timestamp.encode() + raw_body, bytes.fromhex(signature_hex))
return True
except (BadSignatureError, ValueError):
return FalseComponents & forms
Messages can carry rows of buttons or a select menu (ActionRow); interactions can open forms (Modal) with text fields and (since 1.6.0) file upload fields.
{ "type": "row", "components": [
{ "type": "select", "customId": "lang", "placeholder": "Language", "minValues": 1, "maxValues": 1,
"options": [ { "label": "Deutsch", "value": "de" }, { "label": "English", "value": "en", "default": true } ] }
] }{ "type": "MODAL", "data": {
"customId": "feedback", "title": "Feedback",
"fields": [
{ "type": "text", "customId": "topic", "label": "Topic", "style": "short", "required": true, "maxLength": 100 },
{ "type": "text", "customId": "text", "label": "Your message", "style": "paragraph", "minLength": 10 }
] } }| Limit | Value |
|---|---|
| Rows per message | 5 |
| Per row | 1–5 buttons or exactly one select menu |
| Button styles | primary, secondary, success, danger, link – only link has an https url and no customId |
label | 1–80 characters; emoji up to 64; disabled |
customId | 1–100 printable ASCII characters, unique within the message / form |
| Select menu | 1–25 options (label / value 1–100, description up to 100, default), minValues 0–25, maxValues ≥ minValues and ≤ options, placeholder up to 150 |
| Form | title 1–45 characters, 1–5 fields: text fields (short / paragraph, required default true, minLength / maxLength up to 4000, placeholder, pre-filled value) and up to 2 file upload fields (since 1.6.0) |
| Message text | 4000 characters (Markdown) |
On every click Coviqo checks that the component exists on the message and is not disabled (404 BOT_COMPONENT_NOT_FOUND) and that chosen values are valid (400 BOT_COMPONENT_VALUES_INVALID). Form submissions are checked against your definition (400 BOT_MODAL_INVALID) and expire after 15 minutes (410 BOT_INTERACTION_EXPIRED). Invalid components in API calls answer 400 BOT_COMPONENTS_INVALID with details.errors. Since 1.6.0 a rejected submission keeps the form open – the person can correct it and send again.
Finding your boards again (1.7.0): the message history GET /bot/rooms/{roomId}/messages returns every message with its components as stored (null without). Your own messages keep their customIds – e.g. look for roles:board after a restart and delete duplicates (?author=self works without MESSAGE_CONTENT). Messages of other bots come with the same rows without customId – those ids are the other bot’s internal state.
File uploads in forms
Since 1.6.0 a form can ask for files: up to 2 fields of type file (aliases file_upload / FILE_UPLOAD, schema FileInput).
{ "type": "file", "customId": "screens", "label": "Screenshots of your rank", "required": true,
"maxFiles": 3, "maxSizeBytes": 5242880, "allowedTypes": ["image/*", ".pdf"] }| Field | Rules |
|---|---|
required | default true – at least one file |
maxFiles | 1–10, default 1 (alias max_files) |
maxSizeBytes | per file, at most 25 MiB (the default); the upload limit of the space’s plan applies too (alias max_size_bytes) |
allowedTypes | 1–20 MIME types (image/png), families (image/*) or extensions (.pdf); absent = every type (alias allowed_types) |
- The person picks or drops files in the form; each file goes through Coviqo’s normal upload pipeline – organisation policy, storage quota, encryption, type check of the first bytes and the malware scan. The form can only be sent once every file is clean. Files are stored privately in the room of the interaction: never in the room’s file list, search or reports.
- On submit Coviqo checks again that every file is the person’s own upload for exactly this form and field, clean, complete and within your limits – never pending, infected or end-to-end encrypted files.
- The
MODAL_SUBMITpayload carriesfiles: field customId → ModalFile[] (every file field,[]when empty) withid,name,size,mimeTypeand a signed absoluteurl(valid 10 minutes, fetch it without the bot token). - A fresh URL: GET /bot/interactions/:interactionId/files/:fileId/download (bot token, no interaction token) – only for files submitted to your bot in that interaction, for 72 hours; then Coviqo deletes them, so store what you keep. Other files: 404
BOT_FORM_FILE_NOT_FOUND. Deliveries and downloads are audited (without file names).
import { fileInput, showModal } from './coviqo-bot.mjs';
// open the form
return showModal({ customId: 'apply', title: 'Apply to the clan', fields: [
{ type: 'text', customId: 'about', label: 'About you', style: 'paragraph' },
fileInput('screens', 'Screenshots of your rank', { maxFiles: 3, allowedTypes: ['image/*'] }),
] });
// MODAL_SUBMIT
for (const f of i.files.screens) {
const res = await fetch(f.url); // signed – no token needed
await saveToWebsite(f.name, Buffer.from(await res.arrayBuffer()));
}
// later (within 72 h): const { url } = await bot.formFileDownloadUrl(i, f.id);Forms over the gateway (and after a deferred answer)
Gateway bots receive every interaction deferred, so they cannot answer with MODAL. Instead they call POST /bot/interactions/:interactionId/modal with the interaction token header and a Modal as body (the same limits as above). Bots with an interaction URL can do the same after DEFERRED_MESSAGE / DEFERRED_UPDATE. The form opens for the person who used the command or button (only in the tab that started it) and replaces “<Bot> is thinking …”; the submission arrives as a new MODAL_SUBMIT interaction (interaction.created on the gateway).
- Once per interaction, never for
MODAL_SUBMIT, not after an answer was posted (409BOT_MODAL_NOT_ALLOWED). - Only within 60 seconds of the interaction – the person is still waiting (409
BOT_MODAL_TOO_LATE). - Invalid forms: 400
BOT_MODAL_INVALIDwithdetails.errors. SDK:bot.openModal(interaction, modal).
// gateway bot: button "report" → form → submission
gateway.on('interaction.created', async (ev) => {
const i = ev.data.interaction;
if (i.type === 'COMPONENT' && i.customId === 'report') {
await bot.openModal(i, { customId: 'report-form', title: 'Report a problem', fields: [
{ type: 'text', customId: 'details', label: 'What happened?', style: 'paragraph', minLength: 10, maxLength: 1000 },
] });
} else if (i.type === 'MODAL_SUBMIT' && i.customId === 'report-form') {
await bot.followup(i, { content: 'Thanks – we look into it.', ephemeral: true });
}
});Incoming webhooks
The simplest integration: a URL that posts into one room – for CI, monitoring or scripts. No bot account, no token, no code on the Coviqo side. Space admins with bots.manage create it in Space settings → Bots → Webhooks (room, name, optional avatar). The URL is shown once:
https://app.coviqo.com/api/v1/hooks/<webhookId>?secret=<secret>curl -X POST "https://app.coviqo.com/api/v1/hooks/$HOOK_ID" \
-H "X-Coviqo-Webhook-Secret: $HOOK_SECRET" \
-H "Content-Type: application/json" \
-d '{"content":"Deploy **v2.3.1** succeeded","username":"CI","avatarUrl":"https://ci.example.com/logo.png"}'- The secret goes into the query parameter
secretor the headerX-Coviqo-Webhook-Secret. Rotating creates a new URL; the old one stops working at once. - Body
{ content (1–4000 characters), username?, avatarUrl? }→ 204.username/avatarUrl(https) override name and avatar for this one message. - The author is the person who created the webhook; the message shows the webhook’s name. If that person can no longer write in the room → 409
WEBHOOK_OWNER_INVALIDuntil an admin takes the webhook over. - Errors: 404
WEBHOOK_INVALID(unknown webhook or wrong secret – deliberately the same), 403WEBHOOK_DISABLED, 400MESSAGE_EMPTY/VALIDATION_FAILED, 429RATE_LIMITED, 503WEBHOOKS_DISABLED. Reference: POST /hooks/{webhookId}.
Events
Bots receive events from the spaces they are installed in – as event webhook (signed HTTPS POST to your event URL) or over the WebSocket gateway. Both use the same catalogue, the same checks and the same idempotency id.
- Only for active installations; bots stopped by Coviqo or paused in a space receive nothing.
- The intent must be granted; room events only when the bot can see the room and holds the event’s permission there.
- Message text only with
MESSAGE_CONTENT(otherwisecontent: null); direct messages to the bot always carry the text. - Switching voice rooms sends
voice.leftfor the old room, thenvoice.joinedwithfromRoomId= the old room (own switch, moved by a moderator or bot, AFK move; since 1.6.0) – only when your bot can view that room, otherwisenull. Moves additionally sendvoice.member_moved/voice.moved_afkas before; since 1.6.0voice.member_moved.fromRoomIdis filtered the same way (nullwhen your bot cannot view the origin room). - The bot’s own actions are not echoed back. End-to-end encrypted rooms never produce events for bots.
- At most 600 events per bot and minute – events beyond that are dropped.
| Event | Requirements |
|---|---|
message.created | the bot sees the room (room.view) and holds chat.view there; message text only with MESSAGE_CONTENT (otherwise content: null) |
message.updated | the bot sees the room (room.view) and holds chat.view there; message text only with MESSAGE_CONTENT (otherwise content: null) |
message.deleted | the bot sees the room (room.view) and holds chat.view there |
reaction.added | the bot sees the room (room.view) and holds chat.view there |
reaction.removed | the bot sees the room (room.view) and holds chat.view there |
member.joined | intent MEMBERS |
member.left | intent MEMBERS |
member.roles_updated | intent MEMBERS |
task.created | the bot sees the room (room.view) and holds tasks.view there; intent CONTENT_READ |
task.updated | the bot sees the room (room.view) and holds tasks.view there; intent CONTENT_READ |
task.completed | the bot sees the room (room.view) and holds tasks.view there; intent CONTENT_READ |
event.created | the bot sees the room (room.view) and holds events.view there |
event.updated | the bot sees the room (room.view) and holds events.view there |
event.canceled | the bot sees the room (room.view) and holds events.view there |
event.rsvp_updated | the bot sees the room (room.view) and holds events.view there; intent EVENT_RSVPS |
file.uploaded | the bot sees the room (room.view) and holds files.view there; intent CONTENT_READ |
voice.joined | the bot sees the room (room.view) |
voice.left | the bot sees the room (room.view) |
voice.member_moved | the bot sees the room (room.view) |
presence.updated | intent PRESENCE |
interaction.created | intent COMMANDS |
dm.message_created | intent COMMANDS |
meeting.transcript_ready | the bot sees the room (room.view); intent TRANSCRIPTS (plan: botTranscriptAccess) |
ticket.created | the bot sees the room (room.view); intent TICKETS_READ; the bot may see the ticket (tickets.view / handle / manage, or tickets.create for its own tickets) |
ticket.updated | the bot sees the room (room.view); intent TICKETS_READ; the bot may see the ticket (tickets.view / handle / manage, or tickets.create for its own tickets) |
ticket.message.created | the bot sees the room (room.view); intent TICKETS_READ; the bot may see the ticket (tickets.view / handle / manage, or tickets.create for its own tickets); internal notes only with tickets.handle |
ticket.message.updated | the bot sees the room (room.view); intent TICKETS_READ; the bot may see the ticket (tickets.view / handle / manage, or tickets.create for its own tickets); internal notes only with tickets.handle |
ticket.message.deleted | the bot sees the room (room.view); intent TICKETS_READ; the bot may see the ticket (tickets.view / handle / manage, or tickets.create for its own tickets); internal notes only with tickets.handle |
ticket.rated | the bot sees the room (room.view); intent TICKETS_READ; team permission tickets.view / handle / manage in the room |
ticket.sla.breached | the bot sees the room (room.view); intent TICKETS_READ; team permission tickets.view / handle / manage in the room |
member.banned | the bot holds moderate.ban in the space; intent MEMBERS |
member.unbanned | the bot holds moderate.ban in the space; intent MEMBERS |
member.kicked | the bot holds moderate.kick in the space; intent MEMBERS |
member.timeout | the bot holds moderate.timeout in the space; intent MEMBERS |
member.linked_account_updated | intent LINKED_ACCOUNTS |
invite.created | the bot holds space.invite in the space; intent MEMBERS |
invite.revoked | the bot holds space.invite in the space; intent MEMBERS |
voice.state_updated | the bot sees the room (room.view) |
voice.moved_afk | the bot sees the room (room.view) |
team.application.created | the bot holds teams.manage in the space; intent MEMBERS |
team.application.decided | the bot holds teams.manage in the space; intent MEMBERS |
team.member.added | intent MEMBERS |
team.member.removed | intent MEMBERS |
forum.post_created | the bot sees the room (room.view) and holds chat.view there; message text only with MESSAGE_CONTENT (otherwise content: null) |
room.created | the bot sees the room (room.view) |
room.updated | the bot sees the room (room.view) |
room.deleted | the bot could view the room (room.view) right before it was deleted |
room.permissions_updated | the bot sees the room (room.view); the changed entry (override) only with room.manage in the room |
Envelope (webhook body and gateway d) – EventEnvelope; the data of every event type is described in the OpenAPI webhooks section:
{
"id": "cm1evt…",
"type": "message.created",
"applicationId": "cm1app…",
"spaceId": "cm1space…",
"installationId": "cm1inst…",
"createdAt": "2026-10-02T09:00:00.000Z",
"data": { "message": { "id": "cm1msg…", "roomId": "cm1room…", "content": "Hello" } }
}Subscriptions
Which events arrive is set per installation: PUT /bot/spaces/{spaceId}/subscriptions (400 BOT_EVENTS_INVALID for unknown types). New installations take the default subscriptions you set in the developer area (Settings → Developer → your bot → “Events”); direct messages use these defaults too.
Event webhooks
- Enter the event URL in the developer area. Same URL rules as for interactions; on saving a signed
PINGevent (data: {}) must be answered with 2xx and a PING with a wrong signature with 401 – otherwise 400BOT_EVENT_URL_VERIFICATION_FAILED. - Headers: the signature headers plus
X-Coviqo-Event-Id(idempotency id – the same for retries and manual redeliveries; drop duplicates),X-Coviqo-Event-Type,X-Coviqo-Delivery-Attempt. - Answer 2xx within 5 seconds; the body is ignored (read up to 16 KB). Acknowledge first, process afterwards.
- 5xx, 408, 429, timeouts and network errors are retried – up to 8 attempts, exponential from 10 s (10 s, 20 s, 40 s … about 21 minutes). Other answers (incl. redirects) fail at once. At most 4 deliveries per bot run in parallel.
- Automatic stop: after 50 failed events in a row spanning at least 24 hours, Coviqo stops deliveries and notifies you (in-app and e-mail). A short outage never stops anything. Re-enable in the developer area.
- The developer area shows the last 100 deliveries (time, event, status, attempts, HTTP status, latency, error) with “Redeliver” (same event id, 10 per minute).
WebSocket gateway
The alternative to event webhooks – no public URL needed, your bot connects out: wss://app.coviqo.com/api/v1/bot/gateway. Plain WebSocket with JSON text frames { op, d?, s?, t? }.
| op | Name | Direction | Content |
|---|---|---|---|
| 10 | HELLO | server → bot | first frame, { heartbeatInterval: 41250 } (ms) |
| 2 | IDENTIFY | bot → server | { token, events? } – events narrows the event types (default: the subscriptions) |
| 6 | RESUME | bot → server | { token, sessionId, seq } after a dropped connection |
| 0 | DISPATCH | server → bot | t = event type, s = sequence, d = envelope; first t: "READY" ({ sessionId, applicationId, botUserId, events, spaces }) or "RESUMED" ({ sessionId, replayed }) with s: null |
| 1 | HEARTBEAT | bot → server | d = last sequence received |
| 11 | HEARTBEAT_ACK | server → bot | heartbeat acknowledged |
| 7 | RECONNECT | server → bot | reconnect and resume |
| 9 | INVALID_SESSION | server → bot | session cannot be resumed – identify again |
{"op":10,"d":{"heartbeatInterval":41250}}
{"op":2,"d":{"token":"cqb_…","events":["message.created","interaction.created"]}}
{"op":0,"t":"READY","s":null,"d":{"sessionId":"…","applicationId":"…","botUserId":"…","events":["message.created","interaction.created"],"spaces":[{"id":"…","installationId":"…"}]}}
{"op":0,"t":"message.created","s":1,"d":{"id":"cm1evt…","type":"message.created","data":{"message":{"…":"…"}}}}
{"op":1,"d":1}
{"op":11}| Code | Meaning |
|---|---|
| 4000 | unknown error / session taken over by another connection |
| 4001 | unknown op |
| 4002 | invalid JSON, binary frame, frame too large or invalid RESUME |
| 4003 | no IDENTIFY / RESUME within 10 s |
| 4004 | invalid token – do not reconnect with it |
| 4005 | second IDENTIFY / RESUME |
| 4007 | RESUME with a sequence ahead of the session |
| 4008 | more than 120 frames per minute |
| 4009 | heartbeat missing |
| 4013 | unknown type in events |
| 4014 | more than 5 connections per bot |
| 4015 | bot stopped by Coviqo, platform switched off, token rotated or bot deleted |
- Resume: up to 1000 events per session are kept for 5 minutes after a connection drops; later →
INVALID_SESSION. - Client frames at most 16 KB. Permissions, intents and content rules are the same as for webhooks.
- Interactions over the gateway: a bot without interaction URL and with an open connection receives commands, clicks and form submissions as
interaction.created(incl. token; alsodm.message_created, always on for such bots). The person sees “<Bot> is thinking …” (clicks are acknowledged likeDEFERRED_UPDATE); answer with the follow-up endpoints within 15 minutes. To open a form, callPOST /bot/interactions/:interactionId/modal(see Forms over the gateway). The composer shows the commands of such bots while a connection is open.
Direct messages
A conversation with a bot is a normal 1:1 direct message with the BOT account as participant.
- People can write to a bot when they share a space where it is installed and active, or when it is listed in the bot directory. Not possible: group conversations with bots (400
DM_BOT_GROUP). - The bot may only write in a conversation where the person wrote first, or in answer to an interaction of that person (within 15 minutes) – never unsolicited (403
BOT_DM_NOT_ALLOWED). People can block a bot (403BOT_DM_BLOCKED). - Direct messages support link buttons only (400
BOT_DM_COMPONENTS_LINK_ONLY). Limits: 120 messages per minute per bot, 20 per person. - Endpoints: Direct messages; new messages arrive as event
dm.message_created.
Voice bots
From plan Community Pro (voiceBots) a bot can join voice rooms with POST /bot/rooms/{roomId}/voice/join. The answer contains a LiveKit URL and token for the bot identity – connect with any LiveKit client SDK. The bot is a visible participant with the BOT badge.
- Joining needs
room.viewandroom.join. - Listening (
listen: true) needs the approved intentVOICE_LISTEN; while a listening bot is in the room, everybody in the room sees “Bot is listening”. - Speaking (
publish: true, microphone only) needsvoice.speak; on stages and in moderated rooms the bot speaks only as a speaker, like people (403BOT_VOICE_CANNOT_SPEAK). Never video, screen sharing or data channels. - Never in meetings, direct messages or lobby rounds (403
BOT_VOICE_NOT_ALLOWED); in rooms external guests can see only with the room setting “Bots may join despite external guests” (403BOT_VOICE_GUESTS); archived rooms 409ROOM_ARCHIVED; without the plan 402PLAN_LIMIT_VOICE_BOTS. - Sessions are re-checked every 15 seconds: pausing, removing, stopping the bot, revoking
VOICE_LISTEN, a plan change or a guest joining end the connection. Every join and leave is audited.
Transcript access
Plans Business and Enterprise (botTranscriptAccess): with the privileged intent TRANSCRIPTS and the rooms a space admin released to the bot, it receives finished meeting transcripts and AI summaries.
- Event
meeting.transcript_readyonce per meeting and stage (transcript, later the summary). - Fetch with GET /bot/spaces/{spaceId}/meetings and GET /bot/meetings/{meetingId}/transcript.
- The space’s rules for meeting results apply: if transcripts are visible to participants only, no bot gets them (403
BOT_TRANSCRIPT_POLICY). Rooms not released → 403BOT_TRANSCRIPT_ROOM_NOT_APPROVED; running meetings → 409MEETING_STILL_LIVE. - Every delivery and read is audited; the meeting view shows “Bot receives transcript”.
Tickets
Private support tickets of the room module TICKETS. A bot needs both: the privileged data access TICKETS_READ (reads, events) / TICKETS_WRITE (writes), approved by a space admin, and the ticket permissions of its role in the room.
| Room permission | Tickets | Internal notes | Writes |
|---|---|---|---|
tickets.create | only the tickets it opened itself | no | open tickets, reply and close own tickets, edit their description and hand them over to a member (1.6.0) |
tickets.view | all tickets of the room | no | – |
tickets.handle | all tickets of the room | yes | reply, internal notes, status, priority, category, assignee, fields, description, participants; statistics and ratings |
tickets.manage | all tickets of the room | no | statistics and ratings |
- Tickets the bot may not see answer 404
TICKET_NOT_FOUND. Being added as participant never makes a ticket visible to a bot; bots cannot be participants or assignees. - Writes run as the bot account (history, notifications, SLA as for people), count against the write limits and are audited (
BOT_TICKET_ACTION, without content). Writes on tickets the bot did not open needTICKETS_READtoo. - Events:
ticket.created,ticket.updated(withchanges),ticket.message.created, since 1.7.0ticket.message.updatedandticket.message.deleted(ids and kind only) – internal notes only withtickets.handle–,ticket.ratedandticket.sla.breached(team permissions only). The SLA check runs every minute; due times that passed while the service was down for more than 10 minutes are not reported afterwards. - Attachments: upload with POST /bot/rooms/{roomId}/tickets/attachments (needs
tickets.createortickets.handle) – the same three steps and limits as for people (plan upload size and storage, blocked file types, malware scan, never in end-to-end encrypted rooms) – then pass the file ids inattachmentIdsof a new ticket or a reply (at most 10). - Reading attachments (1.4.0,
TICKETS_READ): GET /bot/tickets/{ticketId}/attachments lists every attachment the bot may see – files of the description (messageIdnull) and of the replies, files of internal notes only withtickets.handle; deleted files are left out, files still in the malware scan or blocked as infected are listed with theirstatus. GET /bot/tickets/{ticketId}/attachments/{fileId}/download returns a full signed URL (https://…, always absolute since 1.6.0) valid for 10 minutes (fetch it without the bot token; streamed, HTTP Range supported). Errors: 404TICKET_ATTACHMENT_NOT_FOUND(not an attachment of this ticket, or hidden from the bot), 409FILE_NOT_READY, 403FILE_INFECTED, 409FILE_E2EE(end-to-end encrypted). The organisation's download policy applies; every download is recorded in the audit log (without the file name). - Description (1.6.0): PATCH /bot/tickets/{ticketId} with
body– withtickets.handleon any ticket of the room, otherwise only on the bot's own tickets (opened as itself or on behalf of the creator). Same limits as when opening; closed tickets only withtickets.handle(409TICKET_CLOSED). The history shows “description edited” (without the text),ticket.updatedcarries{ field: "description" }, newly mentioned people are notified. - Message authors (1.6.0): every ticket message – in the ticket, in the answer to a reply and in
ticket.message.created– hasauthorwithid,displayName(the name inside the space) andisBot. No username, avatar or e-mail. - Edit and delete replies (1.7.0): PATCH / DELETE /bot/tickets/{ticketId}/messages/{messageId} – messages the bot wrote itself or posted on behalf of a member (others: 403
TICKET_MESSAGE_NOT_AUTHOR; deleting messages of others needstickets.manage). Public replies of closed tickets only withtickets.handle. People can edit and delete their own replies in the app the same way.
On behalf of a member
By default the bot is the creator of the tickets it opens. To open a ticket for a member – the member becomes the creator (reporter) and gets the usual notifications, the ticket shows “created by <bot> on behalf of <member>” – there are exactly two ways:
- Interaction proof (
TICKETS_WRITEonly): passinteractionIdand send the interaction token in the headerX-Coviqo-Interaction-Token– e.g. after a form (MODAL_SUBMIT). The person of that interaction becomes the reporter; the ticket room may differ from the interaction room but must be in the same space. onBehalfOfUserId– any active member, only with the privileged data accessTICKETS_CREATE_ON_BEHALF(together withTICKETS_WRITE), e.g. for a ticket form on your clan website.
The member must be an active member of the space (no guests, bots or people who left) and able to open the ticket as a person (sees the room, tickets.create, not timed out) – otherwise 403 TICKET_REPORTER_INVALID / TICKET_REPORTER_NO_ACCESS. Both ways together must name the same person (403 TICKET_INTERACTION_MISMATCH). At most 10 tickets on behalf of members per minute and space (429 RATE_LIMITED, scope botticket); every one is audited with the member. The bot keeps seeing such a ticket only with team permissions.
Hand over tickets the bot opened as itself (1.6.0): POST /bot/tickets/{ticketId}/reporter with onBehalfOfUserId or interactionId (+ header) – e.g. old tickets your website opened before it knew the person. Same rules and limit as above; the ticket then is exactly a ticket opened on behalf. Already handed over by this bot → 409 TICKET_REPORTER_ALREADY_SET (details.sameReporter tells whether it was the same member – treat a retry as done); tickets of other reporters → 409 TICKET_REPORTER_NOT_BOT. The answer contains the ticket once.
Reply on behalf of a member (1.7.0): POST /bot/tickets/{ticketId}/messages with onBehalfOfUserId (privileged data access TICKETS_REPLY_ON_BEHALF – a separate approval, TICKETS_CREATE_ON_BEHALF does not cover replies) or the interaction proof. The person must be the reporter or a participant of the ticket, an active member who still sees the room and is not timed out (403 TICKET_REPLIER_INVALID with details.reason); public replies only. The reply is the member’s own reply – status rules, notifications and mentions as if they had written it – and shows “via <bot>” (viaBotId). At most 30 per minute and space (scope botticketreply); audited with the member. The bot needs to see the ticket (team permissions or its own ticket).
# open tickets with an overdue SLA, newest change first
curl -H "Authorization: Bot $TOKEN" \
"https://app.coviqo.com/api/v1/bot/rooms/$ROOM/tickets?status=open&overdue=true"
# reply to the creator
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"Thanks – we are on it.","kind":"PUBLIC"}' \
"https://app.coviqo.com/api/v1/bot/tickets/$TICKET/messages"
# take over and raise the priority (tickets.handle)
curl -X PATCH -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"status":"IN_PROGRESS","priority":"HIGH"}' \
"https://app.coviqo.com/api/v1/bot/tickets/$TICKET"
# correct the description of your own ticket (1.6.0)
curl -X PATCH -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"Server api-2 is down since 09:12 (updated)"}' \
"https://app.coviqo.com/api/v1/bot/tickets/$TICKET"# open a ticket for a member from your website form (TICKETS_CREATE_ON_BEHALF)
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Application: Alex","body":"Sent via the clan website","onBehalfOfUserId":"'$MEMBER'"}' \
"https://app.coviqo.com/api/v1/bot/rooms/$ROOM/tickets"
# … or for the person who just submitted your form (interaction proof)
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-H "X-Coviqo-Interaction-Token: $INTERACTION_TOKEN" \
-d '{"title":"Report","interactionId":"'$INTERACTION_ID'"}' \
"https://app.coviqo.com/api/v1/bot/rooms/$ROOM/tickets"
# relay an e-mail answer of the reporter into the ticket (1.7.0, TICKETS_REPLY_ON_BEHALF)
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"Still broken after the update.","onBehalfOfUserId":"'$MEMBER'"}' \
"https://app.coviqo.com/api/v1/bot/tickets/$TICKET/messages"
# hand a ticket the bot opened as itself over to the member (1.6.0)
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"onBehalfOfUserId":"'$MEMBER'"}' \
"https://app.coviqo.com/api/v1/bot/tickets/$TICKET/reporter"
# attach a screenshot: 1) announce, 2) PUT the bytes, 3) complete, then use the id
curl -X POST -H "Authorization: Bot $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"screenshot.png","mimeType":"image/png","size":48213}' \
"https://app.coviqo.com/api/v1/bot/rooms/$ROOM/tickets/attachments"
# read attachments: list, then fetch a signed URL and download it
curl -H "Authorization: Bot $TOKEN" "https://app.coviqo.com/api/v1/bot/tickets/$TICKET/attachments"
URL=$(curl -s -H "Authorization: Bot $TOKEN" "https://app.coviqo.com/api/v1/bot/tickets/$TICKET/attachments/$FILE/download" | jq -r .url)
curl -o attachment.bin "$URL"Reference: GET /bot/rooms/{roomId}/tickets, GET /bot/tickets/{ticketId}, POST /bot/rooms/{roomId}/tickets, POST /bot/rooms/{roomId}/tickets/attachments, POST /bot/tickets/{ticketId}/messages, PATCH /bot/tickets/{ticketId}, POST /bot/tickets/{ticketId}/reporter, GET /bot/tickets/{ticketId}/attachments, GET /bot/tickets/{ticketId}/attachments/{fileId}/download.
Rooms, permissions & voice moves
Bots can create, edit, archive and delete rooms and categories and set room permissions for roles, members and teams (teams since 1.6.0) – with the privileged data access ROOMS_MANAGE (a space admin approves it per installation) plus the permissions of the bot role, exactly like a person: room.create to create, room.manage in the room to edit, archive, delete and change permissions, room.manage in the space for categories. Everything shows up in the activity / audit log with the bot as actor and counts to the write limit per bot and space; a bot creates at most 50 rooms and categories per space and day (429 RATE_LIMITED).
| Endpoint | Needs | What it does |
|---|---|---|
GET /bot/spaces/:spaceId/categories | – | Categories (all of them with room.create). |
POST /bot/spaces/:spaceId/categories | ROOMS_MANAGE + room.manage (space) | { name } – also PATCH / DELETE /bot/categories/:categoryId. Deleting keeps the rooms. |
POST /bot/spaces/:spaceId/rooms | ROOMS_MANAGE + room.create | { name, type, visibility?, description?, categoryId?, modules?, tags?, icon?, color?, userLimit? } → 201 room. |
PATCH /bot/rooms/:roomId | ROOMS_MANAGE + room.manage | Edit; omitted fields stay. A category move (categoryId) also needs room.manage in the space. |
POST /bot/rooms/:roomId/archive | unarchive | ROOMS_MANAGE + room.manage | Archive (read-only, restorable) or restore. |
DELETE /bot/rooms/:roomId | ROOMS_MANAGE + room.manage | Permanent – the content goes with the room, a running call ends. |
GET /bot/rooms/:roomId/overrides | room.manage | Role, member and team entries of the room. |
PUT|DELETE /bot/rooms/:roomId/overrides/roles/:roleId | ROOMS_MANAGE + room.manage | { allow, deny } replaces the role’s entry; DELETE removes it. |
PUT|DELETE /bot/rooms/:roomId/overrides/members/:userId | ROOMS_MANAGE + room.manage | Same for one member. |
PUT|DELETE /bot/rooms/:roomId/overrides/teams/:teamId | ROOMS_MANAGE + room.manage | Same for a team and its sub-teams (since 1.6.0) – see “Team entries”. |
POST /bot/rooms/:roomId/voice/members/:userId/move | voice.move_members (+ room.join in the target) | { targetRoomId } – moves a person between voice rooms → 204. |
DELETE /bot/rooms/:roomId/voice/members/:userId | voice.move_members | Disconnects a person from the call (they can rejoin) → 204. |
What bots can create
Room types: STANDARD, VOICE, PROJECT, EVENT, COMMUNITY, STAGE, FORUM – no meeting, lobby or map rooms. Modules: VOICE, CHAT, NOTES, FILES, TASKS, EVENTS, WHITEBOARD, FORUM, TICKETS, DECISIONS, MEETINGS (web pages and feeds are set up by people). Without categoryId the room lands in the first category, and a new room starts synced with its category’s permissions – as in the app. Rooms of meetings and lobby rounds cannot be changed (409 MEETING_ROOM_MANAGED / LOBBY_SESSION_MANAGED), end-to-end encrypted rooms are off-limits for bots, and the space’s default room and AFK room are never deleted or archived by a bot (409 BOT_ROOM_PROTECTED).
curl -X POST https://app.coviqo.com/api/v1/bot/spaces/SPACE_ID/rooms \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Squad 3","type":"VOICE","userLimit":5}'Room permissions
allow / deny take room-scoped permission keys (at most 64 each; deny wins). On top of the rules for people, a bot may only target roles strictly below its own role – never the owner role, another bot’s role (its own neither) or a role with admin-level permissions – and members whose highest role is below the bot role (never the owner, administrators or bots). The default role (everyone) is allowed: lock a room for everyone, then open it for one role.
No escalation: every permission the request adds or removes – in allow or deny – must be held by the bot itself in that room (403 PERMISSION_ESCALATION) and must not be one bots can never hold (400 BOT_PERMISSION_FORBIDDEN). Entries an admin set with more rights stay as they are; the bot can leave them untouched but not change or delete them. Changing an entry of a room that is synced with its category ends the sync.
# hide #officers from everyone, open it for the Officer role
curl -X PUT https://app.coviqo.com/api/v1/bot/rooms/ROOM_ID/overrides/roles/DEFAULT_ROLE_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"allow":[],"deny":["room.view"]}'
curl -X PUT https://app.coviqo.com/api/v1/bot/rooms/ROOM_ID/overrides/roles/OFFICER_ROLE_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"allow":["room.view","room.join"],"deny":[]}'Team entries (since 1.6.0)
A team entry applies to every member of the team and of its sub-teams – handy for clan squads: one team per squad, its members get the squad channel. The team must be a team of this space or an organisation team of its organisation (like in the room settings, 404 TEAM_NOT_FOUND otherwise). The same permission rules as for roles apply (only permissions the bot holds in the room, never forbidden ones) plus two team rules:
- No bots in the team. A team with a bot account among its members or sub-team members is refused (409
BOT_MEMBER_MANAGED) – room permissions of bots are managed in the space settings. - Deny entries need the rank. A deny – set, kept or lifted by the change – restricts every member, so every member of the team and its sub-teams must be strictly below the bot role: never the owner or a member with an admin-level role (403
TEAM_HIERARCHY{ reason, userId }). Allow-only entries need no rank (teams have none).
Admin-level permissions are never set for teams (400 BOT_PERMISSION_FORBIDDEN). Together with the ceiling “only what the bot holds” this keeps team entries consistent with 1.4.0’s TEAM_GRANTS_ABOVE_BOT: the bot’s own entries never stop it from opening the team, and an OPEN team that anyone may join gets at most what the bot could give the default role.
# squad channel for the team "Squad 3" (and its sub-teams)
curl -X PUT https://app.coviqo.com/api/v1/bot/rooms/ROOM_ID/overrides/teams/TEAM_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"allow":["room.view","room.join","voice.speak"],"deny":[]}'Moving and disconnecting people in voice
Only the permission voice.move_members (no data access, like kick / ban) – in both rooms for a move, plus room.join in the target. The person must be strictly below the bot role: never the owner, administrators, people with an equal or higher role or other bots (403 MODERATION_HIERARCHY, 409 BOT_MEMBER_MANAGED). Moves that would need a confirmation in the app – a full room, or a target the person may not join – are refused (409 VOICE_MOVE_CONFIRM_REQUIRED). A move arrives as the event voice.member_moved with the bot as moverId (since 1.6.0 fromRoomId is null for bots that cannot view the origin room).
curl -X POST https://app.coviqo.com/api/v1/bot/rooms/ROOM_ID/voice/members/USER_ID/move \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"targetRoomId":"TARGET_ROOM_ID"}'Room events (since 1.6.0)
Subscribe to room.created, room.updated, room.deleted and room.permissions_updated (group “Rooms”, no extra data access) to keep a website or a squad list in sync. They come from people and other bots alike (actorId), never for the bot’s own calls, and only for rooms the bot can view: room.created / room.updated carry the room as GET/PATCH return it (room.updated with changes, e.g. name, description, categoryId, visibility, archived, modules; pure re-ordering sends nothing). room.deleted goes to the bots that could view the room right before it was deleted (roomId, name, categoryId, type). room.permissions_updated reports change (override_set, override_removed, synced, category_sync) and the changed entry in override – the entry only for bots with room.manage in the room, otherwise null. A bot that loses view access through a change (visibility, overrides, category sync) gets the same event with only { roomId, spaceId, accessLost: true } – no name, overrides or other details of the new state. Rooms of online meetings, lobby rounds and end-to-end encrypted rooms send nothing.
Calendar events: move, cancel, delete & RSVPs
Bots cancel calendar events with exactly the rules for people: their own events (created by the bot) always, other events only with the permission events.manage on the bot role. Who accepted or declined is personal data – reading it needs the privileged data access EVENT_RSVPS, which a space admin approves per installation, on top of events.view.
| Endpoint | Needs | What it does |
|---|---|---|
POST /bot/events/:eventId/cancel | own event or events.manage | Cancels the event (a recurring event as a whole series) and returns it with canceledAt set. Idempotent. |
DELETE /bot/events/:eventId | own event or events.manage | Same as cancel, answers 204. Coviqo never hard-deletes calendar events – RSVPs and paid tickets stay for the record. |
GET /bot/events/:eventId/rsvps?status&cursor&limit | EVENT_RSVPS + events.view | Answers, oldest first, with counts per status. Page size 100 by default, at most 500. |
PUT /bot/events/:eventId/rsvps/:userId | interaction proof or EVENT_RSVP_ON_BEHALF | { status: GOING | MAYBE | DECLINED, interactionId? } – answers on behalf of a member (since 1.4.0). |
DELETE /bot/events/:eventId/rsvps/:userId?interactionId | interaction proof or EVENT_RSVP_ON_BEHALF | Removes the member’s answer – back to “no answer” (since 1.5.0). 409 EVENT_RSVP_TICKET_HELD when a ticket is attached. |
Cancel and delete
Both run through the same service as the app: not while the bot is timed out, not in archived rooms and not while the events module of the room is off. The calendar entry of an online meeting can only be canceled by a moderator of the meeting (403 MEETING_NOT_ORGANIZER) – canceling it also cancels the meeting and tells its invitees. Every cancel counts to the write limit per bot and space and shows up in the activity with the bot as actor.
curl -X POST https://app.coviqo.com/api/v1/bot/events/EVENT_ID/cancel \
-H "Authorization: Bot $COVIQO_BOT_TOKEN"RSVPs
Statuses: GOING, REGISTERED, MAYBE, DECLINED, INVITED – REGISTERED is an acceptance of an event with registration, INVITED an invitation without an answer yet. Each item is { user: { id, username, displayName, bot }, status, respondedAt, viaBotId }; pass nextCursor as cursor for the next page. Coviqo records answers, not actual attendance.
curl "https://app.coviqo.com/api/v1/bot/events/EVENT_ID/rsvps?status=GOING&limit=100" \
-H "Authorization: Bot $COVIQO_BOT_TOKEN"{
"eventId": "cm1event000000000000000",
"counts": { "GOING": 12, "REGISTERED": 0, "MAYBE": 3, "DECLINED": 2, "INVITED": 0 },
"items": [
{ "user": { "id": "cm1user0000000000000000", "username": "anna", "displayName": "Anna", "bot": false }, "status": "GOING", "respondedAt": "2026-10-02T09:00:00.000Z" }
],
"nextCursor": null
}Answering on behalf of members
Since 1.4.0 a bot can answer an event for a member: with the interaction proof (interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space by exactly this person – no extra data access) or with the privileged data access EVENT_RSVP_ON_BEHALF (any active member). The bot must see the event; the member must be an active member who sees it – then all rules of a personal answer apply (canceled events, capacity, tickets, webinar registration) and event.rsvp_updated is sent. The answer shows “via” and the bot’s name (viaBotId in the RSVP list) until the member answers themselves. At most 10 per minute and space on top of the write limit; audited as BOT_MEMBER_ACTION.
curl -X PUT https://app.coviqo.com/api/v1/bot/events/EVENT_ID/rsvps/USER_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"status":"GOING"}'Since 1.5.0 a bot can also remove a member’s answer entirely (DELETE /bot/events/:eventId/rsvps/:userId, 204) – the person is back to “no answer”; invitees of an online meeting stay invited. Same gate as answering: with the interaction proof (pass interactionId as query parameter plus the header) only the answer of the person who used the bot; with EVENT_RSVP_ON_BEHALF any answer of that member, no matter whether the member, your bot or another bot gave it – Coviqo stays the source of truth for the member’s choice and shows the change right away. Answers tied to a ticket (paid or free) are never removed by bots: 409 EVENT_RSVP_TICKET_HELD with details.ticketStatus and details.paid – the member cancels or the organiser refunds the ticket in Coviqo. Without an answer nothing happens. Counts to the same limit as answering; audited as BOT_MEMBER_ACTION event_rsvp_removed.
curl -X DELETE "https://app.coviqo.com/api/v1/bot/events/EVENT_ID/rsvps/USER_ID?interactionId=INTERACTION_ID" \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" \
-H "X-Coviqo-Interaction-Token: $INTERACTION_TOKEN"events.view: the room of the event must be visible to the bot (otherwise 404 EVENT_NOT_FOUND), calendar entries of online meetings only when the bot may see the meeting, nothing from end-to-end encrypted rooms. Only answers of current members of the space are listed, with their space identity and without avatar or e-mail.Moving an event to another room
Since 1.6.0 PATCH /bot/events/:eventId accepts roomId: the event moves to that room of the same space – a move, not a copy, so the id stays and RSVPs, tickets and reminders stay attached. A recurring event moves as a whole series. The bot needs events.manage in the old and the new room (for events the bot created: events.create in the new room), must see the new room with events.view (a room it cannot see answers 404 ROOM_NOT_FOUND, an event in a room it cannot see 404 EVENT_NOT_FOUND), the Events module must be on there and the room not archived (400 ROOM_MODULE_DISABLED, 409 ROOM_ARCHIVED). End-to-end encrypted rooms are refused. Calendar entries of online meetings move their meeting along and need “Meetings” in the new room. Bots move events between rooms only – roomId: null (space level) is 400 VALIDATION_FAILED. At most 10 moves per minute and space on top of the write limit; audited as EVENT_MOVED with the bot as actor.
curl -X PATCH https://app.coviqo.com/api/v1/bot/events/EVENT_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"roomId":"ROOM_ID"}'The resulting event.updated carries previousRoomId (the old room, null when the event was space-wide) and reaches every bot that can view the new or the old room – once. Members who cannot see the new room keep their answer but no longer see the event.
Events
event.created, event.updated carry { event } (event.updated only when the event itself changed, not when just the number of answers did). event.canceled carries { eventId, spaceId, roomId } – sent for cancel and delete alike. All three need events.view in the room (space level for space-wide events). event.rsvp_updated carries { eventId, roomId, user, status, respondedAt, source, viaBotId } and additionally needs the EVENT_RSVPS data access; it is never sent for calendar entries of online meetings. Since 1.5.0 source tells who answered – member (the person) or bot with the bot account in viaBotId (compare it with your own bot user id) – and status is null when the answer was removed.
Forum rooms
Forum rooms hold posts instead of a flowing chat: a post has a title, an optional first message, tags and a state (solved with an accepted answer, pinned, locked); the replies are the post’s discussion. Bots write here with the same permissions as people – chat.send to post and reply, forum.manage for moderator tags, pinning, locking and solving posts of others. Reading posts (titles, first messages, replies) needs the data access MESSAGE_CONTENT, like the message history; the forum itself (tags and rules) is standard data. POST /bot/rooms/:roomId/messages does not post into forum rooms – use the endpoints below.
| Endpoint | Needs | What it does |
|---|---|---|
GET /bot/rooms/:roomId/forum | chat.view | Tags, whether a tag is required, default sort, canPost / canManage of the bot. |
GET /bot/rooms/:roomId/forum/posts | chat.view + MESSAGE_CONTENT | ?sort=activity|newest|reactions&status=all|open|solved&tag=&q=&cursor=&limit= (default 25, at most 50). Pinned posts first. |
POST /bot/rooms/:roomId/forum/posts | chat.send | { title, content?, tagIds?, components?, nonce? } – creates a post. |
GET /bot/forum/posts/:postId | chat.view + MESSAGE_CONTENT | { post, message, acceptedAnswer } – the post with its first message. |
PATCH /bot/forum/posts/:postId | own post (chat.send) or forum.manage | { title?, tagIds? } – changes title / tags. |
GET /bot/forum/posts/:postId/replies | chat.view + MESSAGE_CONTENT | ?before=<replyId>&limit= (default 50, at most 100) – newest first. |
POST /bot/forum/posts/:postId/replies | chat.send (locked posts: forum.manage) | { content?, components?, replyToId?, nonce? } – replies in the post. |
PUT /bot/forum/posts/:postId/solved | own post or forum.manage | { solved, answerId? } – marks the post solved, optionally with a reply as the answer. |
PUT /bot/forum/posts/:postId/pinned | locked | forum.manage | { value } – pins / locks the post. |
Posting
Titles have 3–200 characters, the first message at most 4000 characters and may carry buttons / select menus like any bot message. A post has at most 5 tags of its forum; when the forum requires a tag, a post without one is refused (400 FORUM_TAG_REQUIRED), and moderator-only tags need forum.manage (403 FORUM_TAG_RESTRICTED). Every write counts against the write limits per bot and space.
curl -X POST https://app.coviqo.com/api/v1/bot/rooms/ROOM_ID/forum/posts \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Server maintenance on Friday","content":"The game server is down **20:00–22:00**.","tagIds":["TAG_ID"]}'
curl -X POST https://app.coviqo.com/api/v1/bot/forum/posts/POST_ID/replies \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"content":"Done – the server is back online."}'The post id is the id of its first message: edit or delete the body with PATCH / DELETE /bot/messages/:postId, react with the message reaction endpoints. Replies are messages too. Answers to changes (PATCH, solved, pinned, locked) carry title and excerpt for the bot’s own posts, for posts of others only with MESSAGE_CONTENT (otherwise null). A deleted post stays as a placeholder (deleted: true, no title) while it has replies.
Events
forum.post_created carries { post } – room-scoped on chat.view; title and excerpt are null without MESSAGE_CONTENT. The first message also arrives as message.created, and so do replies – with threadRootId set to the post id.
E2EE_FEATURE_DISABLED). Archived rooms, timeouts of the bot and locked posts apply exactly as for people.Teams, applications & announcements
Teams are named groups inside a space (they form a tree – members of a sub-team count for every parent team). Bots read teams with the data access MEMBERS; every change (members, application decisions, announcements) additionally needs the permission teams.manage on the bot role. Changes count to the write limit per bot and space and show up in the space’s activity / audit log with the bot as actor.
| Endpoint | Needs | What it does |
|---|---|---|
GET /bot/spaces/:spaceId/teams | MEMBERS | Space teams + organisation teams usable in the space (organisation teams are read-only). |
PATCH /bot/teams/:teamId | TEAMS_MANAGE + MEMBERS + teams.manage | { name?, joinPolicy?, applicationPrompt?, password? } – space teams only (since 1.4.0; password since 1.6.0). |
GET /bot/teams/:teamId/members | MEMBERS | Direct members, leads first. |
PUT /bot/teams/:teamId/members/:userId | MEMBERS + teams.manage | Add a member (idempotent), body { isLead? } sets the lead flag. |
DELETE /bot/teams/:teamId/members/:userId | MEMBERS + teams.manage | Remove a member (not for identity-provider memberships: 409 TEAM_IDP_MANAGED). |
GET /bot/teams/:teamId/applications?status | MEMBERS + teams.manage | Applications of a team with join policy APPLICATION, newest first. |
POST /bot/teams/:teamId/applications | MEMBERS + interaction proof or TEAM_APPLY_ON_BEHALF | { message, onBehalfOfUserId?, interactionId? } – applies on behalf of a member (since 1.4.0). |
POST /bot/team-applications/:applicationId/withdraw | MEMBERS + interaction proof or TEAM_APPLY_ON_BEHALF | { interactionId? } – withdraws an open application your bot created for the member, without cooldown (since 1.5.0). |
POST /bot/team-applications/:applicationId/decision | MEMBERS + teams.manage | { decision: ACCEPTED | DECLINED, note? } – accepting adds the person as a member. |
GET|POST /bot/teams/:teamId/announcements | MEMBERS (+ teams.manage to post) | { title?, content, roomId? } – notifies the team; roomId also posts it as a message. At most 10 per team and hour. |
Applications
People apply with a message (1000 characters at most) to teams with the join policy APPLICATION. One open application per team, at most 5 open applications per person and space, and 24 h before a declined or withdrawn applicant may apply to the same team again. Team leads (also of parent teams), space team managers and bots with teams.manage decide.
curl -X POST https://app.coviqo.com/api/v1/bot/team-applications/APPLICATION_ID/decision \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"decision":"ACCEPTED","note":"Welcome aboard!"}'Team settings
Since 1.4.0 a bot with the privileged data access TEAMS_MANAGE (and teams.manage on its role) can rename a space team, change its join policy (OPEN, APPLICATION, INVITE_ONLY) and set the question of the application dialog – nothing else: the team tree, identity-provider groups, position and room grants stay with people, organisation teams are read-only (400 TEAM_SPACE_ONLY). The change shows up as TEAM_UPDATED with the bot as actor.
OPEN) lets every member join on their own – and get the room permissions the team grants. A bot therefore cannot switch a team to OPEN while the team or one of its parent teams grants room or category permissions the bot does not hold itself: 409 TEAM_GRANTS_ABOVE_BOT with details.permissions.curl -X PATCH https://app.coviqo.com/api/v1/bot/teams/TEAM_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"joinPolicy":"APPLICATION","applicationPrompt":"Which games do you play?"}'Joining with a password
Since 1.6.0 the join policy PASSWORD lets people join a team on their own with a password – for example one your website hands out after a sign-up. Send joinPolicy: "PASSWORD" together with password (6–128 characters); password alone changes the password of a team that already uses PASSWORD. Switching to PASSWORD without one is 400 TEAM_JOIN_PASSWORD_REQUIRED, a password with any other join policy 400 TEAM_JOIN_PASSWORD_NOT_APPLICABLE; switching to another policy deletes it. The password is write-only: Coviqo stores a hash, never returns it (the team only shows hasJoinPassword) and the audit names the change joinPassword without the value. People enter it in the app; 5 attempts per person and team (60 per team) every 15 minutes.
OPEN: 409 TEAM_GRANTS_ABOVE_BOT while the team or a parent team grants room or category permissions the bot does not hold itself (not re-checked for a team that is already OPEN).curl -X PATCH https://app.coviqo.com/api/v1/bot/teams/TEAM_ID \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"joinPolicy":"PASSWORD","password":"raid-night-2026"}'Acting on behalf of members
Since 1.4.0 a bot can apply to a team (and answer calendar events, see “Calendar events”) for a member – never for itself (400 BOT_ON_BEHALF_REQUIRED). Two ways to name the member: (1) interaction proof – interactionId plus the header X-Coviqo-Interaction-Token of an interaction of this bot in this space; no extra data access, the person just used the bot; (2) onBehalfOfUserId with the privileged data access TEAM_APPLY_ON_BEHALF (any active member, e.g. a recruitment form on your website). Both given must name the same person (403 BOT_INTERACTION_MISMATCH). Guests, bots, suspended accounts and people who left are refused (403 BOT_ON_BEHALF_INVALID with details.reason). Everything else runs exactly as if the member applied: join policy, one open application per team, cooldown, the deciders are notified, team.application.created is sent. The application shows “via” and the bot’s name (createdByBotId) and the member can withdraw it. At most 10 per minute and space on top of the write limit; audited as BOT_MEMBER_ACTION without the message.
# after a button click / form of the member – no extra data access
curl -X POST https://app.coviqo.com/api/v1/bot/teams/TEAM_ID/applications \
-H "Authorization: Bot $COVIQO_BOT_TOKEN" -H "Content-Type: application/json" \
-H "X-Coviqo-Interaction-Token: $INTERACTION_TOKEN" \
-d '{"message":"I would like to join the raid team.","interactionId":"INTERACTION_ID"}'Since 1.5.0 the bot can withdraw an open application it created for a member (POST /bot/team-applications/:applicationId/withdraw, same proof as applying – the interaction must be the applicant’s, or TEAM_APPLY_ON_BEHALF). It is the same as the member withdrawing it, but starts no 24 h cooldown – so a mistaken application no longer has to be declined. Applications the member wrote themselves or another bot created are refused (403 TEAM_APPLICATION_NOT_BY_BOT), decided ones answer 409 TEAM_APPLICATION_NOT_PENDING. The application then carries withdrawnByBotId; applying and withdrawing share one limit.
Events
team.application.created, team.application.decided carry { application } (message and decision note – only delivered to bots holding teams.manage). A withdrawn application arrives as team.application.decided with status WITHDRAWN (withdrawnByBotId set when a bot withdrew it, since 1.5.0). team.member.added, team.member.removed carry { teamId, userId, isLead, actorId, via } – via is manual, application, self, idp when added and removed, left, idp, member_left when removed. All four need the MEMBERS data access and concern space teams.
voice.move_members or other leads of the team). This is a person-only action – there is no bot endpoint for it.Sign in with Coviqo (OAuth 2.0 / OpenID Connect)
Let people sign in to your website or app with their Coviqo account – like “Sign in with Discord / Google”. Coviqo is a standard OpenID Connect provider: most libraries (Auth.js / NextAuth, Keycloak, Passport openid-client, Spring Security, WordPress OIDC plugins) only need the discovery URL, the client ID and the client secret. Login only: an app receives the identity – never spaces, messages, files or contacts (only exception: membership and roles in spaces a space admin linked to the app, see space.membership).
- Create a login app in Settings → Developer → Login apps: name, redirect URIs and client type. The client secret (
cqs_…) is shown once and can be rotated. - Point your library at the discovery document
https://app.coviqo.com/.well-known/openid-configuration(also served as/.well-known/oauth-authorization-server, RFC 8414). - The person signs in on Coviqo (password, 2FA, SSO as usual), sees a consent screen once per app and is sent back.
| Endpoint | URL |
|---|---|
| Issuer | https://app.coviqo.com |
| Authorization | https://app.coviqo.com/oauth/authorize |
| Token | https://app.coviqo.com/api/v1/oauth/token |
| Userinfo | https://app.coviqo.com/api/v1/oauth/userinfo |
| Revocation (RFC 7009) | https://app.coviqo.com/api/v1/oauth/revoke |
| JWKS | https://app.coviqo.com/api/v1/oauth/jwks |
Scopes and claims
| Scope | Claims |
|---|---|
openid | sub (pairwise), plus an ID token (ES256 JWT) with iss, aud (your client ID), exp, iat, auth_time, nonce |
profile | name, preferred_username, picture, locale |
email | email, email_verified |
space.membership | coviqo_spaces – membership, roles and teams in the spaces linked to your app (details) |
Supported scopes: openid, profile, email, space.membership – nothing else (unknown scopes → invalid_scope). The sub is pairwise: different for every app and stable for the same person, so two apps cannot correlate a user. Key your user table on sub, not on the e-mail address.
Authorization code flow with PKCE
GET https://app.coviqo.com/oauth/authorize
?client_id=<client id>
&redirect_uri=https://example.com/auth/coviqo/callback
&response_type=code
&scope=openid%20profile%20email
&state=<random>
&nonce=<random>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256
→ 302 https://example.com/auth/coviqo/callback?code=<code>&state=<random>&iss=https://app.coviqo.com
POST https://app.coviqo.com/api/v1/oauth/token
Authorization: Basic base64(<client id>:<client secret>)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<code>&redirect_uri=https://example.com/auth/coviqo/callback&code_verifier=<verifier>
→ 200 { "access_token": "cqo_…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile email", "id_token": "eyJ…" }
GET https://app.coviqo.com/api/v1/oauth/userinfo
Authorization: Bearer cqo_…Auth.js / NextAuth example:
{
id: 'coviqo',
name: 'Coviqo',
type: 'oidc',
issuer: 'https://app.coviqo.com',
clientId: '<client id>',
clientSecret: '<client secret>', // from your server's secret store
checks: ['pkce', 'state', 'nonce'],
}Space membership and roles (space.membership)
A community website can learn whether the signed-in person is a member of its space and which roles and teams they have there. A person with the space.manage permission links your login app to the space in Settings → Developer → Login apps → Linked spaces (at most 5 spaces per app; workspaces and communities only). Space admins see and revoke linked apps in the space settings under Integrations. Requesting space.membership without any linked space returns invalid_scope.
- The consent screen lists the linked spaces and tells the person that the app receives their global Coviqo user ID (
user_id, the same for every app and the same ID as in the bot API). Without this scope your app only gets the pairwisesub. - The consent covers the spaces linked at that moment – linking another space asks every person again. Unlinking a space removes it from all consents.
- Checked live on every call: unlinking, revoking access, leaving or being banned from the space and role changes apply at once, also to access tokens already issued. A space that is not (or no longer) linked or shared is simply missing from the array.
GET https://app.coviqo.com/api/v1/oauth/userinfo
Authorization: Bearer cqo_… (scope: openid profile space.membership)
→ 200
{
"sub": "<pairwise>",
"name": "Alex",
"preferred_username": "alex",
"picture": "https://…",
"locale": "de",
"coviqo_spaces": [
{
"space_id": "csp1", "space_slug": "wardogsgermany", "space_name": "WardogsGermany",
"member": true,
"user_id": "cusr1", "member_id": "cmem1",
"display_name": "Alex [WDG]",
"is_guest": false,
"joined_at": "2026-09-01T18:00:00.000Z",
"roles": [
{ "id": "crole2", "name": "Officer", "position": 5, "color": "#e67e22" },
{ "id": "crole1", "name": "Member", "position": 1, "color": null }
],
"teams": [{ "id": "cteam1", "name": "Squad Alpha", "is_lead": true }],
"timeout_until": null
},
{ "space_id": "csp2", "space_slug": "wdg-events", "space_name": "WDG Events", "member": false }
]
}roles: highestpositionfirst, then by name.teams: teams of this space only.display_name: the person's name in this space.timeout_until: only while a timeout is running, elsenull. Non-members (left, banned, never joined) get the short form withmember: false.- One space:
GET https://app.coviqo.com/api/v1/oauth/userinfo/spaces/{spaceId}/member(same Bearer token) returns exactly one element ofcoviqo_spaces. Errors:403 insufficient_scope(token without the scope),404 space_not_linked,429 slow_downafter 60 lookups per token and minute (Retry-After: 60). - The ID token carries a short form computed when the code is exchanged:
"coviqo_spaces": [{ "space_id": "csp1", "member": true, "role_ids": ["crole2", "crole1"] }]. Call userinfo for the current state. - Discovery lists
space.membershipinscopes_supportedandcoviqo_spacesinclaims_supported. There are no refresh tokens: to keep roles up to date later, use the bot API of a bot installed in the space with the storeduser_id(token API reference).
Rules
- PKCE (S256) is mandatory for every app;
plainis rejected. Send astate, and anoncewhen you use the ID token. Check theissparameter of the redirect (RFC 9207). - Redirect URIs match exactly (at most 10 per app): https only, http only for
localhost/127.0.0.1/[::1](the loopback port may vary, RFC 8252), private-use schemes of native apps likecom.example.app:/callback; no wildcards, no#fragment. An unknown client or redirect URI shows an error page on Coviqo – it is never redirected. - Client authentication at the token and revocation endpoint:
client_secret_basicorclient_secret_post. Public clients (native / mobile apps that cannot keep a secret) send onlyclient_idand rely on PKCE. Browser single-page apps need a small backend – there is no CORS on the token endpoint. - The code is valid for 60 seconds and redeemable once. Access tokens are opaque, valid for 1 hour and only for the userinfo endpoint (not for the bot API). ID tokens are valid for 1 hour. There are no refresh tokens – keep your own session.
prompt=nonechecks silently (errorslogin_required/consent_required);prompt=consentalways shows the consent screen.- Signing keys rotate; always verify ID tokens against the JWKS by
kid(retired keys stay published for 24 hours). Cache the JWKS for minutes, not days. - Bot, guest and demo accounts cannot sign in to apps, and organisations can forbid it for their managed accounts – the person sees why on the consent screen and can cancel, which returns
access_deniedto your app. - People can remove access at any time (Settings → Connected services); the current access token stops working at once. Coviqo can block an app – sign-in, token and userinfo then fail.
- At most 25 login apps per person. The token and revocation endpoints are limited to 120 requests per minute per IP.
Errors of the token, userinfo and revocation endpoints follow RFC 6749 §5.2: { "error": "invalid_grant", "error_description": "…" } with Cache-Control: no-store – e.g. invalid_request, invalid_client, invalid_grant, unsupported_grant_type, invalid_token, temporarily_unavailable.
Rate limits
Fixed one-minute windows. Exceeding a limit answers 429.
| Scope | Limit | Counts |
|---|---|---|
| per bot | 120 / min | all token API calls of a bot (details.scope bot) |
| per bot and space | 30 / min | writes: messages, edits, deletions, reactions, tasks, notes, files, moderation … (botspace) |
| per space | 120 / min | writes of all bots and webhooks of a space together (space) |
| per incoming webhook | 30 / min | calls of one webhook (hook) |
| direct messages | 120 / min per bot, 20 / min per person | (botdm, botdmuser) |
| rooms per bot and space | 50 / day | new rooms and categories (botrooms) |
| per person | 30 / min | interactions a person triggers (commands, clicks, forms) |
| autocomplete per person | 120 / min | suggestion requests while typing (own counter, user-ac) |
| form uploads per person | 30 / min | files uploaded for file fields of forms (user-form-upload, since 1.6.0) |
| HTTP throttler | 600 / min per token | all requests of one token (incoming webhooks: 600 / min per IP) |
- Platform limits answer 429
RATE_LIMITEDwithdetails: { retryAfter, limit, windowSec, scope }–retryAfteris the number of seconds until the next window. - The HTTP throttler answers 429
TOO_MANY_REQUESTSwith headerRetry-After(seconds). Every token API response carriesX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Resetof this throttler – the stricter platform limits above are not part of these headers. - Back off on 429 and retry after the given time; do not retry 4xx answers otherwise.
Errors
Every error has the same JSON shape ApiError: { statusCode, code, message, details? }. Branch on code, not on message. The endpoint reference lists the codes per endpoint.
{ "statusCode": 403, "code": "MISSING_PERMISSION", "message": "Missing permission chat.send", "details": { "permission": "chat.send" } }| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED, MESSAGE_EMPTY, BOT_COMPONENTS_INVALID, BOT_COMMANDS_INVALID, BOT_EVENTS_INVALID | invalid input (details lists the problems) |
| 401 | BOT_AUTH_REQUIRED, BOT_TOKEN_INVALID | header missing / token unknown or rotated |
| 402 | PLAN_LIMIT_VOICE_BOTS, PLAN_LIMIT_BOT_TRANSCRIPTS, PLAN_LIMIT_STORAGE | not included in the plan of the space |
| 403 | MISSING_PERMISSION | a permission of the bot role is missing (details.permission) |
| 403 | BOT_INTENT_REQUIRED | data access missing (details.intent) |
| 403 | BOT_DISABLED, BOT_PAUSED | stopped by Coviqo / paused in this space by an admin |
| 403 | MEMBER_TIMED_OUT, MESSAGE_NOT_AUTHOR | the bot is timed out / editing a message of somebody else |
| 404 | SPACE_NOT_FOUND, ROOM_NOT_FOUND, MESSAGE_NOT_FOUND | not installed there, unknown room / message |
| 404 | BOT_INTERACTION_EXPIRED, BOT_INTERACTION_NO_ORIGINAL | interaction token wrong / expired; no original answer yet |
| 409 | E2EE_FEATURE_DISABLED | end-to-end encrypted room – bots cannot read or write there |
| 429 | RATE_LIMITED, TOO_MANY_REQUESTS | see rate limits |
| 503 | BOTS_DISABLED, WEBHOOKS_DISABLED | switched off platform-wide by the operator |
Plan limits
Bot features depend on the plan of the space’s organisation (current values – the operator can change plans and agree individual limits):
| Plan | Bots per space | Incoming webhooks per space | Voice bots | Transcript access |
|---|---|---|---|---|
| Free | 2 | 2 | no | no |
| Community Pro | 15 | 10 | yes | no |
| Community Plus | 15 | 10 | yes | no |
| Business | 50 | 50 | yes | yes |
| Enterprise | unlimited | unlimited | yes | yes |
“Bots per space” counts developer bots and Coviqo bots together (402 PLAN_LIMIT_BOTS_PER_SPACE when installing); webhooks: 402 PLAN_LIMIT_INCOMING_WEBHOOKS_PER_SPACE. Independent of the plan: at most 25 developer bots per account (409 BOT_APP_LIMIT).
Security & privacy
Rules for your URLs (SSRF protection)
- Interaction and event URLs must be https, without user name / password, at most 500 characters.
- No internal names (
localhost,*.local,*.internal,*.lan,*.home.arpa,*.corp,*.intranet, names without a dot) and no private, loopback or link-local addresses. - Ports 443, 80 or 1024 and above – well-known ports of internal services (databases, Redis, SSH, mail …) are refused.
- DNS is checked when you save the URL and again on every connection (DNS pinning, no DNS rebinding); redirects are never followed.
- Errors when saving: 400
BOT_URL_INVALID,BOT_URL_SCHEME,BOT_URL_CREDENTIALS,BOT_URL_PRIVATE,BOT_URL_PORT,BOT_URL_TOO_LONG. - Requests come from the public IP addresses of the Coviqo API servers. You may filter by them, but the signature check is the real protection.
What Coviqo never sends to your bot
- Anything from rooms the bot cannot see, or from spaces where it is paused or not installed.
- Message text without
MESSAGE_CONTENT, files / notes / tasks withoutCONTENT_READ, presence withoutPRESENCE, audio withoutVOICE_LISTEN, transcripts withoutTRANSCRIPTSand an admin’s room release. - Anything from end-to-end encrypted rooms – bots can neither read nor write there.
- E-mail addresses, phone numbers or other contact data of people – only ids, user names and display names.
- Secrets: the bot’s private signing key, other tokens, webhook secrets. Your own token cannot be shown again either – Coviqo stores only its hash.
Your responsibilities
- Bots receive decrypted content. The consent dialog and the bot profile therefore show operator, server location and privacy policy (https). You are responsible for how your bot processes data.
- If your bot generates answers with AI, switch on “Uses AI”: all its messages then carry the AI label (EU AI Act Art. 50).
- Keep the token secret; rotate it immediately if it leaks. Verify every signature and timestamp.
- Admins can change permissions and data access, pause or remove the bot at any time; Coviqo Trust & Safety can stop a bot platform-wide (403
BOT_DISABLED).
Bot directory
Public bots can be listed in the Coviqo bot directory, where people search by category and install with one click. Every listing is reviewed by Coviqo first; private bots stay installable with their link.
- Submit in the developer area (section “Bot directory”): short description 10–140 characters, description up to 4000, 1–3 categories (
moderation,productivity,utilities,fun,music,gaming,education,developer,social,ai), up to 5 https screenshots. Operator, server location and privacy URL are required (400BOT_DIRECTORY_INCOMPLETE). - Status:
NONE→PENDING(in review) →APPROVED(listed) orREJECTED(with a reason). You can withdraw at any time. - Changing requested permissions or data access, the interaction or event URL, privacy URL, operator, server location or “Uses AI” sends a listed bot back to review (hidden until approved again).
- Listed bots can be installed by anybody with
bots.managein a space, and people can start direct conversations with them. “Verified” marks bots whose operator identity Coviqo checked.
SDK & example bot
A tiny, dependency-free Node SDK (plain ESM with JSDoc types, Node 22 or newer) – copy it into your project and adapt it:
- coviqo-bot.mjs – the SDK: REST client, signature check, interaction and event handlers, gateway client
- example-bot.mjs – example bot with /ping, /feedback (form), /vote (buttons) and /timezone (autocomplete)
CoviqoBot (REST client of the token API; request(method, path, body) for anything else), verifySignature, createInteractionHandler (node:http handler: signature check, PING → PONG, dispatch), createEventHandler (event URL with signature check and de-duplication), CoviqoGateway (WebSocket client with heartbeats, reconnect and resume) and response helpers reply, deferReply, updateMessage, deferUpdate, showModal, button, row; since 1.6.0 the builders fileInput, roomOption and choice and bot.formFileDownloadUrl.
COVIQO_BOT_TOKEN=cqb_… COVIQO_PUBLIC_KEY=<64 hex> COVIQO_API=https://app.coviqo.com/api/v1 \
node example-bot.mjsimport { CoviqoBot, CoviqoGateway } from './coviqo-bot.mjs';
const { COVIQO_BOT_TOKEN } = process.env; // cqb_… – keep it secret
const bot = new CoviqoBot({ token: COVIQO_BOT_TOKEN, apiBase: 'https://app.coviqo.com/api/v1' });
await bot.setCommands([{ name: 'ping', description: 'Answers with Pong' }]);
// events without a public URL
const gateway = new CoviqoGateway({ token: COVIQO_BOT_TOKEN, apiBase: 'https://app.coviqo.com/api/v1' });
gateway.on('message.created', (event) => console.log(event.data.message.content)); // null without MESSAGE_CONTENT
gateway.on('interaction.created', async (event) => {
await bot.followup(event.data.interaction, { content: 'Pong', ephemeral: true });
});
gateway.connect();OpenAPI
The token API, incoming webhooks, the interaction request and every event delivery are described as OpenAPI 3.1 (version 1.7.0; interaction requests and events in the webhooks section):
- Download OpenAPI (JSON) · YAML
- Postman: Import → Link → paste the JSON URL. Insomnia: Import → From URL. Set the authorization to API key, header
Authorization, valueBot <token>, and the server variablehostto your Coviqo host. - Generate clients with any OpenAPI 3.1 generator.
Data types
All request and response schemas of the reference (same names as in the OpenAPI document).
ApiError
| Field | Type | Description |
|---|---|---|
statusCode · required | integer | HTTP status |
code · required | string | Machine-readable error code, e.g. MISSING_PERMISSION |
message · required | string | Human-readable message (English) |
details | Extra data of some errors, e.g. { permission }, { intent }, { errors }, { retryAfter, limit, windowSec, scope } |
UserSummary
| Field | Type | Description |
|---|---|---|
id · required | string | User id |
username · required | string | |
displayName · required | string | Name to show (the per-space name when the person uses one in this space) |
avatarUrl · required | string | null | |
bot | boolean | BOT account |
deactivated | boolean | Deactivated by their organisation |
spaceIdentity | object | Per-space identity details, when present |
BotProfile
| Field | Type | Description |
|---|---|---|
applicationId · required | string | Application id |
botUser · required | UserSummary | |
name · required | string | |
description · required | string | null | |
avatarUrl · required | string | null | |
operatorName · required | string | null | Who runs the bot |
serverLocation · required | string | null | |
privacyUrl · required | string | null | |
usesAi · required | boolean | Messages carry the AI label (EU AI Act Art. 50) |
builtinKind · required | string | null | Always null for developer bots |
disabled · required | boolean | Stopped by Coviqo Trust & Safety |
verified · required | boolean | Operator identity checked by Coviqo (bot directory) |
BotMe
| Field | Type | Description |
|---|---|---|
applicationId · required | string | Application id |
botUser · required | UserSummary | |
name · required | string | |
description · required | string | null | |
avatarUrl · required | string | null | |
operatorName · required | string | null | Who runs the bot |
serverLocation · required | string | null | |
privacyUrl · required | string | null | |
usesAi · required | boolean | Messages carry the AI label (EU AI Act Art. 50) |
builtinKind · required | string | null | Always null for developer bots |
disabled · required | boolean | Stopped by Coviqo Trust & Safety |
verified · required | boolean | Operator identity checked by Coviqo (bot directory) |
publicKey · required | string | Ed25519 public key (raw 32 bytes, hex) – verify interaction and event requests with it · pattern ^[0-9a-f]{64}$ |
requestedPermissions · required | string[] | |
requestedIntents · required | "COMMANDS" | "MEMBERS" | "MESSAGE_CONTENT" | "CONTENT_READ" | "PRESENCE" | "VOICE_LISTEN" | "TRANSCRIPTS" | "TICKETS_READ" | "TICKETS_WRITE" | "ROLES_ASSIGN" | "VOICE_ACTIVITY" | "LINKED_ACCOUNTS" | "ROOMS_MANAGE" | "EVENT_RSVPS" | "TICKETS_CREATE_ON_BEHALF" | "TEAMS_MANAGE" | "TEAM_APPLY_ON_BEHALF" | "EVENT_RSVP_ON_BEHALF" | "TICKETS_REPLY_ON_BEHALF"[] |
BotSpace
| Field | Type | Description |
|---|---|---|
id · required | string | Space id |
name · required | string | |
slug · required | string | |
iconUrl · required | string | null | |
installationId · required | string | Installation id |
enabled · required | boolean | false = paused by a space admin (write calls answer 403 BOT_PAUSED) |
permissions · required | string[] | Permissions of the managed role "Bot: <name>" |
intents · required | "COMMANDS" | "MEMBERS" | "MESSAGE_CONTENT" | "CONTENT_READ" | "PRESENCE" | "VOICE_LISTEN" | "TRANSCRIPTS" | "TICKETS_READ" | "TICKETS_WRITE" | "ROLES_ASSIGN" | "VOICE_ACTIVITY" | "LINKED_ACCOUNTS" | "ROOMS_MANAGE" | "EVENT_RSVPS" | "TICKETS_CREATE_ON_BEHALF" | "TEAMS_MANAGE" | "TEAM_APPLY_ON_BEHALF" | "EVENT_RSVP_ON_BEHALF" | "TICKETS_REPLY_ON_BEHALF"[] | Approved privileged intents (standard intents are always granted) |
BotRoom
| Field | Type | Description |
|---|---|---|
id · required | string | Room id |
name · required | string | |
type · required | "STANDARD" | "VOICE" | "PROJECT" | "MEETING" | "EVENT" | "COMMUNITY" | "STAGE" | "LOBBY" | "FORUM" | "MAP" | |
categoryId · required | string | null | |
position · required | integer | |
description · required | string | null |
BotRole
| Field | Type | Description |
|---|---|---|
id · required | string | Role id |
name · required | string | |
color · required | string | null | |
position · required | integer | |
mentionable · required | boolean | |
isDefault · required | boolean | The space's default role (@everyone): every member has it automatically – it is never assigned or removed. Since 1.6.0 |
assignable | boolean | The bot may assign this role right now (ROLES_ASSIGN intent, allow-list, below the bot role; admin-level roles only with “allow roles with admin rights”) |
JoinSource
Only for bots holding space.invite or space.manage_members
| Field | Type | Description |
|---|---|---|
via · required | "invite" | "vanity" | "public" | "lfg" | "sso" | "join_request" | "handover" | "onboarding" | How the member joined (null = before tracking) |
inviteId · required | string | null | |
inviteCode · required | string | null | |
vanityName · required | string | null | |
inviterId · required | string | null |
LinkedAccount
Only with the LINKED_ACCOUNTS intent and the member’s consent in this space
| Field | Type | Description |
|---|---|---|
provider · required | "discord" | |
id · required | string | Provider user id (Discord snowflake) |
username · required | string | null |
BotPresence
Only with presence=true and the PRESENCE intent
| Field | Type | Description |
|---|---|---|
status · required | "ONLINE" | "AWAY" | "BUSY" | "FOCUS" | "OFFLINE" | "INVISIBLE" |
BotMember
| Field | Type | Description |
|---|---|---|
memberId · required | string | Space member id |
user · required | UserSummary | |
displayName · required | string | |
roleIds · required | string[] | |
isGuest · required | boolean | |
joinedAt · required | string | date-time |
timeoutUntil | string | null | Timed out until (single member, moderation results) · date-time |
presence | BotPresence | |
joinSource | JoinSource | null | |
linkedAccounts | LinkedAccount[] |
BotMemberPage
| Field | Type | Description |
|---|---|---|
items · required | BotMember[] | |
nextCursor · required | string | null | Pass as cursor for the next page |
Button
| Field | Type | Description |
|---|---|---|
type · required | "button" | |
style · required | "primary" | "secondary" | "success" | "danger" | "link" | |
label · required | string | 1–80 chars |
customId | string | Required for every style except link · 1–100 chars |
url | string | Only for style link: https URL (no customId) · max 500 chars, uri |
emoji | string | max 64 chars |
disabled | boolean |
SelectOption
| Field | Type | Description |
|---|---|---|
label · required | string | 1–100 chars |
value · required | string | Unique within the menu · 1–100 chars |
description | string | max 100 chars |
emoji | string | max 64 chars |
default | boolean |
Select
| Field | Type | Description |
|---|---|---|
type · required | "select" | |
customId · required | string | 1–100 printable ASCII characters, unique within the message / form · 1–100 chars |
placeholder | string | max 150 chars |
options · required | SelectOption[] | 1–25 items |
minValues | integer | Default 1 · 0–25 |
maxValues | integer | Default 1; at least minValues, at most the number of options · 1–25 |
disabled | boolean |
ActionRow
ComponentsActionRow[]
At most 5 rows
TextInput
| Field | Type | Description |
|---|---|---|
type · required | "text" | |
customId · required | string | 1–100 printable ASCII characters, unique within the message / form · 1–100 chars |
label · required | string | 1–80 chars |
style | "short" | "paragraph" | Default short |
required | boolean | Default true |
placeholder | string | max 150 chars |
minLength | integer | 0–4000 |
maxLength | integer | 1–4000 |
value | string | Pre-filled value · max 4000 chars |
FileInput
Since 1.6.0. At most 2 per form. The person uploads through the normal upload pipeline (malware scan, storage quota, organisation policy); the submit delivers files[customId] (ModalFile) with signed download URLs. Only clean, completely scanned files that fit the limits are accepted – never end-to-end encrypted ones.
| Field | Type | Description |
|---|---|---|
type · required | "file" | File upload field. Since 1.6.0 – file_upload / FILE_UPLOAD are accepted as aliases |
customId · required | string | 1–100 printable ASCII characters, unique within the message / form · 1–100 chars |
label · required | string | 1–80 chars |
required | boolean | Default true: at least one file |
maxFiles | integer | Files the person may upload, default 1. Alias max_files · 1–10 |
maxSizeBytes | integer | Per file, default and hard cap 26214400 (25 MiB); the upload limit of the space's plan applies too. Alias max_size_bytes · 1–26214400 |
allowedTypes | string[] | MIME types (image/png), families (image/*) or extensions (.pdf), lower case – absent = every type. Alias allowed_types · 1–20 items |
Modal
Reaction
| Field | Type | Description |
|---|---|---|
emoji · required | string | |
count · required | integer | |
userIds · required | string[] | |
me · required | boolean | The bot reacted with this emoji |
ReactionList
| Field | Type | Description |
|---|---|---|
reactions · required | Reaction[] |
Message
MessageDto – the same shape the web app receives. Further optional fields may appear; ignore unknown fields.
| Field | Type | Description |
|---|---|---|
id · required | string | Message id |
roomId · required | string | Room id |
spaceId · required | string | Space id |
author · required | UserSummary | |
content · required | string | |
type · required | "DEFAULT" | "SYSTEM" | |
replyTo | object | null | |
attachments | object[] | File attachments |
reactions · required | Reaction[] | |
mentions · required | string[] | Mentioned user ids |
mentionEveryone · required | boolean | |
pinned | boolean | |
editedAt | string | null | date-time |
deletedAt | string | null | date-time |
createdAt · required | string | date-time |
components | Components | null | |
ai | object | null | AI label (bots with "uses AI") |
webhook | object | null | Posted through an incoming webhook |
interaction | object | null | Answer to a slash command: who used which command |
MessagePage
| Field | Type | Description |
|---|---|---|
items · required | Message[] | |
nextCursor · required | string | null | Pass as before for the next (older) page |
BotMessageRequest
At least content or components (400 MESSAGE_EMPTY otherwise). Unknown fields are ignored.
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Message to reply to |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
BotEditMessageRequest
Missing fields stay unchanged; components: null removes the components.
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | null |
BotFollowupRequest
At least content or components.
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
ephemeral | boolean | Only the person who used the interaction sees it (not stored). Default: the visibility chosen with DEFERRED_MESSAGE, else false |
expiresAfterSeconds | integer | Ephemeral messages only (400 VALIDATION_FAILED otherwise): the message disappears from the person’s screen after this many seconds (3–3600). Default for the first answer of a deferred interaction: the value given with DEFERRED_MESSAGE. Since 1.7.0 · 3–3600 |
FollowupResult
| Field | Type | Description |
|---|---|---|
id · required | string | Message id (or ephemeral message id) |
ephemeral · required | boolean |
EditOriginalResult
| Field | Type | Description |
|---|---|---|
id · required | string | Edited message id (or ephemeral message id) |
OpenModalResult
| Field | Type | Description |
|---|---|---|
opened · required | true | |
expiresInSec · required | integer | How long the form can be submitted (single use) |
CommandOption
| Field | Type | Description |
|---|---|---|
name · required | string | a-z 0-9 _ - (lower case) · pattern ^[a-z0-9_-]{1,32}$ |
description · required | string | 1–100 chars |
type · required | "string" | "integer" | "number" | "boolean" | "user" | "room" | "role" | user / room / role values are ids of this space |
required | boolean | Required options come before optional ones |
choices | object[] | Only for string / integer / number; values must respect the limits below · max 25 items |
minValue | number | integer / number only: smallest allowed value (inclusive; integers for integer). Since 1.4.0 – min_value is accepted as an alias |
maxValue | number | integer / number only: largest allowed value (inclusive, ≥ minValue). Since 1.4.0 – alias max_value |
minLength | integer | string only: at least this many characters after trimming (0–4000). Since 1.4.0 – alias min_length · 0–4000 |
maxLength | integer | string only: at most this many characters (1–4000, ≥ minLength). Since 1.4.0 – alias max_length · 1–4000 |
autocomplete | boolean | string / integer / number only, not together with choices: Coviqo sends AUTOCOMPLETE interactions while the person types this option – answer AUTOCOMPLETE_RESULT. Since 1.4.0 |
channelTypes | "text" | "voice"[] | room only: which rooms may be chosen – text = rooms with chat, voice = rooms with their own voice channel (a room with both fits either); absent = every room. The room picker of the app offers only matching rooms, other rooms are refused before delivery (400 BOT_COMMAND_OPTIONS_INVALID). Since 1.6.0 – alias channel_types · 1–2 items |
AutocompleteChoice
| Field | Type | Description |
|---|---|---|
name · required | string | Shown to the person · 1–100 chars |
value · required | string | number | Inserted as the option value – must match the option type and limits (invalid entries are dropped) |
description | string | Short secondary line shown under the name – longer than 100 characters is cut off, non-strings are ignored. Since 1.6.0 |
AutocompleteResultData
AUTOCOMPLETE_RESULT data (HTTP answer) / body of POST /bot/interactions/{interactionId}/autocomplete (gateway bots). Since 1.4.0
| Field | Type | Description |
|---|---|---|
choices · required | AutocompleteChoice[] | At most 25 (more are cut off) · max 100 items |
AutocompleteAnswerResult
| Field | Type | Description |
|---|---|---|
accepted · required | true | |
choices · required | integer | Choices passed on to the person |
dropped · required | string[] | Why entries were dropped (invalid name / value, cut off) – for debugging |
CommandInput
| Field | Type | Description |
|---|---|---|
name · required | string | a-z 0-9 _ - (lower case), unique per bot · pattern ^[a-z0-9_-]{1,32}$ |
description · required | string | 1–100 chars |
options | CommandOption[] | max 10 items |
Command
| Field | Type | Description |
|---|---|---|
name · required | string | a-z 0-9 _ - (lower case), unique per bot · pattern ^[a-z0-9_-]{1,32}$ |
description · required | string | 1–100 chars |
options | CommandOption[] | max 10 items |
id · required | string | Command id |
applicationId · required | string | Application id |
ReplaceCommandsRequest
| Field | Type | Description |
|---|---|---|
commands · required | CommandInput[] | The complete list (replaces all commands), at most 50 · max 50 items |
BotSubscriptions
| Field | Type | Description |
|---|---|---|
installationId · required | string | Installation id |
spaceId · required | string | Space id |
events · required | "message.created" | "message.updated" | "message.deleted" | "reaction.added" | "reaction.removed" | "member.joined" | "member.left" | "member.roles_updated" | "task.created" | "task.updated" | "task.completed" | "event.created" | "event.updated" | "event.canceled" | "event.rsvp_updated" | "file.uploaded" | "voice.joined" | "voice.left" | "voice.member_moved" | "presence.updated" | "interaction.created" | "dm.message_created" | "meeting.transcript_ready" | "ticket.created" | "ticket.updated" | "ticket.message.created" | "ticket.message.updated" | "ticket.message.deleted" | "ticket.rated" | "ticket.sla.breached" | "member.banned" | "member.unbanned" | "member.kicked" | "member.timeout" | "member.linked_account_updated" | "invite.created" | "invite.revoked" | "voice.state_updated" | "voice.moved_afk" | "team.application.created" | "team.application.decided" | "team.member.added" | "team.member.removed" | "forum.post_created" | "room.created" | "room.updated" | "room.deleted" | "room.permissions_updated"[] |
SubscriptionsRequest
| Field | Type | Description |
|---|---|---|
events · required | string[] | Event types (400 BOT_EVENTS_INVALID with details.events for unknown ones) · max 96 items |
EventUser
| Field | Type | Description |
|---|---|---|
id · required | string | User id |
username · required | string | |
displayName · required | string | |
bot · required | boolean |
EventMessage
| Field | Type | Description |
|---|---|---|
id · required | string | Message id |
roomId · required | string | Room id |
spaceId · required | string | Space id |
author · required | EventUser | |
content · required | string | null | null without MESSAGE_CONTENT – direct messages to the bot always carry the text |
replyToId · required | string | null | |
threadRootId | string | null | Reply in a thread: id of the root message – in forum rooms the forum post id (null for timeline messages) |
mentions · required | string[] | |
mentionEveryone · required | boolean | |
attachmentCount · required | integer | |
webhook · required | object | null | |
createdAt · required | string | date-time |
editedAt · required | string | null | date-time |
EventMessagePage
| Field | Type | Description |
|---|---|---|
items · required | EventMessage[] | |
nextCursor · required | string | null | Pass as before for the next (older) page |
HistoryMessage
A message of the history: EventMessage plus its components (Since 1.7.0).
| Field | Type | Description |
|---|---|---|
id · required | string | Message id |
roomId · required | string | Room id |
spaceId · required | string | Space id |
author · required | EventUser | |
content · required | string | null | null without MESSAGE_CONTENT – direct messages to the bot always carry the text |
replyToId · required | string | null | |
threadRootId | string | null | Reply in a thread: id of the root message – in forum rooms the forum post id (null for timeline messages) |
mentions · required | string[] | |
mentionEveryone · required | boolean | |
attachmentCount · required | integer | |
webhook · required | object | null | |
createdAt · required | string | date-time |
editedAt · required | string | null | date-time |
components · required | Components | null |
HistoryMessagePage
| Field | Type | Description |
|---|---|---|
items · required | HistoryMessage[] | |
nextCursor · required | string | null | Pass as before for the next (older) page |
EventMember
| Field | Type | Description |
|---|---|---|
memberId · required | string | |
user · required | EventUser | |
displayName · required | string | |
isGuest · required | boolean | |
roleIds · required | string[] | |
joinedAt · required | string | date-time |
linkedAccounts | LinkedAccount[] |
Invite
| Field | Type | Description |
|---|---|---|
id · required | string | Invite id |
code · required | string | |
url · required | string | |
createdBy · required | EventUser | null | |
uses · required | integer | |
maxUses · required | integer | null | |
expiresAt · required | string | null | date-time |
revokedAt · required | string | null | date-time |
asGuest · required | boolean | |
roleId · required | string | null | |
joinedMembers · required | integer | Active members that joined through this invite |
createdAt · required | string | date-time |
VoiceState
| Field | Type | Description |
|---|---|---|
selfMuted · required | boolean | |
serverMuted · required | boolean | |
deafened · required | boolean | |
video · required | boolean | |
screenShare · required | boolean | |
onStage · required | boolean | |
inAfkRoom · required | boolean |
TeamApplication
| Field | Type | Description |
|---|---|---|
id · required | string | Team application id |
teamId · required | string | Team id |
userId · required | string | User id |
user · required | EventUser | |
message · required | string | |
status · required | "PENDING" | "ACCEPTED" | "DECLINED" | "WITHDRAWN" | |
decidedById · required | string | null | |
decidedAt · required | string | null | date-time |
decisionNote · required | string | null | |
createdByBotId · required | string | null | Bot account that applied on behalf of the applicant ("via <bot>") – null = applied themselves. Since 1.4.0 |
withdrawnByBotId · required | string | null | Bot account that withdrew the application on behalf of the applicant (status WITHDRAWN, no re-application cooldown) – null = not withdrawn or withdrawn by the person. Since 1.5.0 |
createdAt · required | string | date-time |
Task
| Field | Type | Description |
|---|---|---|
id · required | string | Task id |
spaceId · required | string | Space id |
roomId · required | string | null | |
title · required | string | |
description · required | string | null | |
status · required | "TODO" | "IN_PROGRESS" | "DONE" | "CANCELED" | |
priority · required | "LOW" | "MEDIUM" | "HIGH" | "URGENT" | |
dueDate · required | string | null | date-time |
assigneeId · required | string | null | |
createdById · required | string | null | |
tabId · required | string | null | Task list ("tab") of the room – null = the default tab |
categoryId · required | string | null | Category (task type) of the room – null = none |
category · required | object | null | |
completedAt · required | string | null | date-time |
createdAt · required | string | date-time |
updatedAt · required | string | date-time |
CreateTaskRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
description | string | null | max 10000 chars |
assigneeId | string | null | User id of a member |
dueDate | string | null | date-time |
priority | "LOW" | "MEDIUM" | "HIGH" | "URGENT" | |
tabId | string | null | Task list ("tab") of the room – omitted / null = the default tab |
categoryId | string | null | Category (task type) of the room – omitted / null = none |
UpdateTaskRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
description | string | null | max 10000 chars |
status | "TODO" | "IN_PROGRESS" | "DONE" | "CANCELED" | |
priority | "LOW" | "MEDIUM" | "HIGH" | "URGENT" | |
dueDate | string | null | date-time |
assigneeId | string | null | |
tabId | string | null | Move the task to another task list of its room – null = the default tab |
categoryId | string | null | Category (task type) of the room – null = none |
CalendarEvent
| Field | Type | Description |
|---|---|---|
id · required | string | Event id |
spaceId · required | string | Space id |
roomId · required | string | null | |
title · required | string | |
description · required | string | null | |
startAt · required | string | date-time |
endAt · required | string | date-time |
allDay · required | boolean | |
timezone · required | string | IANA time zone |
location · required | string | null | |
createdById · required | string | null | |
canceledAt · required | string | null | date-time |
createdAt · required | string | date-time |
EventRsvp
| Field | Type | Description |
|---|---|---|
user · required | EventUser | |
status · required | "GOING" | "REGISTERED" | "MAYBE" | "DECLINED" | "INVITED" | GOING / REGISTERED = attending, MAYBE, DECLINED, INVITED = invited without an answer yet |
respondedAt · required | string | date-time |
viaBotId · required | string | null | Bot account that gave this answer on behalf of the person ("via <bot>") – null = answered themselves. Since 1.4.0 |
PutEventRsvpRequest
| Field | Type | Description |
|---|---|---|
status · required | "GOING" | "MAYBE" | "DECLINED" | GOING becomes REGISTERED for events with registration |
interactionId | string | Interaction proof: an interaction of this bot by this person – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
EventRsvpResult
| Field | Type | Description |
|---|---|---|
eventId · required | string | Event id |
userId · required | string | User id |
status · required | "GOING" | "REGISTERED" | "MAYBE" | "DECLINED" | "INVITED" | Effective answer (GOING → REGISTERED for events with registration) |
viaBotUserId · required | string | The bot account |
respondedAt · required | string | date-time |
EventRsvpPage
| Field | Type | Description |
|---|---|---|
eventId · required | string | Event id |
counts · required | object | Answers per status (all statuses, independent of the filter) |
items · required | EventRsvp[] | Oldest answer first |
nextCursor · required | string | null | Pass as cursor for the next page |
CreateEventRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
startAt · required | string | date-time |
endAt · required | string | date-time |
description | string | null | max 5000 chars |
allDay | boolean | |
timezone | string | IANA time zone, e.g. Europe/Berlin · max 64 chars |
recurrenceRule | string | null | RFC 5545 RRULE · max 500 chars |
location | string | null | max 500 chars |
requiresRegistration | boolean | |
capacity | integer | null | 1–1000000 |
reminderMinutes | integer[] | Reminders before the start (minutes) · max 5 items |
UpdateEventRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
startAt | string | date-time |
endAt | string | date-time |
description | string | null | max 5000 chars |
allDay | boolean | |
timezone | string | IANA time zone, e.g. Europe/Berlin · max 64 chars |
recurrenceRule | string | null | RFC 5545 RRULE · max 500 chars |
location | string | null | max 500 chars |
requiresRegistration | boolean | |
capacity | integer | null | 1–1000000 |
reminderMinutes | integer[] | Reminders before the start (minutes) · max 5 items |
roomId | string | Move the event (a recurring series as a whole) to another room of the same space – RSVPs, tickets and reminders stay. Needs events.manage in the old and the new room (own events: events.create in the new room), the Events module on there, not archived. Rooms only. Since 1.6.0 · max 64 chars |
File
| Field | Type | Description |
|---|---|---|
id · required | string | File id |
spaceId · required | string | Space id |
roomId · required | string | null | |
folderId · required | string | null | |
name · required | string | |
mimeType · required | string | |
size · required | integer | Bytes |
status · required | "PENDING" | "SCANNING" | "READY" | "INFECTED" | "DELETED" | Not READY while the malware scan runs |
uploadedById · required | string | null | |
createdAt · required | string | date-time |
updatedAt · required | string | date-time |
FileList
| Field | Type | Description |
|---|---|---|
folders · required | object[] | |
files · required | File[] |
UploadFileRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–255 chars |
mimeType · required | string | e.g. application/pdf · max 255 chars, pattern ^[\w.+-]+/[\w.+-]+$ |
size · required | integer | Exact size in bytes (the upload URL accepts at most this many) · min 0 |
folderId | string |
FileUpload
| Field | Type | Description |
|---|---|---|
file · required | File | |
uploadUrl · required | string | PUT the raw bytes here (no bot token), with headers · uri |
headers · required | object | |
expiresInSec · required | integer | |
completeUrl · required | string | POST here with the bot token after the upload · uri |
FileDownload
| Field | Type | Description |
|---|---|---|
url · required | string | Short-lived signed absolute URL (https://…, built from the public base URL of the instance) – no bot token needed · uri |
expiresInSec · required | integer |
Note
| Field | Type | Description |
|---|---|---|
id · required | string | Note id |
spaceId · required | string | Space id |
roomId · required | string | null | |
title · required | string | |
content · required | string | null | Text with light Markdown (# headings, - lists); null in lists |
createdById · required | string | null | |
updatedById | string | null | |
version | integer | Pass as baseVersion when replacing the text |
createdAt · required | string | date-time |
updatedAt · required | string | date-time |
CreateNoteRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 1–200 chars |
content | string | Text with light Markdown (# headings, - lists, paragraphs) · max 100000 chars |
UpdateNoteRequest
| Field | Type | Description |
|---|---|---|
title | string | 1–200 chars |
content | string | Replaces the whole text as a new version · max 100000 chars |
baseVersion | integer | Optimistic concurrency (409 NOTE_VERSION_CONFLICT); default: the current version · min 1 |
TimeoutRequest
| Field | Type | Description |
|---|---|---|
minutes · required | integer | 1 minute … 28 days · 1–40320 |
reason | string | max 1000 chars |
KickRequest
| Field | Type | Description |
|---|---|---|
reason | string | max 1000 chars |
BanRequest
| Field | Type | Description |
|---|---|---|
reason | string | max 1000 chars |
durationHours | integer | Temporary ban; omit for a permanent ban · 1–87600 |
deleteMessageHours | integer | Delete the member’s messages of the last hours · 0–168 |
DmConversation
| Field | Type | Description |
|---|---|---|
conversationId · required | string | Id of the conversation (as in dm.message_created) |
roomId · required | string | |
user · required | UserSummary | |
lastMessageAt · required | string | date-time |
canReply · required | boolean | The person wrote at least once and no block exists |
blocked · required | boolean |
DmConversationPage
| Field | Type | Description |
|---|---|---|
items · required | DmConversation[] | |
nextCursor · required | string | null | lastMessageAt of the last item – pass as before |
InteractionDmResult
| Field | Type | Description |
|---|---|---|
conversationId · required | string | |
message · required | Message |
VoiceJoinRequest
At least one of both must be true
| Field | Type | Description |
|---|---|---|
listen | boolean | Hear the room – needs VOICE_LISTEN |
publish | boolean | Speak (microphone only) – needs voice.speak |
VoiceJoinResult
| Field | Type | Description |
|---|---|---|
url · required | string | LiveKit server URL |
token · required | string | LiveKit access token for the bot identity |
roomName · required | string | |
identity · required | string | |
canPublish · required | boolean | |
canSubscribe · required | boolean | |
expiresAt · required | string | date-time |
VoiceParticipant
| Field | Type | Description |
|---|---|---|
userId · required | string | |
roomId · required | string | |
muted · required | boolean | |
deafened · required | boolean | |
speaking · required | boolean | |
video | boolean | |
screenShare | boolean | |
onStage | boolean | |
joinedAt · required | string | date-time |
bot | boolean | |
botListening | boolean |
VoiceBot
| Field | Type | Description |
|---|---|---|
roomId · required | string | |
applicationId · required | string | |
botUser · required | UserSummary | |
name · required | string | |
listening · required | boolean | |
publishing · required | boolean | |
joinedAt · required | string | date-time |
VoiceRoom
| Field | Type | Description |
|---|---|---|
roomId · required | string | |
participants · required | VoiceParticipant[] | |
self · required | VoiceBot | null |
MeetingItem
| Field | Type | Description |
|---|---|---|
meetingId · required | string | |
roomId · required | string | |
title · required | string | |
startedAt · required | string | null | date-time |
endedAt · required | string | null | date-time |
hasTranscript · required | boolean | |
hasSummary · required | boolean |
MeetingList
| Field | Type | Description |
|---|---|---|
items · required | MeetingItem[] | |
nextCursor · required | string | null | Pass as cursor |
TranscriptSegment
| Field | Type | Description |
|---|---|---|
speakerId · required | string | null | |
speakerName · required | string | null | |
startMs · required | integer | |
endMs · required | integer | |
text · required | string |
MeetingTranscript
| Field | Type | Description |
|---|---|---|
meetingId · required | string | |
roomId · required | string | |
title · required | string | |
endedAt · required | string | null | date-time |
segments · required | TranscriptSegment[] | |
summary · required | object | null |
TicketSlaPhase
| Field | Type | Description |
|---|---|---|
state · required | "none" | "running" | "dueSoon" | "overdue" | "paused" | "met" | "breached" | |
due · required | string | null | Effective due time · date-time |
TicketSla
| Field | Type | Description |
|---|---|---|
firstResponse · required | TicketSlaPhase | |
resolution · required | TicketSlaPhase | |
badge · required | "overdue" | "dueSoon" | null | The worse of both open clocks |
Ticket
| Field | Type | Description |
|---|---|---|
id · required | string | Ticket id |
spaceId · required | string | Space id |
roomId · required | string | Room id |
number · required | integer | Sequential per room (#42) |
title · required | string | |
body · required | string | null | Description (Markdown, <@userId> mentions) – null in lists |
status · required | "NEW" | "IN_PROGRESS" | "WAITING_FOR_REPLY" | "RESOLVED" | "CLOSED" | |
priority · required | "LOW" | "NORMAL" | "HIGH" | "URGENT" | |
categoryId · required | string | null | |
fields · required | object | Custom field values (field id → value) |
creatorId · required | string | Reporter – the member when a bot opened the ticket on behalf of someone |
createdByBotId · required | string | null | BOT account that opened the ticket on behalf of creatorId (shown as "created by <bot> on behalf of <member>") – null = opened by the creator |
assigneeId · required | string | null | |
assigneeRoleId · required | string | null | |
participantIds · required | string[] | |
createdAt · required | string | date-time |
updatedAt · required | string | date-time |
lastActivityAt · required | string | Internal notes count only with tickets.handle · date-time |
firstResponseAt · required | string | null | date-time |
resolvedAt · required | string | null | date-time |
closedAt · required | string | null | date-time |
mergedIntoId · required | string | null | |
sla · required | TicketSla | |
rating · required | integer | null | 1–5 stars – only for bots with tickets.view / handle / manage · 1–5 |
TicketMessage
| Field | Type | Description |
|---|---|---|
id · required | string | Ticket message id |
ticketId · required | string | |
kind · required | "PUBLIC" | "INTERNAL" | INTERNAL = note for the team – only for bots with tickets.handle |
authorId · required | string | |
author · required | TicketMessageAuthor | |
body · required | string | |
attachmentCount · required | integer | Number of attachments – list / download them via GET /bot/tickets/{ticketId}/attachments |
createdAt · required | string | date-time |
editedAt · required | string | null | date-time |
viaBotId · required | string | null | BOT account that posted the reply on behalf of its author (interaction proof or TICKETS_REPLY_ON_BEHALF) – shown as “via <bot>”; null = written by the author. Since 1.7.0 |
TicketMessageAuthor
Who wrote the message – id, space display name and BOT flag only (no username, avatar or e-mail). Since 1.6.0.
| Field | Type | Description |
|---|---|---|
id · required | string | = authorId |
displayName · required | string | Display name inside the space (space identity / nickname, else the profile name) |
isBot · required | boolean | BOT account (e.g. this bot) |
TicketRating
| Field | Type | Description |
|---|---|---|
stars · required | integer | 1–5 |
comment · required | string | null | |
createdAt · required | string | date-time |
updatedAt · required | string | date-time |
TicketDetail
| Field | Type | Description |
|---|---|---|
ticket · required | Ticket | |
messages · required | TicketMessage[] | Oldest first, at most 500 |
rating · required | TicketRating | null |
TicketPage
| Field | Type | Description |
|---|---|---|
items · required | Ticket[] | |
nextCursor · required | string | null | Pass as cursor for the next page |
TicketAttachment
file.status: only READY files can be downloaded (still scanning → 409 FILE_NOT_READY, INFECTED → 403 FILE_INFECTED).
| Field | Type | Description |
|---|---|---|
file · required | File | |
messageId · required | string | null | Reply / internal note the file belongs to – null = attachment of the ticket description |
createdAt · required | string | When the file was attached · date-time |
TicketAttachmentList
| Field | Type | Description |
|---|---|---|
items · required | TicketAttachment[] | Oldest first, at most 1000 |
TicketRatingPage
| Field | Type | Description |
|---|---|---|
items · required | TicketRating[] | |
nextCursor · required | string | null | Pass as cursor |
TicketCategory
| Field | Type | Description |
|---|---|---|
id · required | string | |
name · required | string | |
color · required | string | null | |
position · required | integer |
TicketField
| Field | Type | Description |
|---|---|---|
id · required | string | |
categoryId · required | string | null | null = applies to every category |
label · required | string | |
type · required | "TEXT" | "LONG_TEXT" | "NUMBER" | "SELECT" | "CHECKBOX" | "DATE" | "URL" | |
required · required | boolean | |
options · required | string[] | SELECT only |
position · required | integer |
TicketCannedResponse
| Field | Type | Description |
|---|---|---|
id · required | string | |
title · required | string | |
body · required | string | Placeholders {name} {ticket} {title} {agent} {room} are not filled in |
position · required | integer |
TicketStats
| Field | Type | Description |
|---|---|---|
total · required | integer | |
open · required | integer | |
byStatus · required | object | |
unassigned · required | integer | |
overdue · required | integer | |
ratingAverage · required | number | null | |
ratingCount · required | integer | |
avgFirstResponseMinutes · required | integer | null | Tickets of the last 90 days |
TicketChange
| Field | Type | Description |
|---|---|---|
field · required | string | status | priority | category | assignee | assigneeRole | title | participant | field:<fieldId> | merged | description (since 1.6.0; from / to null – the text is not repeated) | reporter (since 1.6.0; from = bot, to = member) |
from · required | string | null | |
to · required | string | null | |
label | string | null | Category name / field label at the time of the change |
CreateTicketRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 3–200 chars |
body | string | Description (Markdown, <@userId> mentions) · max 20000 chars |
categoryId | string | null | |
priority | "LOW" | "NORMAL" | "HIGH" | "URGENT" | Only with tickets.handle – otherwise NORMAL |
fields | object | Custom field values (field id → value); required fields of the category are enforced |
attachmentIds | string[] | Ticket uploads of the bot in this room (POST /bot/rooms/{roomId}/tickets/attachments), not attached yet – at most 10 · max 10 items |
interactionId | string | On behalf of the person of this interaction (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed |
onBehalfOfUserId | string | On behalf of this member – needs the approved data access TICKETS_CREATE_ON_BEHALF (or an interaction of the same person) |
UploadTicketAttachmentRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–255 chars |
mimeType · required | string | e.g. image/png · max 255 chars, pattern ^[\w.+-]+/[\w.+-]+$ |
size · required | integer | Exact size in bytes (the upload URL accepts at most this many) · min 0 |
UpdateTicketRequest
| Field | Type | Description |
|---|---|---|
title | string | 3–200 chars |
body | string | Description (Markdown, <@userId> mentions; may be empty) – tickets.handle, or the bot’s own ticket (opened as itself or on behalf of the creator). Since 1.6.0. · max 20000 chars |
status | "NEW" | "IN_PROGRESS" | "WAITING_FOR_REPLY" | "RESOLVED" | "CLOSED" | Workflow: tickets.handle any allowed transition; the bot’s own ticket: close / reopen within the window |
priority | "LOW" | "NORMAL" | "HIGH" | "URGENT" | |
categoryId | string | null | |
assigneeId | string | null | A person with tickets.handle in the room (never a bot) |
assigneeRoleId | string | null | Role of the space |
fields | object | Replaces the custom field values |
ReassignTicketReporterRequest
Name the member with one of both (both given must name the same person). Since 1.6.0.
| Field | Type | Description |
|---|---|---|
interactionId | string | The person of this interaction becomes the reporter (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed |
onBehalfOfUserId | string | This member becomes the reporter – needs the approved data access TICKETS_CREATE_ON_BEHALF (or an interaction of the same person) |
CreateTicketMessageRequest
| Field | Type | Description |
|---|---|---|
body · required | string | Markdown, <@userId> mentions · 1–20000 chars |
kind | "PUBLIC" | "INTERNAL" | Default PUBLIC; INTERNAL needs tickets.handle |
attachmentIds | string[] | Ticket uploads of the bot in the ticket’s room, not attached yet – at most 10 · max 10 items |
interactionId | string | Reply on behalf of the person of this interaction (interaction proof) – only together with the header X-Coviqo-Interaction-Token of the same interaction; no extra data access needed. Since 1.7.0 |
onBehalfOfUserId | string | Reply on behalf of this member – needs the approved data access TICKETS_REPLY_ON_BEHALF (or an interaction of the same person). Since 1.7.0 |
UpdateTicketMessageRequest
Since 1.7.0.
| Field | Type | Description |
|---|---|---|
body · required | string | The new text (Markdown, <@userId> mentions) – may be empty only while the message has attachments · max 20000 chars |
IncomingWebhookPayload
| Field | Type | Description |
|---|---|---|
content · required | string | 1–4000 chars |
username | string | Display name for this one message |
avatarUrl | string | https avatar for this one message (shown through the image proxy) · uri |
InteractionPayload
| Field | Type | Description |
|---|---|---|
id · required | string | Interaction id |
type · required | "PING" | "COMMAND" | "COMPONENT" | "MODAL_SUBMIT" | "AUTOCOMPLETE" | AUTOCOMPLETE (since 1.4.0): the person is typing an autocomplete option – answer AUTOCOMPLETE_RESULT |
applicationId · required | string | Application id |
token · required | string | Interaction token – header X-Coviqo-Interaction-Token of the follow-up endpoints, valid 15 minutes |
spaceId | string | |
roomId | string | |
user | object | The person who used the command / component |
memberRoleIds | string[] | Role ids of that person in the space |
command | object | COMMAND: name + checked options (types, choices and limits enforced). AUTOCOMPLETE: the options typed so far (only valid ones) + focused |
customId | string | COMPONENT / MODAL_SUBMIT |
values | string[] | COMPONENT: chosen values of a select menu |
fields | object | MODAL_SUBMIT: field customId → value |
files | object | MODAL_SUBMIT of a form with file upload fields: field customId → the uploaded files (every file field, [] when empty). Since 1.6.0 |
message | object | null | COMPONENT: the message with the component – null for ephemeral messages |
locale | string | UI language of the person |
createdAt · required | string | date-time |
ModalFile
One file of a submitted file upload field (malware scan passed). Since 1.6.0
| Field | Type | Description |
|---|---|---|
id · required | string | File id |
name · required | string | File name as uploaded |
size · required | integer | Bytes |
mimeType · required | string | Checked against the first bytes of the file |
url · required | string | Signed absolute download URL – fetch without the bot token, valid 10 minutes; a fresh one: GET /bot/interactions/{interactionId}/files/{fileId}/download · uri |
urlExpiresAt · required | string | date-time |
MessageData
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
ephemeral | boolean | Only the person who triggered the interaction sees it; never stored |
expiresAfterSeconds | integer | Ephemeral answers only: the message disappears from the person’s screen after this many seconds (3–3600) – also after a reload, its components stop working then. With DEFERRED_MESSAGE (+ ephemeral) it applies to the deferred answer. Edits keep the expiry; UPDATE_MESSAGE of an ephemeral message may set a new one. Since 1.7.0 · 3–3600 |
InteractionResponse
| Field | Type | Description |
|---|---|---|
type · required | "PONG" | "MESSAGE" | "DEFERRED_MESSAGE" | "UPDATE_MESSAGE" | "DEFERRED_UPDATE" | "MODAL" | "AUTOCOMPLETE_RESULT" | |
data | MessageData | Modal | AutocompleteResultData | MessageData for MESSAGE / UPDATE_MESSAGE (DEFERRED_MESSAGE: only ephemeral and – since 1.7.0 – expiresAfterSeconds), Modal for MODAL, AutocompleteResultData for AUTOCOMPLETE_RESULT (the only valid answer to AUTOCOMPLETE) |
EventEnvelope
| Field | Type | Description |
|---|---|---|
id · required | string | Idempotency id – identical for retries, redeliveries and webhook + gateway |
type · required | "message.created" | "message.updated" | "message.deleted" | "reaction.added" | "reaction.removed" | "member.joined" | "member.left" | "member.roles_updated" | "task.created" | "task.updated" | "task.completed" | "event.created" | "event.updated" | "event.canceled" | "event.rsvp_updated" | "file.uploaded" | "voice.joined" | "voice.left" | "voice.member_moved" | "presence.updated" | "interaction.created" | "dm.message_created" | "meeting.transcript_ready" | "ticket.created" | "ticket.updated" | "ticket.message.created" | "ticket.message.updated" | "ticket.message.deleted" | "ticket.rated" | "ticket.sla.breached" | "member.banned" | "member.unbanned" | "member.kicked" | "member.timeout" | "member.linked_account_updated" | "invite.created" | "invite.revoked" | "voice.state_updated" | "voice.moved_afk" | "team.application.created" | "team.application.decided" | "team.member.added" | "team.member.removed" | "forum.post_created" | "room.created" | "room.updated" | "room.deleted" | "room.permissions_updated" | "PING" | |
applicationId · required | string | |
spaceId · required | string | null | null for direct messages and PING |
installationId · required | string | null | |
createdAt · required | string | date-time |
data · required | object | Event-specific payload |
LinkedAccountEntry
| Field | Type | Description |
|---|---|---|
userId · required | string | User id |
memberId · required | string | Space member id |
linkedAccounts · required | LinkedAccount[] | Never empty |
LinkedAccountPage
| Field | Type | Description |
|---|---|---|
items · required | LinkedAccountEntry[] | |
nextCursor · required | string | null | Pass as cursor for the next page (a user id) |
Team
| Field | Type | Description |
|---|---|---|
id · required | string | Team id |
tenantId · required | string | Organisation id |
spaceId · required | string | null | null = organisation team (read-only for bots) |
scope · required | "SPACE" | "ORG" | |
parentId · required | string | null | Parent team id (teams form a tree; members of sub-teams count for every parent team) |
name · required | string | |
description | string | null | |
color | string | null | |
icon | string | null | |
joinPolicy · required | "OPEN" | "INVITE_ONLY" | "APPLICATION" | "PASSWORD" | OPEN: members join themselves, INVITE_ONLY: added by leads / managers, APPLICATION: people apply, PASSWORD: people join with a password (since 1.6.0) |
applicationPrompt | string | null | joinPolicy APPLICATION: question shown in the application dialog |
hasJoinPassword | boolean | joinPolicy PASSWORD only: a join password is set – the password itself is never returned. Since 1.6.0 |
mentionable · required | boolean | |
position · required | integer | |
memberCount · required | integer | Direct members |
leadUserIds · required | string[] |
TeamList
| Field | Type | Description |
|---|---|---|
items · required | Team[] | Space teams and the organisation teams usable in the space |
TeamMember
| Field | Type | Description |
|---|---|---|
teamId · required | string | Team id |
userId · required | string | User id |
user · required | UserSummary | |
displayName · required | string | |
avatarUrl | string | null | |
isLead · required | boolean | |
source · required | "MANUAL" | "IDP" | IDP: synced from the identity provider – cannot be removed manually |
addedAt · required | string | date-time |
TeamMemberList
| Field | Type | Description |
|---|---|---|
items · required | TeamMember[] | Leads first |
TeamApplicationPage
| Field | Type | Description |
|---|---|---|
items · required | TeamApplication[] | Newest first |
nextCursor · required | string | null | Pass as cursor for the next page |
TeamAnnouncement
| Field | Type | Description |
|---|---|---|
id · required | string | Announcement id |
teamId · required | string | Team id |
spaceId · required | string | Space id |
author · required | UserSummary | null | |
title · required | string | null | |
content · required | string | |
roomId · required | string | null | Room the announcement was also posted to as a message |
messageId · required | string | null | |
createdAt · required | string | date-time |
editedAt · required | string | null | date-time |
TeamAnnouncementPage
| Field | Type | Description |
|---|---|---|
items · required | TeamAnnouncement[] | Newest first |
nextCursor · required | string | null | Pass as cursor for the next page |
UpdateTeamRequest
| Field | Type | Description |
|---|---|---|
name | string | 1–60 chars |
joinPolicy | "OPEN" | "INVITE_ONLY" | "APPLICATION" | "PASSWORD" | OPEN is refused (409 TEAM_GRANTS_ABOVE_BOT) while the team or a parent team grants room permissions the bot does not hold |
applicationPrompt | string | null | Question of the application dialog (joinPolicy APPLICATION) · max 300 chars |
password | string | joinPolicy PASSWORD: the join password (6–128 characters, write-only – never returned). Required when switching to PASSWORD; send it alone to change the password of a PASSWORD team; refused (400 TEAM_JOIN_PASSWORD_NOT_APPLICABLE) with any other join policy. Switching away clears it. Since 1.6.0 · 6–128 chars, password |
CreateTeamApplicationRequest
| Field | Type | Description |
|---|---|---|
message · required | string | 1–1000 characters · 1–1000 chars |
onBehalfOfUserId | string | Apply for this member – needs the approved data access TEAM_APPLY_ON_BEHALF (or an interaction of the same person) |
interactionId | string | Interaction proof: apply for the person of this interaction – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
WithdrawTeamApplicationRequest
| Field | Type | Description |
|---|---|---|
interactionId | string | Interaction proof: an interaction of this bot by the applicant – only together with the header X-Coviqo-Interaction-Token; no extra data access needed |
PutTeamMemberRequest
| Field | Type | Description |
|---|---|---|
isLead | boolean | Make / unmake a team lead (omitted: unchanged for members, false for new ones) |
TeamApplicationDecisionRequest
| Field | Type | Description |
|---|---|---|
decision · required | "ACCEPTED" | "DECLINED" | |
note | string | Shown to the applicant (≤ 500 characters) · max 500 chars |
CreateTeamAnnouncementRequest
| Field | Type | Description |
|---|---|---|
title | string | null | ≤ 120 characters · max 120 chars |
content · required | string | 1–4000 characters · 1–4000 chars |
roomId | string | null | Also post it as a message into this room of the team’s space (the bot needs chat.send there) |
ForumTag
| Field | Type | Description |
|---|---|---|
id · required | string | Tag id |
name · required | string | 1–24 characters |
color · required | string | null | #rrggbb |
emoji · required | string | null | |
moderatorsOnly · required | boolean | Only with forum.manage (403 FORUM_TAG_RESTRICTED otherwise) |
position · required | integer |
Forum
| Field | Type | Description |
|---|---|---|
roomId · required | string | Room id |
tags · required | ForumTag[] | In display order |
requireTag · required | boolean | Every new post needs at least one tag (400 FORUM_TAG_REQUIRED) |
defaultSort · required | "activity" | "newest" | "reactions" | |
maxTagsPerPost · required | integer | |
canPost · required | boolean | chat.send, room not archived, bot not timed out |
canManage · required | boolean | forum.manage: moderator tags, solve / pin / lock any post, reply to locked posts |
ForumPost
| Field | Type | Description |
|---|---|---|
id · required | string | Post (= id of its first message – edit / delete the body with PATCH / DELETE /bot/messages/{messageId}) id |
roomId · required | string | Room id |
spaceId · required | string | Space id |
title · required | string | null | null: deleted post, or no MESSAGE_CONTENT (events; write answers about posts of others) |
excerpt · required | string | null | Plain text beginning of the first message (≈ 280 characters); null like title |
author · required | EventUser | |
tagIds · required | string[] | |
replyCount · required | integer | |
reactionCount · required | integer | Reactions on the first message |
solved · required | boolean | |
solvedAt · required | string | null | date-time |
acceptedAnswerId · required | string | null | Reply marked as the answer |
pinned · required | boolean | |
locked · required | boolean | Only forum.manage may still reply |
deleted · required | boolean | The first message was deleted – the post stays as placeholder while it has replies |
createdAt · required | string | date-time |
lastActivityAt · required | string | date-time |
editedAt · required | string | null | date-time |
ForumPostPage
| Field | Type | Description |
|---|---|---|
items · required | ForumPost[] | Pinned first, then by sort |
nextCursor · required | string | null | Pass as cursor for the next page |
ForumPostDetail
| Field | Type | Description |
|---|---|---|
post · required | ForumPost | |
message · required | EventMessage | |
acceptedAnswer · required | EventMessage | null |
CreateForumPostRequest
| Field | Type | Description |
|---|---|---|
title · required | string | 3–200 characters (whitespace is collapsed) · max 250 chars |
content | string | First message (optional – the title is the post). Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
tagIds | string[] | At most 5 tags of this forum · max 5 items |
components | Components | |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
ForumReplyRequest
At least content or components (400 MESSAGE_EMPTY otherwise).
| Field | Type | Description |
|---|---|---|
content | string | Markdown text, at most 4000 characters. Mentions: <@userId>, roles <@&roleId>, teams <@team:teamId>, @everyone only with chat.mention_everyone. · max 4000 chars |
components | Components | |
replyToId | string | Quote a message of this post (its first message or a reply) · max 64 chars |
nonce | string | Client nonce (idempotency for retries), at most 64 characters · max 64 chars |
UpdateForumPostRequest
Missing fields stay unchanged.
| Field | Type | Description |
|---|---|---|
title | string | 3–200 characters · max 250 chars |
tagIds | string[] | Replaces the tags (at most 5) · max 5 items |
ForumSolvedRequest
| Field | Type | Description |
|---|---|---|
solved · required | boolean | |
answerId | string | null | A reply of this post (400 FORUM_ANSWER_INVALID otherwise) · max 64 chars |
ForumFlagRequest
| Field | Type | Description |
|---|---|---|
value · required | boolean |
BotRoomDetail
| Field | Type | Description |
|---|---|---|
id · required | string | Room id |
spaceId · required | string | Space id |
categoryId · required | string | null | |
name · required | string | |
type · required | "STANDARD" | "VOICE" | "PROJECT" | "MEETING" | "EVENT" | "COMMUNITY" | "STAGE" | "LOBBY" | "FORUM" | "MAP" | |
visibility · required | "PUBLIC" | "PRIVATE" | "RESTRICTED" | "INVITE_ONLY" | |
description · required | string | null | |
tags · required | string[] | |
icon · required | string | null | |
color · required | string | null | Room colour token |
userLimit · required | integer | null | Voice user limit |
position · required | integer | |
modules · required | string[] | Enabled module keys |
archivedAt · required | string | null | date-time |
permissionsSynced · required | boolean | The room follows the permissions of its category |
createdAt · required | string | date-time |
RoomCategory
| Field | Type | Description |
|---|---|---|
id · required | string | Category id |
spaceId · required | string | Space id |
name · required | string | |
position · required | integer |
RoomOverride
| Field | Type | Description |
|---|---|---|
id · required | string | Override id |
roomId · required | string | Room id |
targetType · required | "ROLE" | "MEMBER" | "TEAM" | TEAM entries can be changed by bots since 1.6.0 (PUT / DELETE …/overrides/teams/{teamId}) |
roleId · required | string | null | |
userId · required | string | null | Member overrides: user id of the member |
teamId · required | string | null | |
allow · required | string[] | |
deny · required | string[] | Deny wins over allow |
CreateRoomRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–64 characters · 1–64 chars |
type · required | "STANDARD" | "VOICE" | "PROJECT" | "EVENT" | "COMMUNITY" | "STAGE" | "FORUM" | Not MEETING, LOBBY or MAP |
visibility | "PUBLIC" | "PRIVATE" | "RESTRICTED" | "INVITE_ONLY" | Default PUBLIC; PRIVATE / INVITE_ONLY add the bot as room member |
description | string | max 500 chars |
categoryId | string | Create inside this category – needs room.manage in the space; the room starts synced with the category’s permissions |
modules | "VOICE" | "CHAT" | "NOTES" | "FILES" | "TASKS" | "EVENTS" | "WHITEBOARD" | "FORUM" | "TICKETS" | "DECISIONS" | "MEETINGS"[] | Default: the modules of the room type. Not WEBPAGES / FEED (configured by people only) |
tags | string[] | max 10 items |
icon | string | max 64 chars |
color | "blue" | "cyan" | "green" | "yellow" | "orange" | "red" | "pink" | "purple" | |
userLimit | integer | Voice user limit · 1–500 |
UpdateRoomRequest
| Field | Type | Description |
|---|---|---|
name | string | 1–64 chars |
description | string | null | max 500 chars |
visibility | "PUBLIC" | "PRIVATE" | "RESTRICTED" | "INVITE_ONLY" | |
tags | string[] | max 10 items |
icon | string | null | max 64 chars |
color | "blue" | "cyan" | "green" | "yellow" | "orange" | "red" | "pink" | "purple" | |
userLimit | integer | null | 1–500 |
type | "STANDARD" | "VOICE" | "PROJECT" | "EVENT" | "COMMUNITY" | "STAGE" | "FORUM" | Lobby and map rooms keep their type |
categoryId | string | null | Move to this category (null = no category) – needs room.manage in the space |
permissions | "category" | "keep" | With a category move: category takes over the category’s permissions, keep keeps the room’s own (default rules when omitted) |
CategoryRequest
| Field | Type | Description |
|---|---|---|
name · required | string | 1–48 characters · 1–48 chars |
PutOverrideRequest
| Field | Type | Description |
|---|---|---|
allow · required | string[] | Room-scoped permissions (≤ 64) · max 64 items |
deny · required | string[] | Room-scoped permissions (≤ 64); deny wins over allow · max 64 items |
VoiceMoveRequest
| Field | Type | Description |
|---|---|---|
targetRoomId · required | string | Voice room to move the person to (same space) |
RoleAssignRequest
| Field | Type | Description |
|---|---|---|
reason | string | Audit reason (≤ 512 characters) · max 512 chars |
RolesPatchRequest
| Field | Type | Description |
|---|---|---|
add | string[] | Roles to add (≤ 25) · max 25 items |
remove | string[] | Roles to remove (≤ 25) · max 25 items |
reason | string | Audit reason (≤ 512 characters) · max 512 chars |
InviteList
| Field | Type | Description |
|---|---|---|
items · required | Invite[] | Newest first |
vanity · required | object | null | |
nextCursor · required | string | null | Pass as cursor for the next page (an invite id) |
Ban
BanPage
| Field | Type | Description |
|---|---|---|
items · required | Ban[] | Active bans ordered by user id |
nextCursor · required | string | null | Pass as cursor for the next page (a user id) |
ModerationAction
| Field | Type | Description |
|---|---|---|
id · required | string | Moderation action id |
type · required | "WARN" | "KICK" | "BAN" | "UNBAN" | "TIMEOUT" | "REMOVE_TIMEOUT" | "VOICE_MUTE" | "DELETE_MESSAGE" | |
userId · required | string | Target user id |
moderatorId · required | string | null | null = automatic (e.g. an expired ban) |
reason · required | string | null | |
durationMinutes · required | integer | null | TIMEOUT: minutes; BAN: until expiry; else null |
expiresAt · required | string | null | date-time |
createdAt · required | string | date-time |
ModerationActionPage
| Field | Type | Description |
|---|---|---|
items · required | ModerationAction[] | Newest first |
nextCursor · required | string | null | Pass as cursor for the next page |
VoiceStates
| Field | Type | Description |
|---|---|---|
afkRoomId · required | string | null | AFK room while the AFK detection is on |
items · required | object[] | Everybody in a voice room the bot can see |
VoiceActivityItem
| Field | Type | Description |
|---|---|---|
userId · required | string | |
sessions · required | integer | Sessions counted (overlapping the range; AFK-room sessions only with includeAfkRoom) |
totalSec · required | integer | Time in voice rooms within [from, to) – without the AFK room unless includeAfkRoom |
presentSec · required | integer | totalSec − deafenedSec |
activeSec · required | integer | totalSec − deafenedSec − mutedSec |
mutedSec · required | integer | Muted (self or server) but not deafened – the first mutedGraceMinutes of each muted interval do not count |
deafenedSec · required | integer | |
afkRoomSec · required | integer | Time in the AFK room (always reported separately) |
firstJoinedAt · required | string | null | Join time of the earliest counted session (may be before from) · date-time |
lastLeftAt · required | string | null | Leave time of the latest counted session; null while one is still open · date-time |
rooms · required | object[] | Per room, longest first |
VoiceActivity
| Field | Type | Description |
|---|---|---|
from · required | string | date-time |
to · required | string | date-time |
afkRoomId · required | string | null | |
mutedGraceMinutes · required | integer | |
items · required | VoiceActivityItem[] | One per person with a session in the range, ordered by userId |
nextCursor · required | string | null | Pass as cursor for the next page (a user id) |
VoiceSession
| Field | Type | Description |
|---|---|---|
id · required | string | Voice session id |
userId · required | string | |
roomId · required | string | |
joinedAt · required | string | date-time |
leftAt · required | string | null | null = still connected · date-time |
durationSec · required | integer | null | null while connected |
intervals · required | object[] | Muted / deafened intervals, oldest first |
VoiceSessionPage
| Field | Type | Description |
|---|---|---|
items · required | VoiceSession[] | Oldest first |
nextCursor · required | string | null | Pass as cursor for the next page (a session id) |
MemberStats
| Field | Type | Description |
|---|---|---|
from · required | string | First day (YYYY-MM-DD, UTC) · date |
to · required | string | Last day (YYYY-MM-DD, UTC) · date |
days · required | object[] | Only days with a snapshot (daily at 00:15 UTC from the deploy on – no backfill) |
Changelog & versioning
- The API is versioned in the path (
/api/v1). Within v1, changes are additive: new endpoints, new optional fields, new event types, new error codes. Ignore unknown fields and event types; do not rely on the order of JSON keys. - Breaking changes would come with a new path version and an announcement here; v1 then keeps working for a transition period.
- The OpenAPI document carries its own version (currently 1.7.0).
| Date | Change |
|---|---|
| 2026-10-06 | 1.7.0: ephemeral answers that disappear by themselves (expiresAfterSeconds, expiresAt, 410 BOT_EPHEMERAL_EXPIRED); ticket replies on behalf of the reporter or a participant (interaction proof or the new data access TICKETS_REPLY_ON_BEHALF, viaBotId on ticket messages, 403 TICKET_REPLIER_INVALID); edit and delete ticket messages (PATCH / DELETE /bot/tickets/:ticketId/messages/:messageId) with the new events ticket.message.updated and ticket.message.deleted; components in the message history (custom ids only on the bot’s own messages). |
| 2026-10-06 | 1.6.0: move calendar events to another room (PATCH /bot/events/:eventId with roomId, previousRoomId on event.updated); room permission entries for teams (PUT / DELETE /bot/rooms/:roomId/overrides/teams/:teamId); new events room.created, room.updated, room.deleted, room.permissions_updated (minimal accessLost payload for bots that lose the room); fromRoomId on voice.joined and nullable fromRoomId on voice.member_moved (null when the bot cannot see the origin room); isDefault on roles; own messages without MESSAGE_CONTENT (?author=self); team join policy PASSWORD with a write-only password; tickets: edit the description (PATCH /bot/tickets/:ticketId), message authors with displayName / isBot, hand over the bot’s own tickets to a member (POST /bot/tickets/:ticketId/reporter); file fields in forms (files in MODAL_SUBMIT), channelTypes for room options, description on autocomplete choices. Fix: attachment, upload and download URLs are always absolute URLs. |
| 2026-10-06 | 1.5.0: withdrawing an application a bot created on behalf of a member, without cooldown (POST /bot/team-applications/:id/withdraw); removing a member’s event answer (DELETE /bot/events/:eventId/rsvps/:userId, 409 EVENT_RSVP_TICKET_HELD for ticket holders); event.rsvp_updated carries the origin source (member / bot) and viaBotId, status is null after a removal; new field withdrawnByBotId on applications. |
| 2026-10-05 | 1.4.0: ticket attachments readable by bots – list and signed download URL (TICKETS_READ); team settings PATCH /bot/teams/:teamId (TEAMS_MANAGE); applying to teams and answering events on behalf of members (interaction proof or TEAM_APPLY_ON_BEHALF / EVENT_RSVP_ON_BEHALF, shown “via” the bot); new fields createdByBotId and viaBotId; slash command option limits minValue / maxValue / minLength / maxLength (enforced before delivery) and autocomplete – interaction AUTOCOMPLETE, response AUTOCOMPLETE_RESULT, POST /bot/interactions/:id/autocomplete for gateway bots. |
| 2026-10-05 | 1.3.0: forms over the gateway (POST /bot/interactions/:id/modal); rooms, categories, room permissions and voice move / disconnect (data access ROOMS_MANAGE); cancel / delete events and RSVP lists (EVENT_RSVPS) with the events event.updated, event.canceled, event.rsvp_updated; tickets on behalf of members (interaction proof or TICKETS_CREATE_ON_BEHALF) and ticket attachments; forum posts and replies with forum.post_created; threadRootId on message events. |
| 2026-10-05 | 1.2.1: team mentions <@team:teamId> documented for message content; new section “Finding IDs & endpoints” (developer mode in the app). |
| 2026-10-03 | 1.1.0: tickets – data access TICKETS_READ / TICKETS_WRITE, ticket endpoints and the events ticket.*. |
| 2026-10-02 | Public developer documentation and the OpenAPI 3.1 description (1.0.0) published. |