X (Twitter) API Setup Support

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.

The scope combination almost nobody checks before requesting access

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 & endpointWhat it does
GET /2/lists/:idGet 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_listsGet all Lists a user owns, paginated
POST /2/listsCreate a List — name required (max 25 characters), description optional (max 100 characters)
GET /2/lists/:id/membersGet all members of a List
POST /2/lists/:id/membersAdd a member to a List
DELETE /2/lists/:id/members/:user_idRemove a member from a List
GET /2/users/:id/list_membershipsSee which Lists a given user belongs to
GET /2/lists/:id/tweetsGet 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

One consent screenAll four create-List scopes requested up front, with no silent re-authorization later
Private-List awareOAuth 2.0 or OAuth 1.0a used wherever private List access is needed; App-only reserved for public data only
Fully paginatedOwned Lists and List members retrieved completely, not just the first page returned

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.