The app permission panel shows Read and Write. The OAuth 2.0 consent screen was configured with list.write. The call to create a List still comes back with an insufficient-scope error.
That is not a bug in your authorization code — it is X’s own API specification. Creating a List requires four scopes together, and list.write is only one of them.
X’s own reference for the create-List endpoint lists list.read, list.write, tweet.read and users.read as required together — not list.write alone. A token missing any one of the four still shows an approved app, still shows Read and Write in the permission panel, and still fails the moment the request hits /2/lists.
Same pattern as the Direct Messages API's dm.write/dm.read pairing: fixing a missing scope after users have already connected their accounts means re-authorization, not a code deploy.
A List is a curated group of accounts, and the API around it gets used less for one-off list-building than for things agencies and SaaS platforms build repeatedly — competitor-monitoring feeds, curated client watchlists, and CRM-style account groupings pulled through automatically instead of maintained by hand. Getting the access model right the first time avoids the scope trap above, and a second failure point baked into how private Lists work.
Two ways to read a List — only one of them sees private ones
App-only Bearer
Public List data only. Never returns a private List's details or members, no matter who owns it or how the request is formatted.
OAuth 2.0 PKCE
Built for user-facing apps. Sees private Lists the authenticated user owns or is a member of.
OAuth 1.0a User Context
Legacy integrations. Same private-List access as OAuth 2.0, through an older authentication flow.
403 Forbidden
X's documented response when a List is private and the credential used has no access to it — a signal to check the auth method, not a formatting error.
The endpoint set
| Method & endpoint | What it does |
|---|---|
| GET /2/lists/:id | Get a List by ID — returns id and name by default; description, member_count, follower_count, owner_id and private are opt-in fields |
| GET /2/users/:id/owned_lists | Get all Lists a user owns, paginated |
| POST /2/lists | Create a List — name required (max 25 characters), description optional (max 100 characters) |
| GET /2/lists/:id/members | Get all members of a List |
| POST /2/lists/:id/members | Add a member to a List |
| DELETE /2/lists/:id/members/:user_id | Remove a member from a List |
| GET /2/users/:id/list_memberships | See which Lists a given user belongs to |
| GET /2/lists/:id/tweets | Get Posts from a List, up to 100 per request |
The 25-character name limit catches automated List-creation tools by surprise. A List name is capped at 25 characters and a description at 100, enforced by the API itself. A naming scheme built around full competitor or client names — which display fine anywhere else on X — can silently fail or need truncation logic nobody planned for at design time.
Where these builds stall
- Requesting only list.write for List creation. The create endpoint needs list.read, tweet.read and users.read alongside it — a token missing any one fails at request time, not at setup time.
- Assuming an App-only Bearer token can read any List by ID. It only sees public Lists; a private List returns 403 regardless of how correctly the request is formatted.
- Treating add-member and remove-member as read operations. Both are write calls, gated by list.write the same way List creation is.
- Not paginating owned_lists for accounts with many Lists. The endpoint returns a fixed page size plus a pagination_token for the rest — stopping after page one silently drops Lists.
- Building competitor-tracking tools around a static membership assumption. List membership, especially on group-style Lists, can change at any time, and a cached member set goes stale without any signal that it has.
What a correctly scoped Lists integration looks like
Getting this right before your first user connects
The pieces here — four scopes, eight endpoints, two access methods — are not complicated individually. What makes a List integration expensive to fix later is timing: the scope list is locked in at the OAuth consent screen, before a single user has connected an account, and any scope missed there has to be recovered through mass re-authorization rather than a code change.
I provide setup, scope-planning and configuration support for X (Twitter) API integrations, including List access design, OAuth flow review and endpoint selection before your first users connect. This sits alongside the platform's OAuth scopes vs. app permissions breakdown and the X API rate limits and usage caps guide covered elsewhere on this site, and the webhook-based Account Activity API for teams that need real-time events rather than polling.
Scopes, endpoints and rate limits on the X API change frequently and must be confirmed against the official documentation and your developer console at the time you build. This page is technical setup and configuration support — no specific outcome or timeline can be promised.