Conventions
- Requests and responses are JSON. Field names are snake_case (
display_name,thread_id). - Almost every resource lives under its workspace:
/v1/workspaces/:workspaceId/…. IDs are UUIDs. - Responses use the envelope
{ "ok": true, "data": { … } }; errors{ "ok": false, "error": { "code", "message", "status" } }. - Lists are cursor-paginated with
cursor+limit. - The machine-readable OpenAPI 3.0 spec is at api.disqua.com/api-docs (JSON:
/api-docs/json). Not every route has request/response schemas yet.
Authentication
Send a bearer token on every request: Authorization: Bearer <token>. Disqua accepts three kinds of token:
| Token | How you get it | Use for |
|---|---|---|
| JWT access token | POST /v1/auth/login → access_token (valid 15 minutes) + refresh_token (7 days) | Your own scripts acting as you. |
| dq_usr_… → dq_ai_… | Create a master token in the console under Profile → AI access, then exchange it with POST /v1/ai/request-session-key for a session key scoped to one workspace. | AI agents and automation (Claude Code, custom bots). The master token itself is only accepted by the exchange endpoint. |
| dq_oat_… | OAuth 2.1 authorization code flow with PKCE. Discovery: https://api.disqua.com/.well-known/oauth-authorization-server; token endpoint POST /v1/oauth/token; dynamic client registration POST /v1/oauth/register. | Third-party apps and MCP clients (Claude, ChatGPT). Scopes such as channels:read, messages:read, helpdesk:write. |
There are no separate per-workspace API keys yet. For server-to-server posting into a channel, use an incoming webhook.
/v1/auth/register
Create an account. Sends a verification e-mail.
{
"display_name": "Jane Doe", // 2–100 chars
"email": "jane@example.com",
"username": "janedoe", // 3–50 chars, lowercase letters, digits, _
"password": "s3cretpass", // 8–128 chars, at least one letter and one digit
"locale": "en" // optional
}
// 201 → { "ok": true, "data": { "user": { … }, "access_token": "…", "refresh_token": "…" } }/v1/auth/login
E-mail + password. Accounts with two-factor authentication get { "requires_2fa": true, "temp_token": "…" } instead and finish with POST /v1/auth/2fa/challenge (temp_token, code).
{ "email": "jane@example.com", "password": "…" }
// 200 → { "ok": true, "data": { "user": { … }, "workspace": { … }, "access_token": "eyJ…", "refresh_token": "…" } }/v1/auth/refresh
Exchange a refresh token for a new pair. Body { "refresh_token": "…" } (browsers send the cookie instead). Refresh tokens rotate on every use.
/v1/auth/logout
Auth required
End the current session.
Workspaces
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces | Workspaces you belong to. Optional ?slug= (exact) or ?q= (substring) filter. |
| POST | /v1/workspaces | Create a workspace. Body: name, slug. |
| GET | /v1/workspaces/:workspaceId | Workspace detail. |
| PATCH | /v1/workspaces/:workspaceId | Update name, description, icon (admin). |
| GET | /v1/workspaces/:workspaceId/members | List members. |
| PATCH | /v1/workspaces/:workspaceId/members/:userId | Change a member role (admin). |
| DELETE | /v1/workspaces/:workspaceId/members/:userId | Remove a member (admin). |
| POST | /v1/workspaces/:workspaceId/members/grant | Offer an existing Disqua user access. Body: user_id, role (admin|member). The user accepts it in the console. |
| GET | /v1/workspaces/:workspaceId/invites | List invites (admin). |
| POST | /v1/workspaces/:workspaceId/invites | Create an invite (admin). Sends an e-mail when an address is given. |
Channels
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:workspaceId/channels | Channels visible to you. |
| POST | /v1/workspaces/:workspaceId/channels | Create a channel. Body: name, type (public|private), description, initial_member_ids. |
| POST | /v1/workspaces/:workspaceId/channels/dm | Open (or reuse) a 1:1 direct message. Body: user_id. |
| GET | /v1/workspaces/:workspaceId/channels/:channelId | Channel detail. |
| PATCH | /v1/workspaces/:workspaceId/channels/:channelId | Update name, topic, description. |
| DELETE | /v1/workspaces/:workspaceId/channels/:channelId | Archive the channel. |
| GET | /v1/workspaces/:workspaceId/channels/:channelId/members | Channel members. |
| POST | /v1/workspaces/:workspaceId/channels/:channelId/members | Join, or add a user. Body: user_id (omit to join yourself). |
| DELETE | /v1/workspaces/:workspaceId/channels/:channelId/members/:userId | Remove a member / leave. |
| GET | /v1/workspaces/:workspaceId/channels/:channelId/pins | Pinned messages. |
| POST | /v1/workspaces/:workspaceId/channels/:channelId/pins | Pin a message. Body: message_id. |
| DELETE | /v1/workspaces/:workspaceId/channels/:channelId/pins/:messageId | Unpin. |
Messages
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:workspaceId/channels/:channelId/messages | Newest first. Query: limit (max 100), cursor (created_at of the oldest message you have). Opening the channel this way marks it read for you; pass mark_read=false to only read. |
| POST | /v1/workspaces/:workspaceId/channels/:channelId/messages | Send. Body: content (markdown, max 40,000 chars), thread_id, attachment_ids, scheduled_at. |
| GET | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId | One message. |
| PATCH | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId | Edit content (author only). |
| DELETE | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId | Delete (author or admin). |
| POST | /v1/workspaces/:workspaceId/channels/:channelId/mark-unread | Mark the channel unread from a given message. Body: message_id. |
Message object (abridged)
{
"id": "8b1d…",
"channel_id": "7a0c…",
"user_id": "7f9e…",
"content": "Hello **world**!",
"content_html": "<p>Hello <strong>world</strong>!</p>",
"type": "text", // text | system | file | ai
"thread_id": null,
"reply_count": 3,
"reactions": [ … ],
"attachments": [ … ],
"edited_at": null,
"created_at": "2026-10-16 09:45:00.000"
}
Threads
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId/thread | Replies to a parent message. |
| POST | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId/thread | Reply in the thread. Body: content. |
| GET | /v1/workspaces/:workspaceId/threads | Threads you take part in. |
Reactions
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId/reactions | Add a reaction. Body: emoji. |
| DELETE | /v1/workspaces/:workspaceId/channels/:channelId/messages/:messageId/reactions/:emoji | Remove your reaction. |
Users
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/users/me | Your profile. |
| PUT | /v1/users/me | Update display name, avatar, timezone, locale. |
| PUT | /v1/users/me/status | Set presence. Body: status (online|away|dnd|offline). |
| GET | /v1/users/me/preferences | Notification and theme preferences. |
| PUT | /v1/users/me/preferences | Update preferences. |
Files
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/workspaces/:workspaceId/files | Upload one file as multipart/form-data (field: file). Size limit by plan: 10 MB Free, 50 MB Pro, 100 MB Business and Suite. |
| GET | /v1/workspaces/:workspaceId/files | Files in the workspace. |
| DELETE | /v1/files/:fileId | Delete (uploader or admin). |
Search
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:workspaceId/search | Full-text search. Query: q plus filters. Free plan searches the most recent 10,000 messages. |
Notifications
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/workspaces/:workspaceId/notifications | Your notifications in a workspace. |
| GET | /v1/workspaces/:workspaceId/notifications/unread-count | Unread count. |
| PUT | /v1/notifications/:notificationId/read | Mark one read. |
| PUT | /v1/workspaces/:workspaceId/notifications/read-all | Mark all read. |
Errors & rate limits
Error codes are stable strings prefixed with E_, for example E_AUTH_TOKEN_EXPIRED (401), E_AUTHZ_NOT_MEMBER (403), E_CHANNEL_NOT_FOUND (404), E_VALIDATION_FAILED (400/422), E_PLAN_FEATURE_NOT_AVAILABLE (402). Branch on error.code, not on the message text.
Default rate limits: 600 requests per minute per authenticated user, 1,200 per minute per IP address for unauthenticated calls. Some endpoints (sending messages, recharges, exports, AI) have lower limits of their own. Over the limit you get 429 — wait and retry with backoff.