Slack API sandbox
Point @slack/web-api (or curl) at https://slack.sandboxapis.dev via the client's slackApiUrl option (paths are /api/<method>, exactly like slack.com/api). Auth is a Bearer bot/user token (xoxb-…/xoxp-…; any value is accepted here). This is the org's team-messaging surface: channels, history, threads, reactions, and the workspace roster: the SAME story the git hosts and trackers tell, discussed. The #incident-bridge channel's burst brackets the real incident issue; #releases announcements cite real tags. Success and failure are BOTH HTTP 200 with an ok boolean, which is Slack's real error model.
Coverage badge
Full17/17 · 100%
The share of read rows with a final answer — served and verified, a deviation, retired upstream, or a reviewed empty/refusal. Computed in the coverage manifest, copied here.
slack.sandboxapis.devServed & verified
17 / 100%
Answering with real universe data, each response checked against Slack's published spec by the conformance suite on this build.
The full read API
17
Every read surface Slack publishes, deferred long tail included, minus the rows excluded by policy (6). Writes are out of scope: this universe is read-only.
Not served yet
0
Nothing — every read surface Slack publishes is answered.
01 / The swap
No SDK of ours, no shim, no recorded fixtures. The same client library you already use, one environment variable different.
curl -H 'Authorization: Bearer xoxb-anything' "https://slack.sandboxapis.dev/api/conversations.list"Verified drop-in clients
@slack/web-api7.19.0The versions the conformance suite drives against this host on every build — pinned in coverage/client-pins.yaml, and watched weekly for upstream releases, because a client library moving without us is how a shipped integration breaks silently.
This spec is hand-authored, and that is a weaker guarantee.
Slack publishes no machine-readable API description, so the spec we conform to is written from their documentation plus live captures (retrieved 2026-08-25) and re-diffed against those pages on a schedule. Where a vendored spec makes conformance a mechanical check, this one is only as good as our reading of the docs — which is why it is stated here rather than buried in the manifest.
Spec of record: docs.slack.dev method docs (no living machine-readable spec — hand-authored shape-of-record) + live captures 2026-08-25
02 / Coverage by family
All 23 rows the coverage manifest carries for Slack, deferred long tail included and nothing capped. Open a family, or filter by path to find the exact endpoint your client calls.
Status — what a conformance test found
Mode — what kind of answer a row gets
A row with no mode shown has not been judged yet. Modes are the manifest's own words, from coverage/MODES.yaml; every empty and refuse carries a written reason and a review date before it counts as final.
Badge — where Full starts
REST + GraphQL + git rows that are not `excluded`. Write operations are NOT rows (DECISIONS 2026-09-01 decision 9): they are counted in meta.write_operations and never enter this ratio.
23 read surfaces in 17 families
SERVED 2026-09-14 (hello-20 `providerTails`, founder ruling F13): reader.channelBookmarks(channel) over `channel_bookmark`, in `ord` order - the bar's own order, and the accessor is SCOPED BY CHANNEL because that is the only way Slack addresses a bookmark, so there is no bare list to forget a filter on. SIX ROWS AT BOTH WINDOWS, by the generator's three rules: the releases channel pins the FIRST release that shipped after the channel existed, the incidents channel pins the status page (dated to whichever of the two came later), and each of the four team channels pins the repository its team works in; the org-wide channel pins NOTHING and answers `{ok:true, bookmarks: []}`, because a bookmark is something somebody did and nothing in this canon says anybody did it there. THE FIELD THIS ROW EXISTS TO GET RIGHT IS `link`. Slack's bookmark carries a URL, which is the class of value invariant #4 forbids inventing, so canon stores WHICH ROW is pinned (target_kind + target) and the renderer composes the link: a repository and a release resolve on the GitHub replica at exactly the html_url renderer-github prints (the renderer-circleci VCS_ORIGIN precedent), and a status page carries its own published URL in canon - nothing is composed for that one. Conformance follows every link back to the row target_kind names and exercises all three shapes. GATED on hasProviderTails(): every generation from hello-11 to hello-19 - which is the slack-2026-08 pin recipe, the 127 pins frozen before hello-20 and what live rolls as this lands - answers the NAMED gap universe_predates_bookmarks inside Slack's ok:false envelope at HTTP 200, never an empty bar, because an empty bar would claim nobody in this workspace ever pinned anything and would go on claiming it after the canon that contradicts it shipped. The CHANNEL is resolved first, so a channel that does not exist is channel_not_found on every generation - the generation is the second question this method asks. THE ARGUMENT IS `channel_id` AND NOT `channel`, which is the one place this method differs from every other channel-scoped read on this host; the doc calls it optional and then says it is required for a public channel, and every channel here is public. NO PAGINATION and no response_metadata: the method declares neither and the doc's own Usage info caps a conversation at 100 bookmarks. emoji, icon_url, entity_id, shortcut_id and app_id are null and date_updated is 0 - each the doc sample's own value for a link bookmark nobody decorated and nobody has edited, not a placeholder; `rank` is the position rendered so that sorting the served ranks reproduces the served order, a recorded divergence in the class ids.ts already records for x-slack-req-id and a match's iid. The spec-of-record's LAST `Deferred.` stub is gone with it: BookmarksListResponse + Bookmark are the fifteen fields of the method doc's own success sample, vendored and sha-pinned the same day (specs/slack/docs/bookmarks.list.md)
scratch — ACCEPTED into the per-key 24-hour overlay for a scratch-enabled key: Slack's own success envelope `{ok, channel, ts, message}` at HTTP 200, validated against the spec-of-record (ChatPostMessageResponse, derived from the method doc vendored 2026-10-04) by packages/conformance/src/slack/scratch.test.ts, and the SAME `message` object answers the `conversations.history` or `conversations.replies` that follows it because the write renders through the read path. `channel` is an encoded id or a channel name and must name a channel that EXISTS in this data set - Slack scratch creates no channels, the line GitHub draws at pull requests and Jira at issue keys. `thread_ts` makes the post a reply: the parent gains the reply in its `reply_count`, `reply_users` and `latest_reply` for this key, and a reply's own ts is re-parented to the root as Slack does. The message is MERGED into the channel model before `oldest`/`latest`/`inclusive` and before the cursor pager, so `has_more` and `next_cursor` stay right. The `ts` is minted by the same derivation every artifact message's is - the write's own second plus the channel's deterministic six-digit suffix with that second's taken suffixes excluded - so it sorts after every artifact message and can never collide with one. The AUTHOR is an existing canonical persona chosen deterministically per (key, channel) from the CHANNEL'S MEMBERS, so `message.user` resolves at `users.info` and appears in `conversations.members`; the response says who you were rendered as in `X-SandboxAPIs-Scratch-As`. `blocks` are accepted, stored verbatim and echoed back on the message the way Slack echoes them; `text` is required unless blocks are sent (Slack's own rule), and `no_text` otherwise. An unknown channel is `channel_not_found`, a malformed `thread_ts` is `invalid_thread_ts`, one naming no message in the channel is `thread_not_found`, malformed blocks are `invalid_blocks_format`/`invalid_blocks`; text over 40,000 characters is Slack's own `msg_too_long`; past the key's row cap or the per-channel cap is the dialect's one real HTTP status, the 429 `ratelimited` envelope with a `Retry-After` set to when the oldest row expires. `search.messages` does NOT merge in this phase and says so with `X-SandboxAPIs-Scratch: unmerged`. Without a scratch-enabled key this method is the read_only_universe envelope, byte for byte.
this workspace has no CUSTOM emoji — the canon's reaction emoji are the standard set, which real Slack never lists here (DECISIONS 2026-08-25 [P34/slack] item 6)
no message in this universe carries a file — `message` has no attachment column and the fleet's only file entity, `attachment`, hangs off the support desk's ticket comments and macros and - from hello-18's attachmentParents - issues, issue comments, merge requests, alerts and the namespace, never off a chat message (clause re-worded at the hello-18 cutover 2026-09-12: the canon grew, the conclusion did not move); Slack's own answer for a workspace with no files is {ok:true, files:[], paging:{...}} at 200. RULED by the founder 2026-09-12 (round 19), coverage wave W5. It is already the third statement of one fact on this host: reactions.get refuses a `file` argument with file_not_found, and files.upload is a write
no message in this workspace is pinned, on any channel — which is what keeps conversations.history's pin_count: 0 coherent (DECISIONS 2026-08-25 [P34/slack] item 6)
SERVED (wave W5) by a BOUNDED message search over the messaging canon — the soql.ts/sosl.ts contract in a third grammar: every modifier outside the subset is refused BY NAME with the coverage headers inside Slack's ok:false envelope, never ignored, because a search that quietly drops `has:link` returns MORE matches than the caller asked for and looks exactly like a correct answer. COVERED: bare terms (all must match, on whole-word tokens), quoted phrases, `in:` by name or id, `from:` by login, id or <@U…>, count/page with the doc's own 100 ceilings, sort=timestamp and sort_dir. REFUSED BY NAME: every other modifier, `-negation`, explicit AND/OR, wildcards, `highlight=true` and cursormark pagination. `sort=score` is NOT refused even though this replica computes no relevance — it is the default, refusing it would refuse every naive call, and the SET and the counts are exact while Slack's match objects carry no score field for us to invent a value for; the order served is the documented newest-first. One narrowing is stated rather than hidden: this replica matches message TEXT only, where the doc says users and channels also match at a lower priority — narrower than the real API, never wider. Matches come from BOTH halves of a channel's history, top-level and thread replies, because this canon puts the detail in the threads
03 / What's simulated
Every Slack call resolves against the same simulated data set every other provider serves. Counted from the slack-2026-08-g12 artifact (universe generation 12):
Each line below is one assertion in the conformance suite named beside it — run on every build, over one artifact, through the real renderers. Nothing is claimed here that no expect checks.
Every message body, every whole-second timestamp, the whole top-level/reply partition, the per-message reaction sets and the user directory are the same on this host and on Microsoft Graph, each dialect's own documented ordering normalised away first, and the residue asserted identical over full cursor walks, not samples. The channel roster served here is the canonical roster exactly; the Graph team holding that channel contains every one of them.
Against Microsoft Teams · parity/messaging-parity.test.ts
Every note PagerDuty prints on an incident is a message this host serves in that incident's channel, byte for byte.
Against PagerDuty · parity/incident-parity.test.ts
Hop 1 of the four-hop walk: the incidents channel, its thread opener and every reply are served here, and the burst's timestamps ascend and fall between the Jira issue's creation and its resolution, the same window the GitHub merge lands in.
Against Jira, Linear, GitHub, GitLab · parity/pinned-cross-category.test.ts
A Google Antigravity trigger's `time_zone` is the IANA zone this host's user profile gives the person who owns that trigger.
Against Google Antigravity · parity/agent-run-parity.test.ts
05 / Pinned snapshots
Each pin regenerates byte-identically on every request, so a test written against one never drifts. Generations are DIFFERENT universes, not versions of one — never swap a suffix expecting the same data.
slack-2026-08.snap.sandboxapis.devgeneration 6unversionedservedslack-2026-08-g10.snap.sandboxapis.devgeneration 10unversionedservedslack-2026-08-g11.snap.sandboxapis.devgeneration 11unversionedservedslack-2026-08-g12.snap.sandboxapis.devgeneration 12unversionedservedslack-2026-08-g8.snap.sandboxapis.devgeneration 8unversionedservedslack-2026-08-g9.snap.sandboxapis.devgeneration 9unversionedservedNeed an endpoint that is not served yet?
Every row Slack's manifest carries is on this page, so “not here” is an answer rather than a gap in the rendering. Tell us which path and which client, and it moves up the queue — the order is set by what people ask for.