Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 9 min read Last reviewed Jul 2026

Slack Error Guide: 'invalid_cursor' — Fix Broken Pagination

Quick answer

Fix the Slack API invalid_cursor error: pass the exact next_cursor from response_metadata, URL-encode it, and keep query params identical across pages.

  • #slack
  • #api
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Slack error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

The invalid_cursor error means you called a paginated Slack method (like conversations.list, conversations.history, users.list, or conversations.members) with a cursor value that Slack does not recognize. Cursor-based pagination in Slack works by returning an opaque next_cursor string inside response_metadata; you must pass that exact string back, unmodified, to fetch the next page. Any mutation — truncation, double URL-encoding, stripping the trailing =, or reusing a cursor from a different query — produces invalid_cursor.

Slack returns HTTP 200 with ok:false and this body:

{
    "ok": false,
    "error": "invalid_cursor"
}

It occurs while iterating pages: the first call succeeds and returns a cursor, but the follow-up call carrying that cursor is rejected.

Symptoms

  • The first page of results returns fine (ok:true), the next page fails with invalid_cursor.
  • Pagination loops that worked in a REST client fail when the cursor is passed through shell/URL encoding.
  • Cursors that end in = (base64 padding) break when the padding is dropped or the value is truncated.
  • Reusing a saved cursor after changing limit, channel, or filter params.

Common Root Causes

1. Not URL-encoding the cursor in a GET request

Cursors are base64-ish strings often ending in = and containing characters that must be percent-encoded in a query string. Passing a raw cursor breaks it.

2. Dropping or altering the trailing padding

Trimming whitespace, stripping =, or lowercasing the cursor makes it unrecognizable.

3. Reusing a cursor across different queries

A cursor is only valid for the exact same method + parameters that produced it. Change limit or the target channel and the cursor no longer applies.

4. Passing an empty string instead of omitting the cursor

Some clients send cursor= on the first call. An empty cursor should simply be omitted, not sent blank.

5. Reading the wrong field

The cursor lives at response_metadata.next_cursor, not at the top level. Grabbing the wrong field yields garbage.

Diagnostic Workflow

Step 1: Capture the cursor from the first page

curl -s "https://slack.com/api/conversations.list?limit=100&types=public_channel" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  | jq '{ok, next: .response_metadata.next_cursor}'
{
  "ok": true,
  "next": "dGVhbTpDMDYxRkE1UEI="
}

An empty next string means there are no more pages — stop, do not send a blank cursor.

Step 2: Reproduce the failure with a raw (unencoded) cursor

curl -s "https://slack.com/api/conversations.list?limit=100&cursor=dGVhbTpDMDYxRkE1UEI=" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq .error
"invalid_cursor"

The trailing = is interpreted as a query delimiter, corrupting the value.

Step 3: URL-encode the cursor

CURSOR="dGVhbTpDMDYxRkE1UEI="
curl -s -G "https://slack.com/api/conversations.list" \
  --data-urlencode "limit=100" \
  --data-urlencode "types=public_channel" \
  --data-urlencode "cursor=$CURSOR" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq '{ok, next: .response_metadata.next_cursor}'
{
  "ok": true,
  "next": "dGVhbTpDMDgxSlA2QTk="
}

Using --data-urlencode (or POSTing form params) preserves the cursor exactly.

Step 4: Loop until the cursor is empty

CURSOR=""
while : ; do
  RESP=$(curl -s -G "https://slack.com/api/conversations.list" \
    --data-urlencode "limit=200" \
    --data-urlencode "cursor=$CURSOR" \
    -H "Authorization: Bearer $SLACK_BOT_TOKEN")
  echo "$RESP" | jq -r '.channels[].name'
  CURSOR=$(echo "$RESP" | jq -r '.response_metadata.next_cursor')
  [ -z "$CURSOR" ] && break
done

Omit the cursor when it is empty (the first iteration sends cursor= which Slack treats as absent).

Example Root Cause Analysis

A nightly job exports every channel for an audit. It builds the next URL by string-concatenating ?cursor= + the raw cursor, and fails on page two:

curl -s "https://slack.com/api/conversations.list?cursor=dGVhbTpDMDYxRkE1UEI=" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq .error
"invalid_cursor"

The cursor ends in =, and because it is concatenated raw into the query string, the shell/HTTP layer treats the trailing = as part of query parsing, so Slack receives a truncated value.

Fix: encode the cursor with --data-urlencode (or send params as a POST body) so the full value reaches Slack:

curl -s -G "https://slack.com/api/conversations.list" \
  --data-urlencode "cursor=dGVhbTpDMDYxRkE1UEI=" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq .ok
true

With the cursor properly encoded, every page loads and the export completes.

Prevention Best Practices

  • Always read the cursor from response_metadata.next_cursor and pass it back byte-for-byte — treat it as opaque.
  • URL-encode the cursor (--data-urlencode) or send it as a POST form/JSON body so padding characters survive.
  • Stop paging when next_cursor is an empty string; never send a blank or fabricated cursor.
  • Keep every other query parameter identical across pages — a cursor is bound to its original method and params.
  • Prefer an official Slack SDK’s paginator, which handles encoding and termination for you.
  • For ad-hoc triage, the free incident assistant can spot a mangled cursor in a failing export. See more in Slack guides.

Quick Command Reference

# First page: capture next_cursor
curl -s "https://slack.com/api/conversations.list?limit=200" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq '.response_metadata.next_cursor'

# Next page: cursor URL-encoded
curl -s -G "https://slack.com/api/conversations.list" \
  --data-urlencode "limit=200" \
  --data-urlencode "cursor=$CURSOR" \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq .ok

# Detect end of pagination
[ -z "$(echo "$RESP" | jq -r '.response_metadata.next_cursor')" ] && echo "done"

Conclusion

invalid_cursor is a pagination-hygiene problem, not a permissions or payload error. The usual root causes:

  1. Passing the cursor unencoded in a GET query so the trailing = corrupts it.
  2. Trimming or altering the opaque cursor value.
  3. Reusing a cursor across different methods or parameters.
  4. Sending a blank cursor instead of omitting it.
  5. Reading the wrong field instead of response_metadata.next_cursor.

Treat the cursor as an opaque token: read it from response_metadata, URL-encode it, keep every other parameter constant, and stop when it comes back empty.

Free download · 368-page PDF

Fixed it? Get 500 Slack & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.