Events API transport: a Plug that receives Slack's HTTP event callbacks.
It verifies Slack's request signature, answers the url_verification
challenge, responds 200 immediately, and dispatches the event off-process
(so your handler can't blow Slack's ~3s response budget).
Mount it in a router:
forward "/slack/events", to: Slink.EventsApi.Plug,
init_opts: [
module: MyBot,
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")
]Or run it standalone with Bandit:
Bandit.start_link(
plug: {Slink.EventsApi.Plug,
module: MyBot,
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")},
port: 4000
)Options:
:module(required) — a module implementing theSlinkbehaviour.:signing_secret(required) — the app's Signing Secret, for request verification. A string, or a 0-arity function returning one (resolved per request — see Mounting in Phoenix).:bot_token— bot token (xoxb-…) passed to handlers for Web API calls. A string or a 0-arity function.
Mounting in Phoenix
Two pitfalls when mounting inside a Phoenix app:
forwardoptions are evaluated at compile time in production, soSystem.fetch_env!/1in the router would read the env var on the build machine (or fail there). Pass functions instead — they're resolved per request:forward "/slack/events", to: Slink.EventsApi.Plug, init_opts: [ module: MyBot, signing_secret: fn -> System.fetch_env!("SLACK_SIGNING_SECRET") end, bot_token: fn -> System.fetch_env!("SLACK_BOT_TOKEN") end ]The raw body must still be readable. Signature verification hashes the raw request body, but
Plug.Parsers(in every generated endpoint) consumes it before the router runs — mounted after it, every request 401s. Mount this plug inendpoint.exbeforeplug Plug.Parsers, or run it as a standalone listener (see above).
Slash commands & interactivity
Slack delivers those as application/x-www-form-urlencoded, not JSON; this
plug decodes both. Point the app's Request URL for Interactivity and
Slash Commands at this same endpoint. Slash commands and most interactions
are handled off-process like events, and your handler replies via the
response_url (see Slink.reply/3).
The one exception is view_submission (a modal submit): Slack expects a
response_action in the immediate reply. For that type the handler runs
synchronously and its {:ack, map} return is sent back as the response — so
keep it fast (Slack's window is ~3s). Returning anything else closes the modal.