# slack-web-api
Typed async Rust client for **every Slack Web API method** — all 330 of them, including `admin.*` — built on reqwest and rustls.
- No OpenSSL: reqwest 0.13 + rustls, HTTP/2 and a shared connection pool
- Request and response types for every method, plus typed Block Kit
- Automatic retry on 429 using `Retry-After` (3 times by default)
- Cursor pagination and file upload (the replacement for the retired `files.upload`) built in
It covers the Web API only. For Socket Mode or receiving Events API payloads, use another crate.
## Installation
```toml
[dependencies]
slack-web-api = "0.2"
```
## Usage
```rust
use slack_web_api::api::ChatPostMessageRequest;
use slack_web_api::blocks::{Block, SectionBlock, TextObject};
use slack_web_api::SlackClient;
#[tokio::main]
async fn main() -> Result<(), slack_web_api::SlackError> {
// Create one client per application and clone it: clones share the connection pool.
let client = SlackClient::new(std::env::var("SLACK_TOKEN").unwrap());
let res = client
.chat_post_message(
&ChatPostMessageRequest::new("C0123456789")
.text("New application received")
.blocks(vec![Block::from(SectionBlock::new().text(TextObject::mrkdwn("*New application received*")))]),
)
.await?;
println!("{:?}", res.ts);
Ok(())
}
```
Method `chat.postMessage` becomes `chat_post_message`, with types `ChatPostMessageRequest` / `ChatPostMessageResponse`.
Required arguments go to `new(...)`; optional ones are set with methods of the same name.
### Sharing your reqwest::Client
```rust
let client = slack_web_api::SlackClient::builder()
.http_client(app_http_client.clone())
.token("xoxb-...")
.build();
```
For apps installed in many workspaces, `client.with_token("xoxb-other")` switches the token while keeping the pool.
### Pagination
```rust
use slack_web_api::api::ConversationsListRequest;
let mut pages = client.pages(ConversationsListRequest::new().limit(200));
while let Some(page) = pages.next_page().await {
for channel in page?.channels {
println!("{:?}", channel.name);
}
}
```
### Uploading files
`files.upload` stopped working on 2025-11-12. `upload_files` runs `files.getUploadURLExternal`, the upload and
`files.completeUploadExternal` for you, uploading several files concurrently.
```rust
use slack_web_api::{FileUpload, UploadDestination};
client
.upload_files(
vec![FileUpload::new("report.csv", bytes)],
UploadDestination::channel("C0123456789").initial_comment("Monthly report"),
)
.await?;
```
### Untyped calls
New methods or arguments that are not in the generated types yet can be called with `call_raw`:
```rust
let value = client.call_raw("chat.postMessage", &serde_json::json!({"channel": "C1", "text": "hi"})).await?;
```
### Errors
`SlackError::Api` means Slack answered `"ok": false`; `err.api_error()` returns the code such as `channel_not_found`.
Details for errors like `invalid_blocks` are in `response_metadata.messages`.
## Supported APIs
Every method listed at <https://docs.slack.dev/reference/methods> (330 methods) except `files.upload`,
which Slack retired; use `upload_files` instead. The per-method list with Rust function names is in the
[`api` module docs](https://docs.rs/slack-web-api/latest/slack_web_api/api/) and in [`llms-full.txt`](llms-full.txt).
| `agents.*` | 2 | `agents.sessions.rename`, `agents.sessions.setStatus` |
| `api.*` | 1 | `api.test` |
| `apps.*` | 23 | `apps.activities.list`, `apps.auth.external.delete`, `apps.auth.external.get`, ... |
| `assistant.*` | 5 | `assistant.search.context`, `assistant.search.info`, `assistant.threads.setStatus`, ... |
| `auth.*` | 3 | `auth.revoke`, `auth.teams.list`, `auth.test` |
| `blocks.*` | 1 | `blocks.validate` |
| `bookmarks.*` | 4 | `bookmarks.add`, `bookmarks.edit`, `bookmarks.list`, ... |
| `bots.*` | 1 | `bots.info` |
| `calls.*` | 6 | `calls.add`, `calls.end`, `calls.info`, ... |
| `canvases.*` | 7 | `canvases.access.delete`, `canvases.access.set`, `canvases.create`, ... |
| `chat.*` | 13 | `chat.appendStream`, `chat.delete`, `chat.deleteScheduledMessage`, ... |
| `conversations.*` | 28 | `conversations.acceptSharedInvite`, `conversations.approveSharedInvite`, `conversations.archive`, ... |
| `dialog.*` | 1 | `dialog.open` |
| `dnd.*` | 5 | `dnd.endDnd`, `dnd.endSnooze`, `dnd.info`, ... |
| `emoji.*` | 1 | `emoji.list` |
| `entity.*` | 3 | `entity.acknowledgeCommentAction`, `entity.presentComments`, `entity.presentDetails` |
| `files.*` | 14 | `files.comments.delete`, `files.completeUploadExternal`, `files.delete`, ... |
| `functions.*` | 8 | `functions.completeError`, `functions.completeSuccess`, `functions.distributions.permissions.add`, ... |
| `migration.*` | 1 | `migration.exchange` |
| `oauth.*` | 6 | `oauth.access`, `oauth.v2.access`, `oauth.v2.beginShortTokenRotation`, ... |
| `openid.*` | 2 | `openid.connect.token`, `openid.connect.userInfo` |
| `pins.*` | 3 | `pins.add`, `pins.list`, `pins.remove` |
| `reactions.*` | 4 | `reactions.add`, `reactions.get`, `reactions.list`, ... |
| `reminders.*` | 5 | `reminders.add`, `reminders.complete`, `reminders.delete`, ... |
| `rtm.*` | 2 | `rtm.connect`, `rtm.start` |
| `search.*` | 3 | `search.all`, `search.files`, `search.messages` |
| `slackLists.*` | 12 | `slackLists.access.delete`, `slackLists.access.set`, `slackLists.create`, ... |
| `stars.*` | 3 | `stars.add`, `stars.list`, `stars.remove` |
| `team.*` | 9 | `team.accessLogs`, `team.billableInfo`, `team.billing.info`, ... |
| `tooling.*` | 1 | `tooling.tokens.rotate` |
| `usergroups.*` | 7 | `usergroups.create`, `usergroups.disable`, `usergroups.enable`, ... |
| `users.*` | 13 | `users.conversations`, `users.deletePhoto`, `users.discoverableContacts.lookup`, ... |
| `views.*` | 4 | `views.open`, `views.publish`, `views.push`, ... |
| `workflows.*` | 8 | `workflows.featured.add`, `workflows.featured.list`, `workflows.featured.remove`, ... |
| `admin.*` | 121 | `admin.analytics.getFile`, `admin.analytics.messages.activity`, `admin.analytics.messages.metadata`, ... |
Block Kit: every block, block element, rich text element and composition object, plus legacy attachments,
views (modal / home) and message metadata, in `slack_web_api::blocks`.
## How the response types are built
Response types are inferred from the documented response examples of all methods, and the core objects
(Message, Conversation, User, File, ...) are shared across methods. Every field is optional (`Option` or an empty `Vec`),
and scalar fields are read leniently: Slack sometimes returns the same field as a string in one place and a number in
another, and that must not fail the whole response. Unknown fields are skipped; use `call_raw` to see everything.
A test calls every method against a mock server returning its documented examples and checks no value is lost.
## Regenerating the types
The types are generated in `codegen/` from the Markdown version of the official docs (docs.slack.dev).
```sh
# Save each .md listed in docs.slack.dev/reference/methods.md into <dir>/m and the object pages into <dir>/obj, then:
python3 codegen/parse_docs.py methods <dir>/m > codegen/spec/methods.json
python3 codegen/parse_docs.py objects <dir>/obj > codegen/spec/objects.json
python3 codegen/generate.py && cargo fmt
```
## License
MIT