XClient

Hex.pmHex DocsCILicense: MIT

A comprehensive, production-grade Elixir client for the X (Twitter) API v1.1.

Features

Installation

Add x_client to your list of dependencies in mix.exs:

def deps do
  [
    {:x_client, "~> 1.1"}
  ]
end

Then run:

mix deps.get

Configuration

Via config.exs

# config/config.exs
config :x_client,
  consumer_key: "YOUR_CONSUMER_KEY",
  consumer_secret: "YOUR_CONSUMER_SECRET",
  access_token: "YOUR_ACCESS_TOKEN",
  access_token_secret: "YOUR_ACCESS_TOKEN_SECRET"

Via environment variables (recommended for production)

# config/runtime.exs
config :x_client,
  consumer_key: {:system, "X_CONSUMER_KEY"},
  consumer_secret: {:system, "X_CONSUMER_SECRET"},
  access_token: {:system, "X_ACCESS_TOKEN"},
  access_token_secret: {:system, "X_ACCESS_TOKEN_SECRET"}

Optional tuning

config :x_client,
  base_url: "https://api.x.com/1.1",       # default
  upload_url: "https://upload.x.com/1.1",  # default
  auto_retry: true,                         # default
  max_retries: 3,                           # default
  retry_base_delay_ms: 1_000,              # default — doubles each retry
  request_timeout_ms: 30_000              # default

Quick Start

# Post a tweet
{:ok, tweet} = XClient.Tweets.update("Hello from Elixir! 🚀")

# Upload an image and attach it to a tweet
{:ok, media} = XClient.Media.upload("priv/photo.jpg")
{:ok, tweet} = XClient.Tweets.update(
  "Check this out!",
  media_ids: [media["media_id_string"]]
)

# Search tweets
{:ok, %{"statuses" => tweets}} = XClient.Search.tweets("elixir lang", count: 100)

# Get a user's timeline
{:ok, tweets} = XClient.Tweets.user_timeline(screen_name: "elixirlang", count: 50)

# Follow a user
{:ok, user} = XClient.Friendships.create(screen_name: "elixirlang")

# Like a tweet
{:ok, tweet} = XClient.Favorites.create("123456789")

# Send a Direct Message
{:ok, event} = XClient.DirectMessages.send("987654321", "Hello!")

# Verify credentials
{:ok, account} = XClient.verify_credentials()

Multi-account Usage

Create a per-request client instead of relying on global config:

client = XClient.client(
  consumer_key: "CK",
  consumer_secret: "CS",
  access_token: "AT",
  access_token_secret: "ATS"
)

{:ok, tweet} = XClient.Tweets.update(client, "Tweet from account 2!")
{:ok, user}  = XClient.Users.show([screen_name: "elixirlang"], client)

Error Handling

Every function returns {:ok, term()} or {:error, %XClient.Error{}}:

case XClient.Tweets.update("Hello!") do
  {:ok, tweet} ->
    IO.puts("Posted: #{tweet["id_string"]}")

  {:error, %XClient.Error{status: 429, rate_limit_info: info}} ->
    IO.puts("Rate limited. Resets at #{info[:reset]}")

  {:error, %XClient.Error{status: 401, code: 32}} ->
    IO.puts("Authentication failed")

  {:error, %XClient.Error{status: 403, code: 187}} ->
    IO.puts("Duplicate tweet")

  {:error, %XClient.Error{message: message}} ->
    IO.puts("Error: #{message}")
end

Available Modules

Module Endpoints
XClient.Tweets update, destroy, retweet, unretweet, show, lookup, user_timeline, mentions_timeline, retweets_of_me, retweets, retweeters_ids
XClient.Media upload, chunked_upload, upload_status, add_metadata
XClient.Users show, lookup, search, suggestions, suggestions_slug, suggestions_members
XClient.Friendships create, destroy, show, followers_ids, followers_list, friends_ids, friends_list
XClient.Favorites create, destroy, list
XClient.DirectMessages send, destroy, list, show
XClient.Lists list, statuses, show, members, members_show, memberships, ownerships, subscribers, subscribers_show, subscriptions
XClient.Search tweets
XClient.Account verify_credentials, settings, update_settings, update_profile, update_profile_image, update_profile_banner, remove_profile_banner
XClient.Trends place, available, closest
XClient.Geo id
XClient.Help configuration, languages, privacy, tos
XClient.API rate_limit_status

Media Uploads

Simple image upload (< 5 MB)

# From a file path (MIME type auto-detected)
{:ok, media} = XClient.Media.upload("priv/photo.jpg")

# From binary data (media_type required)
data = File.read!("priv/photo.png")
{:ok, media} = XClient.Media.upload(data, media_type: "image/png")

# With alt text for accessibility
{:ok, media} = XClient.Media.upload("priv/photo.jpg",
  alt_text: "A sunset over the ocean")

Video upload (chunked, up to 512 MB)

{:ok, media} = XClient.Media.upload("priv/clip.mp4",
  media_category: "tweet_video")

{:ok, tweet} = XClient.Tweets.update(
  "My new video!",
  media_ids: [media["media_id_string"]]
)

Multiple images (up to 4)

media_ids =
  ["img1.jpg", "img2.jpg", "img3.jpg", "img4.jpg"]
  |> Enum.map(fn path ->
    {:ok, media} = XClient.Media.upload(path)
    media["media_id_string"]
  end)

{:ok, tweet} = XClient.Tweets.update("Four photos!", media_ids: media_ids)

Pagination

Cursor-based pagination is supported on all collection endpoints:

# First page
{:ok, %{"ids" => ids, "next_cursor" => cursor}} =
  XClient.Friendships.followers_ids(screen_name: "elixirlang")

# Next page
{:ok, %{"ids" => more_ids, "next_cursor" => next_cursor}} =
  XClient.Friendships.followers_ids(screen_name: "elixirlang", cursor: cursor)

Rate Limiting

The library tracks rate limit windows from response headers and blocks requests proactively when a window is exhausted:

# Check stored rate limit info for an endpoint
info = XClient.RateLimiter.get_limit_info("statuses/user_timeline.json")
# => %{limit: 900, remaining: 847, reset: 1712345678}

# Check all rate limits from the API
{:ok, limits} = XClient.API.rate_limit_status()
{:ok, limits} = XClient.API.rate_limit_status(resources: "statuses,friends")

Telemetry

Attach to XClient's telemetry events for observability:

# In your application.ex start/2 or a dedicated telemetry supervisor
:telemetry.attach_many(
  "my-app-x-client",
  [
    [:x_client, :request, :start],
    [:x_client, :request, :stop],
    [:x_client, :request, :error],
    [:x_client, :rate_limit, :blocked]
  ],
  &MyApp.Telemetry.handle_event/4,
  nil
)

# Event shapes:
# [:x_client, :request, :start]  — %{}, %{method: atom, url: binary, endpoint: binary}
# [:x_client, :request, :stop]   — %{duration_us: integer}, %{status: integer, ...}
# [:x_client, :request, :error]  — %{duration_us: integer}, %{reason: term}
# [:x_client, :rate_limit, :blocked] — %{}, %{endpoint: binary}

Development

# Install dependencies
mix deps.get

# Run the full check suite
mix check          # format + credo + dialyzer

# Run tests
mix test
mix test.ci        # with coverage

# Generate documentation
mix docs

# Individual checks
mix format --check-formatted
mix credo --strict
mix dialyzer

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Ensure all checks pass: mix check
  4. Submit a Pull Request

License

This project is licensed under the MIT License — see LICENSE for details.

Links