A thin Slack Web API client built on Req.
Slack Web API methods are POSTs to https://slack.com/api/<method> with a
bearer token. Note that Slack returns HTTP 200 even on logical failures — the
real status is in the "ok" field of the JSON body, which the helpers here
surface as {:error, reason}.
Genuine rate limiting is different: Slack answers with HTTP 429 and a
Retry-After header. call/3 retries those automatically, honouring the
header, so a burst backs off instead of dropping. Only 429 is retried —
messages aren't idempotent, so we never re-POST on a transport error or a
5xx.
Tokens:
apps.connections.openneeds an app-level token (xapp-…).- Everything else (e.g.
chat.postMessage) needs a bot token (xoxb-…).
Summary
Functions
Add an emoji reaction (name, no colons) to the message at channel/timestamp
via reactions.add. Needs the reactions:write scope.
Append markdown to a streaming message with chat.appendStream.
Who this token authenticates as, via auth.test.
Call any Web API method. Returns {:ok, body} when Slack replies ok: true,
otherwise {:error, reason} (the Slack error string, or a transport error).
Delete a message with chat.delete. Needs the chat:write scope.
Get a permalink to the message at channel/timestamp via chat.getPermalink.
Fetch one page of a channel's message history with conversations.history.
Join a public channel with conversations.join. Needs the channels:join scope.
Exchange an OAuth code for a workspace's tokens, via oauth.v2.access.
Open a Socket Mode WebSocket URL via apps.connections.open.
Open (or resume) a direct message with user via conversations.open.
Open a modal with views.open.
Post a message only user can see, with chat.postEphemeral.
Post a message with chat.postMessage.
Publish a Home tab view for user with views.publish.
Push a new modal onto the stack of an open modal with views.push.
Remove a reaction added with add_reaction/4, via reactions.remove.
Reply to a slash command or interaction via its response_url.
Schedule a message for later with chat.scheduleMessage.
Offer tappable prompts in an assistant thread, via
assistant.threads.setSuggestedPrompts.
Show a status line ("is thinking…") in an assistant thread, via
assistant.threads.setStatus. Pass "" to clear it.
Name an assistant thread, via assistant.threads.setTitle.
Start a streaming message with chat.startStream.
Finish a streaming message with chat.stopStream.
Lazily stream every page of a cursor-paginated Web API method.
Update an existing message with chat.update.
Replace the contents of an open modal with views.update.
Upload a file in one call, hiding Slack's three-step external upload flow
(files.getUploadURLExternal → upload the bytes →
files.completeUploadExternal).
Look up a user's profile with users.info. Needs the users:read scope.
Functions
Add an emoji reaction (name, no colons) to the message at channel/timestamp
via reactions.add. Needs the reactions:write scope.
Append markdown to a streaming message with chat.appendStream.
ts is the streamed message's timestamp from start_stream/4.
markdown_text is capped at 12,000 characters per call by Slack.
Who this token authenticates as, via auth.test.
The body's "user_id" is the bot's own user id (Slink.Identity caches this
to power context.bot_user_id and Slink.mentions_me?/1).
Call any Web API method. Returns {:ok, body} when Slack replies ok: true,
otherwise {:error, reason} (the Slack error string, or a transport error).
Delete a message with chat.delete. Needs the chat:write scope.
Get a permalink to the message at channel/timestamp via chat.getPermalink.
Returns {:ok, url}.
Fetch one page of a channel's message history with conversations.history.
opts is merged into the body — most usefully %{limit: ..., cursor: ...}
for paging (the next cursor is in body["response_metadata"]["next_cursor"]).
Needs the relevant *:history scope for the conversation type.
Join a public channel with conversations.join. Needs the channels:join scope.
Exchange an OAuth code for a workspace's tokens, via oauth.v2.access.
This is the round-trip behind an "Add to Slack" install (see Slink.OAuth).
Unlike other Web API methods it authenticates with the app's client
credentials and a form-encoded body — no bearer token. redirect_uri must
match the one used in the authorize URL, when one was used there.
Open a Socket Mode WebSocket URL via apps.connections.open.
Pass an app-level token (xapp-…) with the connections:write scope.
Open (or resume) a direct message with user via conversations.open.
Returns {:ok, channel_id} — the DM channel to post into (see
Slink.send_dm/4 for the one-call version). Needs the im:write scope.
Open a modal with views.open.
trigger_id comes from the interaction that opened it (see
Slink.Event.trigger_id/1) and is only valid for ~3 seconds, so open the
modal promptly. view is a Block Kit view payload (a map).
Post a message only user can see, with chat.postEphemeral.
Shows up for that one user in channel and vanishes on reload — good for
private acknowledgements. opts is merged into the body. Needs chat:write.
Post a message with chat.postMessage.
opts is merged into the request body (e.g. %{thread_ts: ..., blocks: ...}).
Publish a Home tab view for user with views.publish.
This is what populates a bot's App Home tab; call it from an
:app_home_opened handler. Needs the App Home tab enabled for the app.
Push a new modal onto the stack of an open modal with views.push.
Remove a reaction added with add_reaction/4, via reactions.remove.
Reply to a slash command or interaction via its response_url.
Slash commands and interactive payloads carry a short-lived response_url
(valid ~30 minutes, up to 5 uses) — see Slink.Event.response_url/1. Unlike
the Web API methods this is a plain POST to that URL with no bearer token.
params is the message body, e.g. %{response_type: "ephemeral", text: "…"}
("ephemeral" — only the invoker sees it — or "in_channel"), optionally with
blocks, replace_original, or delete_original.
Schedule a message for later with chat.scheduleMessage.
post_at is a Unix timestamp (seconds). opts is merged into the body
(e.g. %{thread_ts: ...}). Needs the chat:write scope.
Offer tappable prompts in an assistant thread, via
assistant.threads.setSuggestedPrompts.
prompts is a list of %{title: ..., message: ...} maps (the message is
what gets sent when tapped). opts merges into the body (e.g. %{title: "Try one of these"} for the section header).
Show a status line ("is thinking…") in an assistant thread, via
assistant.threads.setStatus. Pass "" to clear it.
Name an assistant thread, via assistant.threads.setTitle.
Start a streaming message with chat.startStream.
Streamed messages are always thread replies, so thread_ts is required.
Returns the started message in the body — its "ts" is what
append_stream/4 and stop_stream/4 take. When streaming into a channel
(not the app's DM), Slack requires recipient_user_id and
recipient_team_id in opts. See Slink.stream_reply/3 for the high-level
helper.
Finish a streaming message with chat.stopStream.
opts merges into the body — a final markdown_text: chunk, or blocks:
(allowed only here, not on start/append).
Lazily stream every page of a cursor-paginated Web API method.
Emits each page's whole body, fetching the next page on demand by following
response_metadata.next_cursor until Slack returns none. Extract the
method's list field to stream items:
Slink.API.stream(token, "conversations.history", %{channel: "C123"})
|> Stream.flat_map(& &1["messages"])
|> Enum.take(500)params gets limit: 200 (Slack's recommended page size) unless you pass
your own. Rate limiting is handled the same as call/3 — a 429 waits out
Retry-After and retries. A page fetch that still fails raises
Slink.API.Error (a lazy stream can't return an error tuple), so wrap
enumeration in a rescue if you need to handle it.
Update an existing message with chat.update.
timestamp is the ts of the message to edit. opts is merged into the body
(e.g. %{blocks: ...}). Needs the chat:write scope.
Replace the contents of an open modal with views.update.
view_id is the id of the view returned when it was opened.
Upload a file in one call, hiding Slack's three-step external upload flow
(files.getUploadURLExternal → upload the bytes →
files.completeUploadExternal).
content is the file's binary content. Options (:filename required):
:filename— e.g."report.csv".:channel— share the file into this channel; without it the file is uploaded private to the bot.:title— display title (defaults to the filename on Slack's side).:initial_comment— message text alongside the shared file.:thread_ts— share into a thread.:alt_text— image description for screen readers.:snippet_type— syntax type for text snippets (e.g."elixir").
Returns {:ok, body} from the completing call (body["files"] holds the
file objects, ids included), or {:error, reason} from whichever step
failed. Needs the files:write scope.
Look up a user's profile with users.info. Needs the users:read scope.