read:users

Reads up to a hundred users in one call, by handle or by id. It is what turns a page of author_ids into names, and the reason Okaybot can read a timeline without paying for expansions it did not quote.

It is also the easiest way to spend a cap by accident: a hundred ids is a dollar.

Fields

usernames X handles, without the @, up to 100. One list or the other
ids X's own numeric ids, as strings, up to 100. One list or the other
profile the account to read as. Optional when the agent has one
reason Required over MCP. One short sentence, in the agent's own words, for the person

Handles and ids never share a call, for the same reason read:user refuses the pair.

What comes back

{ "status": "done",
  "ref": "V5XT8P2J",
  "data": {
    "data": [
      { "id": "1400000000000000044",
        "name": "Playground User",
        "username": "playground_user",
        "created_at": "2024-09-08T14:18:21.362Z",
        "description": "Default playground user for testing API endpoints.",
        "protected": false,
        "verified": false,
        "verified_type": "none",
        "public_metrics": {
          "followers_count": 18, "following_count": 23, "tweet_count": 67,
          "listed_count": 10, "like_count": 109, "media_count": 16
        } }
    ],
    "errors": [
      { "value": "nobody_at_all",
        "detail": "Could not find user with username: [nobody_at_all].",
        "title": "Not Found Error",
        "resource_type": "user",
        "parameter": "usernames",
        "type": "https://api.twitter.com/2/problems/resource-not-found" }
    ]
  } }

Users come back in no guaranteed order, so match them by id or username rather than by position.

A partial answer is the normal case

A handle X does not hold is an entry in errors rather than a gap in data, and the call is still done. When nothing at all was found there is no data key.