Version: Latest
Last Updated: January 2025
Repository: https://github.com/xdevplatform/playground
- Introduction
- Installation
- Quick Start
- CLI Commands
- Configuration
- API Endpoints
- State Management
- Authentication
- Rate Limiting
- Query Parameters & Field Selection
- Expansions
- Request Validation
- Error Handling
- Examples and Use Cases
- Advanced Features
- Performance & Best Practices
- Integration Patterns
- Troubleshooting
- API Reference
The X URL Playground is a local HTTP server that provides a complete simulation of the X (Twitter) API v2 for testing and development. It runs entirely on your local machine and requires no internet connection (after initial setup).
- ✅ Complete API Compatibility: Supports all endpoints from the X API OpenAPI specification
- ✅ Stateful Operations: Maintains in-memory state for realistic testing workflows
- ✅ State Persistence: Optional file-based persistence across server restarts
- ✅ Request Validation: Validates request bodies, query parameters, and field selections
- ✅ Error Responses: Matches real API error formats exactly
- ✅ Rate Limiting: Configurable rate limit simulation
- ✅ OpenAPI-Driven: Automatically supports new endpoints as they're added to the spec
- ✅ No API Credits: Test without consuming your X API quota
- ✅ Offline Development: Work without internet connectivity
- ✅ CORS Support: Works with web applications
- ✅ Interactive Web UI: Data Explorer with relationships view, search operators, and pagination
- ✅ Relationship Management: View and search user relationships (likes, follows, bookmarks, etc.)
- Development: Test API integrations locally without hitting rate limits
- CI/CD: Run automated tests against a predictable API
- Learning: Explore X API endpoints without API keys
- Prototyping: Build and test features before deploying
- Debugging: Reproduce issues in a controlled environment
┌─────────────────┐
│ playground CLI │
│ (standalone) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ HTTP Server │
│ (localhost) │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌──────────────┐
│ OpenAPI Spec │◄─────│ X API Spec │
│ Cache │ │ (api.x.com)│
└────────┬────────┘ └──────────────┘
│
▼
┌─────────────────┐ ┌──────────────┐
│ State Manager │◄─────│ Persistence │
│ (In-Memory) │ │ (File) │
└─────────────────┘ └──────────────┘
- Go 1.21 or later: Required to build the playground
- Internet Connection: Required for initial OpenAPI spec download (optional after)
- 4GB RAM: Recommended for optimal performance
- 100MB Disk Space: For OpenAPI cache and state files
# Clone the repository
git clone https://github.com/xdevplatform/playground.git
cd playground
# Build the binary
go build -o playground ./cmd/playground
# Make executable (optional)
chmod +x playground
# Test installation
./playground --help
# Move to PATH (optional, for global access)
sudo mv playground /usr/local/bin/# Install latest version
go install github.com/xdevplatform/playground/cmd/playground@latest
# Or install a specific version
go install github.com/xdevplatform/playground/cmd/playground@v1.0.0This will install the playground binary to $GOPATH/bin (or $HOME/go/bin by default).
Make sure $GOPATH/bin is in your PATH:
export PATH=$PATH:$(go env GOPATH)/bin# Download from releases
# Visit https://github.com/xdevplatform/playground/releases/latest
# Download the binary for your platform
# Or use wget/curl (example for Linux amd64)
wget https://github.com/xdevplatform/playground/releases/latest/download/playground-linux-amd64 -O playground
chmod +x playground
sudo mv playground /usr/local/bin/# Check help
playground --help
# Expected output:
# X API Playground - Local X API v2 simulator
#
# Usage:
# playground [command]
#
# Available Commands:
# refresh Refresh OpenAPI spec cache
# start Start the playground API server
# status Check playground server status- Create Configuration Directory (optional):
mkdir -p ~/.playground- Create Configuration File (optional):
cat > ~/.playground/config.json <<EOF
{
"persistence": {
"enabled": true,
"auto_save": true,
"save_interval": 60
}
}
EOFplayground startExpected Output:
Loaded OpenAPI spec (version: 3.1.0)
Playground server starting on http://localhost:8080
Supported endpoints: All X API v2 endpoints from OpenAPI spec
Management endpoints: /health, /rate-limits, /config, /state
State persistence: ENABLED (file: ~/.playground/state.json, auto-save: true, interval: 60s)
Web UI: http://localhost:8080/playground
Set API_BASE_URL=http://localhost:8080 to use the playground with your API client
In a new terminal:
# Get the authenticated user
curl http://localhost:8080/2/users/me \
-H "Authorization: Bearer test"
# Expected Response:
# {
# "data": {
# "id": "0",
# "name": "Playground User",
# "username": "playground_user",
# "created_at": "2025-01-01T00:00:00Z"
# }
# }curl -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from the playground!"}'
# Expected Response:
# {
# "data": {
# "id": "1",
# "text": "Hello from the playground!"
# }
# }You can use any HTTP client with the playground:
Using curl:
# Get authenticated user
curl -H "Authorization: Bearer test_token" http://localhost:8080/2/users/me
# Create a tweet
curl -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test_token" \
-H "Content-Type: application/json" \
-d '{"text": "Testing!"}'Using environment variable:
# Set playground as API base URL
export API_BASE_URL=http://localhost:8080
# Now your API client can use the playgroundUsing the Web UI: Open http://localhost:8080/playground in your browser to interactively explore and test endpoints.
The Web UI includes:
- Data Explorer: Browse and search users, posts, lists, relationships, and other entities
- Search Operators: Use advanced search operators like
user:username,type:bookmark,post:idto filter relationships - Relationship Viewing: Explore user relationships including likes, follows, bookmarks, reposts, muting, blocking, and list memberships
- State Statistics: View counts and statistics for all entity types
- API Testing: Build and test API requests interactively
Press Ctrl+C in the terminal where the server is running. The server will:
- Save state (if persistence enabled)
- Shut down gracefully
- Display:
✅ Playground server stopped gracefully
Start the playground API server.
playground start [flags]| Flag | Short | Default | Description |
|---|---|---|---|
--port |
-p |
8080 |
Port to run the playground server on |
--host |
localhost |
Host to bind the playground server to | |
--refresh |
false |
Force refresh of OpenAPI spec cache |
# Start on default port (8080)
playground start
# Start on custom port
playground start --port 3000
# Start on all interfaces (0.0.0.0)
playground start --host 0.0.0.0
# Start and refresh OpenAPI spec
playground start --refresh
# Start on custom port and refresh
playground start --port 9000 --refresh- Server runs until interrupted (
Ctrl+C) - State is saved on shutdown (if persistence enabled)
- OpenAPI spec is cached locally (refreshed if older than 24 hours)
- Logs are written to stdout
0: Normal shutdown1: Error starting server130: Interrupted by user (Ctrl+C)
Check if the playground server is running.
playground status# Check if server is running
playground status
# Expected Output (if running):
# ✅ Playground server is running on port 8080
# Expected Output (if not running):
# ❌ No playground server found- Attempts to connect to
http://localhost:8080/2/users/me - Timeout: 2 seconds
- Returns exit code
1if server is not running
Force refresh the cached OpenAPI specification.
playground refresh# Refresh OpenAPI spec cache
playground refresh
# Expected Output:
# ✅ OpenAPI spec cache refreshed- Fetches latest spec from
https://api.x.com/2/openapi.json - Updates local cache file
- Displays cache location and age
- After X API adds new endpoints
- If you suspect cache is stale
- Before important testing sessions
The OpenAPI spec cache is automatically managed. The cache file is located at ~/.playground/.playground-openapi-cache.json.
Cache Behavior:
- Cache is automatically refreshed if older than 24 hours
- Use
playground start --refreshto force refresh on startup - Use
playground refreshto refresh the cache without starting the server - Cache is automatically created on first server start
The playground reads configuration from:
- User Config:
~/.playground/config.json(takes precedence) - Default Config: Embedded default (used if user config doesn't exist)
The configuration file is JSON format:
{
"tweets": { ... },
"users": { ... },
"places": { ... },
"topics": { ... },
"streaming": { ... },
"rate_limit": { ... },
"errors": { ... },
"auth": { ... },
"persistence": { ... }
}Purpose: Customize tweet texts for seeding.
Structure:
{
"tweets": {
"texts": [
"Custom tweet text 1",
"Custom tweet text 2",
"Another tweet with #hashtags and @mentions"
]
}
}Fields:
texts(array of strings): Custom tweet texts used when creating initial tweets
Example:
{
"tweets": {
"texts": [
"Just shipped a new feature! 🚀",
"Reading about #AI developments",
"Great conversation about API design today"
]
}
}Default: Uses embedded default texts if not specified.
Purpose: Define custom user profiles for seeding.
Structure:
{
"users": {
"profiles": [
{
"username": "username",
"name": "Display Name",
"description": "Bio text",
"location": "Location",
"verified": false,
"protected": false,
"url": "https://example.com"
}
]
}
}Fields:
profiles(array of objects): Custom user profilesusername(string, required): Username (without @)name(string, required): Display namedescription(string, optional): Bio/descriptionlocation(string, optional): Locationverified(boolean, optional): Verified badgeprotected(boolean, optional): Protected accounturl(string, optional): Profile URL
Example:
{
"users": {
"profiles": [
{
"username": "developer",
"name": "Developer",
"description": "Software developer",
"location": "San Francisco, CA",
"verified": true,
"protected": false
},
{
"username": "designer",
"name": "Designer",
"description": "UI/UX Designer",
"verified": false,
"protected": true
}
]
}
}Default: Creates default "Playground User" if not specified.
Purpose: Define custom places/locations.
Structure:
{
"places": {
"places": [
{
"full_name": "San Francisco, CA",
"name": "San Francisco",
"country": "United States",
"country_code": "US",
"place_type": "city",
"latitude": 37.7749,
"longitude": -122.4194
}
]
}
}Fields:
places(array of objects): Custom placesfull_name(string, required): Full place namename(string, required): Place namecountry(string, required): Country namecountry_code(string, required): ISO country codeplace_type(string, required): Type (city, admin, country, poi)latitude(number, required): Latitudelongitude(number, required): Longitude
Purpose: Define custom topics.
Structure:
{
"topics": {
"topics": [
{
"name": "Technology",
"description": "Technology news and discussions"
}
]
}
}Fields:
topics(array of objects): Custom topicsname(string, required): Topic namedescription(string, optional): Topic description
Purpose: Configure streaming endpoint behavior.
Structure:
{
"streaming": {
"default_delay_ms": 200
}
}Fields:
default_delay_ms(integer, optional): Default delay between streamed tweets in milliseconds (default: 200)
Example:
{
"streaming": {
"default_delay_ms": 500
}
}Purpose: Configure rate limiting simulation.
Structure:
{
"rate_limit": {
"enabled": true,
"limit": 15,
"window_sec": 900
}
}Fields:
enabled(boolean, optional): Enable rate limiting (default: false)limit(integer, optional): Requests per window (default: 15)window_sec(integer, optional): Window size in seconds (default: 900 = 15 minutes)
Example:
{
"rate_limit": {
"enabled": true,
"limit": 15,
"window_sec": 900
}
}Behavior:
- When enabled, tracks requests per window
- Returns
429 Too Many Requestswhen limit exceeded - Includes rate limit headers in responses
- Resets after window expires
Rate Limit Headers:
x-rate-limit-limit: Request limit per windowx-rate-limit-remaining: Remaining requestsx-rate-limit-reset: Reset time (Unix timestamp)
Purpose: Configure error simulation (for testing error handling).
Structure:
{
"errors": {
"enabled": false,
"error_rate": 0.0,
"error_type": "rate_limit",
"status_code": 429
}
}Fields:
enabled(boolean, optional): Enable error simulation (default: false)error_rate(float, optional): Probability of error (0.0-1.0, default: 0.0)error_type(string, optional): Type of error: "rate_limit", "server_error", "not_found" (default: "rate_limit")status_code(integer, optional): HTTP status code (default: 429 for rate_limit)
Example:
{
"errors": {
"enabled": true,
"error_rate": 0.1,
"error_type": "server_error",
"status_code": 500
}
}Use Case: Test error handling in your application.
Purpose: Configure authentication validation.
Structure:
{
"auth": {
"disable_validation": false
}
}Fields:
disable_validation(boolean, optional): Iftrue, allows requests without authentication (default: false)
Example (Testing Only):
{
"auth": {
"disable_validation": true
}
}Warning: Only disable for testing. The real X API always requires authentication.
Behavior:
false(default): Enforces authentication like real APItrue: Allows requests withoutAuthorizationheader
Purpose: Configure state persistence to disk.
Structure:
{
"persistence": {
"enabled": true,
"file_path": "~/.playground/state.json",
"auto_save": true,
"save_interval": 60
}
}Fields:
enabled(boolean, optional): Enable state persistence (default: false)file_path(string, optional): Path to state file (default:~/.playground/state.json)auto_save(boolean, optional): Auto-save on state changes (default: true if enabled)save_interval(integer, optional): Auto-save interval in seconds (default: 60)
Example:
{
"persistence": {
"enabled": true,
"file_path": "~/.playground/state.json",
"auto_save": true,
"save_interval": 60
}
}Behavior:
- When enabled, state is saved to disk
- Auto-save runs periodically (every
save_intervalseconds) - State is saved on server shutdown
- State is loaded on server startup
File Format: JSON
File Location: Defaults to ~/.playground/state.json
{
"tweets": {
"texts": [
"Just shipped a new feature! 🚀",
"Reading about #AI developments",
"Great conversation about API design"
]
},
"users": {
"profiles": [
{
"username": "developer",
"name": "Developer",
"description": "Software developer",
"verified": true
}
]
},
"rate_limit": {
"enabled": true,
"limit": 15,
"window_sec": 900
},
"auth": {
"disable_validation": false
},
"persistence": {
"enabled": true,
"file_path": "~/.playground/state.json",
"auto_save": true,
"save_interval": 60
}
}- Edit
~/.playground/config.json - Restart the server:
playground start
# Update config (temporary - lost on restart)
curl -X PUT http://localhost:8080/config/update \
-H "Content-Type: application/json" \
-d '{
"rate_limit": {
"enabled": true,
"limit": 20
}
}'Note: API updates are temporary and lost on server restart.
The playground supports all endpoints from the X API OpenAPI specification. Endpoints are organized into categories below.
All endpoints are prefixed with /2/:
http://localhost:8080/2/{endpoint}
These endpoints are specific to the playground (not part of X API):
Server health and statistics.
Authentication: Not required
Response:
{
"status": "healthy",
"uptime_seconds": 3600,
"requests_total": 150,
"requests_success": 145,
"requests_error": 5,
"avg_response_time_ms": 12
}Fields:
status(string): Server status ("healthy")uptime_seconds(integer): Server uptime in secondsrequests_total(integer): Total requests processedrequests_success(integer): Successful requests (2xx)requests_error(integer): Error requests (4xx, 5xx)avg_response_time_ms(integer): Average response time in milliseconds
Example:
curl http://localhost:8080/healthRate limit configuration and status.
Authentication: Not required
Response:
{
"enabled": true,
"limit": 15,
"window_sec": 900,
"remaining": 10,
"reset_at": "2025-12-15T10:30:00Z"
}Fields:
enabled(boolean): Whether rate limiting is enabledlimit(integer): Requests per windowwindow_sec(integer): Window size in secondsremaining(integer): Remaining requests in current windowreset_at(string): ISO 8601 timestamp when window resets
Example:
curl http://localhost:8080/rate-limitsView current configuration.
Authentication: Not required
Response:
{
"tweets": { ... },
"users": { ... },
"rate_limit": { ... },
"auth": { ... },
"persistence": { ... }
}Example:
curl http://localhost:8080/config | jqUpdate configuration (temporary - lost on restart).
Authentication: Not required
Request Body:
{
"rate_limit": {
"enabled": true,
"limit": 20
}
}Response:
{
"message": "Configuration updated",
"config": { ... }
}Example:
curl -X PUT http://localhost:8080/config/update \
-H "Content-Type: application/json" \
-d '{"rate_limit": {"enabled": true}}'Note: Changes are temporary and lost on server restart.
Reset state to initial seed data.
Authentication: Not required
Response:
{
"message": "State reset successfully"
}Example:
curl -X POST http://localhost:8080/state/resetWarning: This deletes all current state (tweets, users, relationships, etc.).
Export current state as JSON.
Authentication: Not required
Response:
{
"users": { ... },
"tweets": { ... },
"media": { ... },
"lists": { ... },
"relationships": [
{
"id": "like-0-1",
"type": "like",
"user_id": "0",
"target_tweet_id": "1"
},
{
"id": "following-0-2",
"type": "following",
"user_id": "0",
"target_user_id": "2"
}
],
...
}Response Fields:
users: Map of user objects keyed by user IDtweets: Map of tweet/post objects keyed by tweet IDlists: Map of list objects keyed by list IDrelationships: Array of relationship objects representing user interactions (likes, follows, bookmarks, etc.)id: Unique relationship identifiertype: Relationship type (bookmark,like,following,follower,retweet,mute,block,list_member,followed_list,pinned_list)user_id: ID of the user who initiated the relationshiptarget_user_id: ID of the target user (for user-to-user relationships)target_tweet_id: ID of the target tweet/post (for user-to-tweet relationships)target_list_id: ID of the target list (for user-to-list relationships)
Example:
# Export to file
curl http://localhost:8080/state/export > my-state.json
# Pretty print
curl http://localhost:8080/state/export | jq > my-state.jsonUse Case: Backup state, share state between environments, debug state issues.
Import state from JSON.
Authentication: Not required
Request Body:
{
"users": { ... },
"tweets": { ... },
...
}Response:
{
"message": "State imported successfully",
"users_count": 10,
"tweets_count": 50
}Example:
curl -X POST http://localhost:8080/state/import \
-H "Content-Type: application/json" \
-d @my-state.jsonUse Case: Restore from backup, load test data, share state.
Manually save state (if persistence enabled).
Authentication: Not required
Response:
{
"message": "State saved successfully",
"file_path": "~/.playground/state.json"
}Example:
curl -X POST http://localhost:8080/state/saveUse Case: Force save before important operations, ensure state is persisted.
The playground provides API endpoints to programmatically access the same usage and cost data shown in the Usage tab of the web UI. These endpoints track API usage at the developer account level and provide detailed cost breakdowns.
Quick Start:
# Get complete cost breakdown (same as Usage tab)
curl http://localhost:8080/api/accounts/0/cost | jq
# Get usage breakdown by event type
curl "http://localhost:8080/api/accounts/0/usage?interval=30days&groupBy=eventType" | jq
# Get usage breakdown by request type
curl "http://localhost:8080/api/accounts/0/usage?interval=30days&groupBy=requestType" | jqNote: The account_id parameter is your developer account ID, automatically derived from your authentication token. The default token "test" maps to account "0". Different tokens map to different developer accounts.
Get the current pricing configuration for all event types and request types.
Authentication: Not required
Query Parameters:
refresh(boolean, optional): Iftrue, forces a refresh of pricing from the API. Default:false
Response:
{
"eventTypePricing": {
"Post": 0.005,
"User": 0.01,
"Like": 0.001,
"Follow": 0.01,
...
},
"requestTypePricing": {
"Write": 0.01,
"ContentCreate": 0.01,
"UserInteractionCreate": 0.015,
...
},
"defaultCost": 0
}Fields:
eventTypePricing(object): Map of event type names to their cost per resourcerequestTypePricing(object): Map of request type names to their cost per requestdefaultCost(number): Default cost for unpriced endpoints
Example:
# Get current pricing
curl http://localhost:8080/api/credits/pricing | jq
# Force refresh pricing from API
curl "http://localhost:8080/api/credits/pricing?refresh=true" | jqGet usage data for a specific account, grouped by event type or request type.
Authentication: Not required
Path Parameters:
-
account_id(string, required): The developer account ID.Important: In the real X API,
account_idrefers to the developer account ID (the account that owns the API keys/apps), not the user ID. All usage across all apps and users under a developer account is aggregated together.In the playground, the developer account ID is automatically derived from the authentication token:
- Bearer tokens: Developer account ID is derived from the token (same token = same account)
- OAuth 1.0a: Developer account ID is derived from the consumer key
- OAuth 2.0: Developer account ID would be extracted from token claims (in production)
- Default: Falls back to authenticated user ID for simple tokens (typically "0")
To simulate multiple developer accounts, use different Bearer tokens or OAuth consumer keys - each will map to a different developer account ID.
Query Parameters:
interval(string, optional): Time interval for usage data. Options:"7days","30days","90days". Default:"30days"groupBy(string, required): Grouping type. Must be either"eventType"or"requestType"
Response:
{
"accountID": "0",
"interval": "30days",
"groupBy": "eventType",
"groups": {
"Post": {
"type": "Post",
"usage": "150",
"price": "0.005",
"totalCost": 0.75,
"usageDataPoints": [
{
"timestamp": "1704067200000",
"value": "10"
},
...
]
},
...
},
"total": {
"usage": "500",
"totalCost": 2.5,
"usageDataPoints": [...]
}
}Fields:
accountID(string): The account IDinterval(string): The time interval usedgroupBy(string): The grouping type usedgroups(object): Map of type names to usage data- Each group contains:
type(string): The type nameusage(string): Total usage count as stringprice(string): Price per item as stringtotalCost(number): Total cost for this typeusageDataPoints(array): Time series data points
- Each group contains:
total(object): Aggregate totals across all types
Example:
# Get event type usage for account 0
curl "http://localhost:8080/api/accounts/0/usage?interval=30days&groupBy=eventType" | jq
# Get request type usage for account 0
curl "http://localhost:8080/api/accounts/0/usage?interval=30days&groupBy=requestType" | jq
# Get 7-day usage
curl "http://localhost:8080/api/accounts/0/usage?interval=7days&groupBy=eventType" | jqGet detailed cost breakdown for a specific account, including billing cycle information and time series data.
Authentication: Not required
Path Parameters:
-
account_id(string, required): The developer account ID.Note: In the real X API, this is the developer account ID (the account that owns the API keys/apps), not the user ID. In the playground, this is automatically derived from your authentication token. Use different tokens/keys to simulate different developer accounts.
Response:
{
"accountID": "0",
"totalCost": 2.5,
"eventTypeCosts": [
{
"type": "Post",
"usage": 150,
"price": 0.005,
"totalCost": 0.75
},
...
],
"requestTypeCosts": [
{
"type": "Write",
"usage": 50,
"price": 0.01,
"totalCost": 0.5
},
...
],
"billingCycleStart": "2026-01-08T00:00:00Z",
"currentBillingCycle": 1,
"eventTypeTimeSeries": [
{
"date": "2026-01-08",
"timestamp": 1704067200000,
"costs": {
"Post": 0.05,
"User": 0.1
}
},
...
],
"requestTypeTimeSeries": [
{
"date": "2026-01-08",
"timestamp": 1704067200000,
"costs": {
"Write": 0.1,
"ContentCreate": 0.05
}
},
...
]
}Fields:
accountID(string): The account IDtotalCost(number): Total estimated cost for the current billing cycleeventTypeCosts(array): Cost breakdown by event typetype(string): Event type nameusage(integer): Number of resources usedprice(number): Price per resourcetotalCost(number): Total cost for this type
requestTypeCosts(array): Cost breakdown by request type (same structure as eventTypeCosts)billingCycleStart(string): ISO 8601 timestamp of the billing cycle start datecurrentBillingCycle(integer): Current billing cycle number (1-based, resets monthly)eventTypeTimeSeries(array): Daily cost data for event types over the billing cycledate(string): Date in YYYY-MM-DD formattimestamp(integer): Unix timestamp in millisecondscosts(object): Map of event type names to daily costs
requestTypeTimeSeries(array): Daily cost data for request types over the billing cycle (same structure as eventTypeTimeSeries)
Example:
# Get cost breakdown for account 0
curl http://localhost:8080/api/accounts/0/cost | jq
# Pretty print with jq
curl http://localhost:8080/api/accounts/0/cost | jq '.totalCost, .eventTypeCosts, .requestTypeCosts'
# Get time series data only
curl http://localhost:8080/api/accounts/0/cost | jq '.eventTypeTimeSeries'Note:
- The billing cycle is a 30-day period starting from the first request made by the account. The cycle resets monthly.
- Developer Account vs User ID: In the real X API,
account_idrefers to the developer account ID (the account that owns the API keys/apps), not the user ID. All usage across all apps and users under a developer account is aggregated together. In the playground, the developer account ID is automatically derived from your authentication token (Bearer token, OAuth consumer key, etc.), matching the real API behavior.
Getting the Same Data as the Usage Tab:
The /api/accounts/{account_id}/cost endpoint returns all the data displayed in the Usage tab:
- Total cost and billing cycle information
- Cost breakdowns by event type and request type
- Daily time series data for the charts (
eventTypeTimeSeriesandrequestTypeTimeSeries) - Usage statistics
To replicate what the Usage tab shows programmatically:
# Get the complete cost breakdown (includes all Usage tab data)
curl http://localhost:8080/api/accounts/0/cost | jq
# Extract specific fields
curl -s http://localhost:8080/api/accounts/0/cost | jq '.totalCost' # Total cost
curl -s http://localhost:8080/api/accounts/0/cost | jq '.eventTypeCosts' # Event type breakdown
curl -s http://localhost:8080/api/accounts/0/cost | jq '.requestTypeCosts' # Request type breakdown
curl -s http://localhost:8080/api/accounts/0/cost | jq '.eventTypeTimeSeries' # Chart data for event types
curl -s http://localhost:8080/api/accounts/0/cost | jq '.requestTypeTimeSeries' # Chart data for request types
curl -s http://localhost:8080/api/accounts/0/cost | jq '.billingCycleStart' # Billing cycle start
curl -s http://localhost:8080/api/accounts/0/cost | jq '.currentBillingCycle' # Current cycle numberThe Usage tab also fetches additional usage details from /api/accounts/{account_id}/usage endpoints, but the /cost endpoint provides the primary data structure.
Due to the extensive number of endpoints, I'll continue with detailed documentation in the next section. The playground supports all X API v2 endpoints. Here are the main categories:
The X API uses field selection to control which fields are returned in responses. This reduces payload size and improves performance.
Use {object}.fields query parameters to specify which fields to return:
?user.fields=id,name,username
?tweet.fields=id,text,created_at
?list.fields=id,name,description
Default Fields (returned if no user.fields specified):
idnameusername
All Available Fields:
id- User ID (Snowflake)name- Display nameusername- Username (without @)created_at- Account creation date (ISO 8601)description- Bio/descriptionentities- Entities extracted from description/URLlocation- Locationpinned_tweet_id- ID of pinned tweetprofile_image_url- Profile image URLprotected- Protected account (boolean)public_metrics- Public metrics objectfollowers_countfollowing_counttweet_countlisted_countlike_countmedia_count
url- Profile URLverified- Verified badge (boolean)verified_type- Verification typewithheld- Withheld information
Example:
# Get only id and name
curl "http://localhost:8080/2/users/0?user.fields=id,name" \
-H "Authorization: Bearer test"
# Get all fields
curl "http://localhost:8080/2/users/0?user.fields=id,name,username,created_at,description,location,url,verified,protected,profile_image_url,pinned_tweet_id,public_metrics,entities,verified_type,withheld" \
-H "Authorization: Bearer test"Default Fields (returned if no tweet.fields specified):
idtext
All Available Fields:
id- Tweet ID (Snowflake)text- Tweet textattachments- Attachments objectmedia_keys- Array of media keyspoll_ids- Array of poll IDs
author_id- Author user IDcontext_annotations- Context annotationsconversation_id- Conversation IDcreated_at- Creation date (ISO 8601)edit_controls- Edit controlsedit_history_tweet_ids- Array of tweet IDs in edit history (always included)entities- Entities extracted from texthashtags- Array of hashtag entitiesmentions- Array of mention entitiesurls- Array of URL entitiescashtags- Array of cashtag entities
geo- Geo informationin_reply_to_user_id- User ID being replied tolang- Language codenon_public_metrics- Non-public metrics (requires elevated access)organic_metrics- Organic metrics (requires elevated access)possibly_sensitive- Possibly sensitive content (boolean)promoted_metrics- Promoted metrics (requires elevated access)public_metrics- Public metrics objectretweet_countreply_countlike_countquote_countbookmark_countimpression_count
referenced_tweets- Array of referenced tweetsreply_settings- Reply settingssource- Source clientwithheld- Withheld information
Example:
# Get only id and text
curl "http://localhost:8080/2/tweets/0?tweet.fields=id,text" \
-H "Authorization: Bearer test"
# Get tweet with metrics
curl "http://localhost:8080/2/tweets/0?tweet.fields=id,text,created_at,public_metrics,author_id" \
-H "Authorization: Bearer test"Default Fields:
idname
All Available Fields:
id- List IDname- List namecreated_at- Creation datedescription- List descriptionfollower_count- Follower countmember_count- Member countowner_id- Owner user IDprivate- Private list (boolean)
Example:
curl "http://localhost:8080/2/lists/0?list.fields=id,name,description,member_count,follower_count" \
-H "Authorization: Bearer test"Default Fields:
media_keytype
All Available Fields:
media_key- Media keytype- Media type (photo, video, animated_gif)url- Media URLduration_ms- Duration in milliseconds (video)height- Height in pixelswidth- Width in pixelspreview_image_url- Preview image URLpublic_metrics- Public metricsalt_text- Alt textvariants- Video variants
Example:
curl "http://localhost:8080/2/media/upload?command=STATUS&media_id=123&media.fields=media_key,type,url,width,height" \
-H "Authorization: Bearer test"Default Fields:
idtitlestatecreated_at
All Available Fields:
id- Space IDtitle- Space titlestate- Space state (scheduled, live, ended)created_at- Creation dateupdated_at- Last update datestarted_at- Start timeended_at- End timescheduled_start- Scheduled start timecreator_id- Creator user IDhost_ids- Array of host user IDsspeaker_ids- Array of speaker user IDsinvited_user_ids- Array of invited user IDssubscriber_count- Subscriber countparticipant_count- Participant countis_ticketed- Ticketed space (boolean)lang- Language code
Default Fields:
idoptionsvoting_status
All Available Fields:
id- Poll IDoptions- Array of poll optionsposition- Option positionlabel- Option labelvotes- Vote count
duration_minutes- Duration in minutesend_datetime- End datetimevoting_status- Voting status (open, closed)
Default Fields:
idfull_namename
All Available Fields:
id- Place IDfull_name- Full place namename- Place namecountry- Country namecountry_code- ISO country codeplace_type- Place type (city, admin, country, poi)geo- Geo informationtype- Geo type (Point)bbox- Bounding boxproperties- Geo properties
- Request Only Needed Fields: Reduces payload size and improves performance
- Use Defaults When Possible: Default fields are optimized for common use cases
- Combine with Expansions: Request fields for expanded objects too
- Validate Fields: Invalid fields return validation errors
Example:
# Efficient: Only request needed fields
curl "http://localhost:8080/2/users/0?user.fields=id,name,username" \
-H "Authorization: Bearer test"
# Less efficient: Request all fields
curl "http://localhost:8080/2/users/0?user.fields=id,name,username,created_at,description,location,url,verified,protected,profile_image_url,pinned_tweet_id,public_metrics,entities" \
-H "Authorization: Bearer test"Expansions allow you to request related objects in the same response, reducing the number of API calls needed.
Use expansions query parameter with comma-separated expansion names:
?expansions=author_id
?expansions=author_id,referenced_tweets.id
author_id- Expand tweet author (returns user object)referenced_tweets.id- Expand referenced tweetsreferenced_tweets.id.author_id- Expand referenced tweet authorsin_reply_to_user_id- Expand user being replied toattachments.media_keys- Expand media attachmentsattachments.poll_ids- Expand poll attachmentsgeo.place_id- Expand place informationentities.mentions.username- Expand mentioned usersreferenced_tweets.id.author_id- Nested expansion
pinned_tweet_id- Expand pinned tweetpinned_tweet_id.author_id- Expand pinned tweet author
owner_id- Expand list ownerlist_id- Expand list information
creator_id- Expand space creatorhost_ids- Expand space hostsspeaker_ids- Expand space speakersinvited_user_ids- Expand invited users
Expanded objects are returned in the includes object:
{
"data": {
"id": "123",
"text": "Hello!",
"author_id": "456"
},
"includes": {
"users": [
{
"id": "456",
"name": "User",
"username": "user"
}
]
}
}curl "http://localhost:8080/2/tweets/0?expansions=author_id&user.fields=id,name,username" \
-H "Authorization: Bearer test"Response:
{
"data": {
"id": "0",
"text": "Hello!",
"author_id": "0"
},
"includes": {
"users": [
{
"id": "0",
"name": "Playground User",
"username": "playground_user"
}
]
}
}curl "http://localhost:8080/2/tweets/0?expansions=referenced_tweets.id&tweet.fields=id,text" \
-H "Authorization: Bearer test"curl "http://localhost:8080/2/tweets/0?expansions=attachments.media_keys&media.fields=media_key,type,url" \
-H "Authorization: Bearer test"curl "http://localhost:8080/2/tweets/0?expansions=author_id,referenced_tweets.id.author_id&user.fields=id,name,username&tweet.fields=id,text" \
-H "Authorization: Bearer test"Invalid expansions return validation errors:
{
"errors": [
{
"parameter": "expansions",
"value": "invalid_expansion",
"detail": "The following expansions are not valid for this endpoint: invalid_expansion"
}
]
}The playground validates requests against the OpenAPI specification to match real API behavior.
- Request Bodies: Validates JSON structure, required fields, types, constraints
- Query Parameters: Validates types, formats, constraints, bounds
- Path Parameters: Validates format (e.g., Snowflake IDs)
- Field Selection: Validates requested fields exist
- Expansions: Validates requested expansions exist
- Unknown Parameters: Rejects parameters not in OpenAPI spec
Missing required fields return validation errors:
Request:
{}Response (400 Bad Request):
{
"errors": [
{
"parameter": "text",
"value": "",
"detail": "text field is required"
}
]
}Invalid types return validation errors:
Request:
{
"text": 123
}Response (400 Bad Request):
{
"errors": [
{
"parameter": "text",
"value": 123,
"detail": "text must be a string"
}
]
}Values outside constraints return validation errors:
Request:
{
"text": ""
}Response (400 Bad Request):
{
"errors": [
{
"parameter": "text",
"value": "",
"detail": "text field is required"
}
]
}Nested objects are validated recursively:
Request:
{
"text": "Hello",
"media": {
"media_ids": ["invalid"]
}
}Response (400 Bad Request):
{
"errors": [
{
"parameter": "media.media_ids[0]",
"value": "invalid",
"detail": "media ID must be a valid Snowflake ID"
}
]
}Example:
# Invalid: max_results must be integer
curl "http://localhost:8080/2/tweets?max_results=abc" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "max_results",
"value": "abc",
"detail": "max_results must be an integer"
}
]
}Example:
# Invalid: max_results must be between 5-100
curl "http://localhost:8080/2/tweets?max_results=200" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "max_results",
"value": "200",
"detail": "max_results must be at most 100"
}
]
}Example:
# Invalid: start_time must be ISO 8601 format
curl "http://localhost:8080/2/tweets/search/recent?query=hello&start_time=invalid" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "start_time",
"value": "invalid",
"detail": "start_time must be a valid ISO 8601 datetime"
}
]
}Example:
# Invalid: unknown_param is not a valid parameter
curl "http://localhost:8080/2/users/me?unknown_param=test" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "unknown_param",
"value": "test",
"detail": "The query parameter 'unknown_param' is not valid for this endpoint"
}
]
}Invalid fields return validation errors:
Example:
curl "http://localhost:8080/2/users/me?user.fields=id,name,invalid_field" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "user.fields",
"value": "invalid_field",
"detail": "The following fields are not valid for user objects: invalid_field"
}
]
}Invalid expansions return validation errors:
Example:
curl "http://localhost:8080/2/tweets/0?expansions=invalid_expansion" \
-H "Authorization: Bearer test"Response (400 Bad Request):
{
"errors": [
{
"parameter": "expansions",
"value": "invalid_expansion",
"detail": "The following expansions are not valid for this endpoint: invalid_expansion"
}
]
}All errors follow the X API error format:
{
"errors": [
{
"parameter": "parameter_name",
"value": "invalid_value",
"detail": "Error message",
"title": "Error Title",
"type": "https://api.twitter.com/2/problems/error-type"
}
],
"title": "Error Title",
"detail": "Error detail message",
"type": "https://api.twitter.com/2/problems/error-type"
}Status Code: 400 Bad Request
Type: https://api.twitter.com/2/problems/invalid-request
Example:
{
"errors": [
{
"parameter": "max_results",
"value": "200",
"detail": "max_results must be at most 100",
"title": "Invalid Request",
"type": "https://api.twitter.com/2/problems/invalid-request"
}
],
"title": "Invalid Request",
"detail": "One or more parameters to your request was invalid.",
"type": "https://api.twitter.com/2/problems/invalid-request"
}Status Code: 404 Not Found
Type: https://api.twitter.com/2/problems/resource-not-found
Example:
{
"errors": [
{
"parameter": "id",
"value": "999999",
"detail": "User not found",
"title": "Not Found Error",
"type": "https://api.twitter.com/2/problems/resource-not-found",
"resource_id": "999999",
"resource_type": "user"
}
],
"title": "Not Found Error",
"detail": "User not found",
"type": "https://api.twitter.com/2/problems/resource-not-found"
}Status Code: 401 Unauthorized
Type: https://api.twitter.com/2/problems/not-authorized-for-resource
Example:
{
"errors": [
{
"detail": "Unauthorized",
"title": "Unauthorized",
"type": "https://api.twitter.com/2/problems/not-authorized-for-resource"
}
],
"title": "Unauthorized",
"detail": "Unauthorized",
"type": "https://api.twitter.com/2/problems/not-authorized-for-resource"
}Status Code: 403 Forbidden
Type: about:blank
The playground returns 403 Forbidden errors when authenticated users attempt to perform actions they don't have permission for, matching the real X API behavior.
Examples:
Delete List (Not Owner):
{
"detail": "You are not allowed to delete this List.",
"type": "about:blank",
"title": "Forbidden",
"status": 403
}Add List Member (Not Owner):
{
"detail": "You are not allowed to add members to this List.",
"type": "about:blank",
"title": "Forbidden",
"status": 403
}Remove List Member (Not Owner):
{
"detail": "You are not allowed to delete members from this List.",
"type": "about:blank",
"title": "Forbidden",
"status": 403
}Delete Post/Tweet (Not Author):
{
"detail": "You are not authorized to delete this Tweet.",
"type": "about:blank",
"title": "Forbidden",
"status": 403
}When These Errors Occur:
DELETE /2/lists/{id}- When the authenticated user is not the list ownerPOST /2/lists/{id}/members- When the authenticated user is not the list ownerDELETE /2/lists/{id}/members/{user_id}- When the authenticated user is not the list ownerDELETE /2/tweets/{id}- When the authenticated user is not the post author
Status Code: 429 Too Many Requests
Type: https://api.twitter.com/2/problems/rate-limit-exceeded
Example:
{
"errors": [
{
"detail": "Rate limit exceeded",
"title": "Rate Limit Exceeded",
"type": "https://api.twitter.com/2/problems/rate-limit-exceeded"
}
],
"title": "Rate Limit Exceeded",
"detail": "Rate limit exceeded",
"type": "https://api.twitter.com/2/problems/rate-limit-exceeded"
}Status Code: 500 Internal Server Error
Type: https://api.twitter.com/2/problems/server-error
- Check Status Code: Always check HTTP status code first
- Parse Error Object: Parse
errorsarray for details - Handle Multiple Errors:
errorsarray can contain multiple errors - Check Error Type: Use
typefield to determine error category - Log Error Details: Log
parameter,value, anddetailfor debugging
Example Error Handling (JavaScript):
try {
const response = await fetch('http://localhost:8080/2/tweets', {
method: 'POST',
headers: {
'Authorization': 'Bearer test',
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: '' })
});
if (!response.ok) {
const error = await response.json();
if (error.errors) {
error.errors.forEach(err => {
console.error(`Error in ${err.parameter}: ${err.detail}`);
});
}
}
} catch (error) {
console.error('Request failed:', error);
}# 1. Create a tweet
TWEET_ID=$(curl -s -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from the playground!"}' | jq -r '.data.id')
echo "Created tweet: $TWEET_ID"
# 2. Get the tweet
curl -s "http://localhost:8080/2/tweets/$TWEET_ID?tweet.fields=id,text,created_at,public_metrics" \
-H "Authorization: Bearer test" | jq
# 3. Get user's tweets (timeline)
curl -s "http://localhost:8080/2/users/0/tweets?max_results=10&tweet.fields=id,text,created_at" \
-H "Authorization: Bearer test" | jq# 1. Create a second user (via state import or use existing)
# For this example, assume user ID "1" exists
# 2. Follow the user
curl -X POST http://localhost:8080/2/users/0/following \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"target_user_id": "1"}'
# 3. Get following list
curl -s "http://localhost:8080/2/users/0/following?max_results=10&user.fields=id,name,username" \
-H "Authorization: Bearer test" | jq
# 4. Get followers of user 1 (should include user 0)
curl -s "http://localhost:8080/2/users/1/followers?max_results=10&user.fields=id,name,username" \
-H "Authorization: Bearer test" | jq# 1. Create a post
POST_ID=$(curl -s -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"text": "Like this post!"}' | jq -r '.data.id')
# 2. Like the post
curl -X POST http://localhost:8080/2/users/0/likes \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d "{\"tweet_id\": \"$POST_ID\"}"
# 3. Get user's likes
curl -s "http://localhost:8080/2/users/0/likes?max_results=10&tweet.fields=id,text" \
-H "Authorization: Bearer test" | jq
# 4. Get users who liked the post
curl -s "http://localhost:8080/2/tweets/$POST_ID/liking_users?max_results=10&user.fields=id,name,username" \
-H "Authorization: Bearer test" | jqNote: Relationships created via API calls (likes, follows, bookmarks, etc.) are automatically included in the state export and visible in the Data Explorer.
# 1. Create a list
LIST_ID=$(curl -s -X POST http://localhost:8080/2/lists \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"name": "Developers", "description": "List of developers"}' | jq -r '.data.id')
echo "Created list: $LIST_ID"
# 2. Add members (assuming user IDs 1, 2, 3 exist)
for USER_ID in 1 2 3; do
curl -X POST "http://localhost:8080/2/lists/$LIST_ID/members" \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d "{\"user_id\": \"$USER_ID\"}"
done
# 3. Get list members
curl -s "http://localhost:8080/2/lists/$LIST_ID/members?max_results=10&user.fields=id,name,username" \
-H "Authorization: Bearer test" | jq
# 4. Get tweets from list members
curl -s "http://localhost:8080/2/lists/$LIST_ID/tweets?max_results=10&tweet.fields=id,text,created_at" \
-H "Authorization: Bearer test" | jq# 1. Create some tweets with specific content
curl -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"text": "Hello world #testing"}'
curl -X POST http://localhost:8080/2/tweets \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"text": "Another hello #testing"}'
# 2. Search for tweets
curl -s "http://localhost:8080/2/tweets/search/recent?query=hello&max_results=10&tweet.fields=id,text,created_at" \
-H "Authorization: Bearer test" | jq
# 3. Search with time filter
START_TIME=$(date -u -v-1H +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "1 hour ago" +"%Y-%m-%dT%H:%M:%SZ")
curl -s "http://localhost:8080/2/tweets/search/recent?query=hello&start_time=$START_TIME&max_results=10" \
-H "Authorization: Bearer test" | jqimport requests
BASE_URL = "http://localhost:8080"
HEADERS = {
"Authorization": "Bearer test",
"Content-Type": "application/json"
}
# Create a tweet
response = requests.post(
f"{BASE_URL}/2/tweets",
headers=HEADERS,
json={"text": "Hello from Python!"}
)
tweet = response.json()["data"]
print(f"Created tweet: {tweet['id']}")
# Get user tweets
response = requests.get(
f"{BASE_URL}/2/users/0/tweets",
headers=HEADERS,
params={"max_results": 10, "tweet.fields": "id,text,created_at"}
)
tweets = response.json()["data"]
print(f"Found {len(tweets)} tweets")const fetch = require('node-fetch');
const BASE_URL = 'http://localhost:8080';
const HEADERS = {
'Authorization': 'Bearer test',
'Content-Type': 'application/json'
};
// Create a tweet
async function createTweet(text) {
const response = await fetch(`${BASE_URL}/2/tweets`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ text })
});
return response.json();
}
// Get user tweets
async function getUserTweets(userId) {
const response = await fetch(
`${BASE_URL}/2/users/${userId}/tweets?max_results=10&tweet.fields=id,text,created_at`,
{ headers: { 'Authorization': 'Bearer test' } }
);
return response.json();
}
// Usage
createTweet('Hello from Node.js!').then(data => {
console.log('Created tweet:', data.data.id);
return getUserTweets('0');
}).then(data => {
console.log('User tweets:', data.data);
});The playground includes a comprehensive web interface accessible at http://localhost:8080/playground that provides interactive exploration and testing capabilities.
The Data Explorer allows you to browse and search all entities in your playground state:
- Users: Browse all users with their profiles, metrics, and details
- Posts (formerly Tweets): View all posts with text, author information, and metrics
- Lists: Explore lists with members and descriptions
- Relationships: View user interactions including:
- Bookmarks
- Likes
- Following/Followers
- Reposts (formerly Retweets)
- Muting
- Blocking
- List Memberships
- Followed Lists
- Pinned Lists
- Spaces: Browse spaces and their details
- Media: View media attachments
- DM Conversations: Explore direct message conversations
- Stream Rules: View search stream rules
- Communities: Browse communities
The Data Explorer supports advanced search operators for filtering relationships and other entities:
For Relationships:
user:usernameorusername:username- Filter by user (username or name)post:idortweet:id- Filter by post/tweet IDlist:id- Filter by list IDtype:bookmark- Filter by relationship type (bookmark, like, following, follower, retweet, mute, block, list_member, followed_list, pinned_list)id:value- Filter by relationship ID
For Posts/Tweets:
user:usernameorusername:username- Filter by author (username or name)post:idortweet:id- Filter by post/tweet IDid:value- Filter by tweet ID
For Lists:
list:id- Filter by list IDid:value- Filter by list ID
General:
id:value- Filter by entity ID (works for all entity types)
You can combine multiple operators and add free text search. For example:
user:john type:like- Find all likes by user "john"post:123- Find relationships involving post ID "123"type:bookmark user:alice- Find bookmarks by user "alice"
The Relationships view displays user interactions in a clear, readable format:
- Each relationship shows the initiating user and the target entity (user, post, or list)
- Relationships are displayed with action verbs (e.g., "User A followed User B", "User A liked Post 123")
- You can filter by relationship type using the dropdown
- Search operators allow precise filtering of relationships
All entity views support pagination:
- Navigate through results using Previous/Next buttons
- Pagination works correctly with search filters
- Page information shows current page and total pages
- Use Field Selection: Request only needed fields to reduce payload size
- Use Expansions: Reduce API calls by requesting related objects
- Use Pagination: Request reasonable page sizes (10-100 items)
- Cache Responses: Cache responses when appropriate
- Batch Requests: Use batch endpoints when available (e.g.,
/2/users?ids=...)
- Always Check Status Codes: Don't assume requests succeed
- Handle Errors Gracefully: Parse error responses and handle appropriately
- Validate Input: Validate data before sending requests
- Use Appropriate Field Selection: Request only needed fields
- Respect Rate Limits: Implement rate limit handling in production
- Log Requests: Log requests and responses for debugging
- Use HTTPS in Production: Always use HTTPS for real API calls
- Store Credentials Securely: Never commit API keys to version control
async function makeRequest(url, options) {
const response = await fetch(url, options);
if (response.status === 429) {
// Rate limited - wait for reset
const resetTime = parseInt(response.headers.get('x-rate-limit-reset'));
const waitTime = (resetTime * 1000) - Date.now();
await new Promise(resolve => setTimeout(resolve, waitTime));
return makeRequest(url, options); // Retry
}
return response;
}Problem: Port already in use
Solution:
# Find process using port 8080
lsof -ti:8080
# Kill the process
kill $(lsof -ti:8080)
# Or use a different port
playground start --port 3000Problem: Failed to load OpenAPI spec
Solution:
# Refresh the cache
playground refresh
# Or restart with refresh
playground start --refreshProblem: State resets on restart
Checklist:
- Verify persistence is enabled in
~/.playground/config.json - Check file path is writable:
touch ~/.playground/state.json - Check server logs for persistence errors
- Verify
auto_saveis enabled
Solution:
{
"persistence": {
"enabled": true,
"file_path": "~/.playground/state.json",
"auto_save": true,
"save_interval": 60
}
}Problem: Getting 401 Unauthorized
Solution:
- Add
Authorizationheader:Authorization: Bearer test - Or disable validation in config (testing only):
{
"auth": {
"disable_validation": true
}
}Problem: Getting 429 Too Many Requests
Solution:
- Wait for rate limit window to reset
- Check rate limit status:
curl http://localhost:8080/rate-limits - Disable rate limiting in config (testing only):
{
"rate_limit": {
"enabled": false
}
}Problem: Getting 404 Not Found
Checklist:
- Verify endpoint path is correct
- Check server logs for OpenAPI spec loading
- Refresh OpenAPI spec:
playground refresh - Verify endpoint exists in X API v2
Problem: State file is corrupted
Solution:
# Reset state
curl -X POST http://localhost:8080/state/reset
# Or delete state file and restart
rm ~/.playground/state.json
playground startProblem: Getting 400 Bad Request with validation errors
Solution:
- Check error response for specific parameter issues
- Verify request body matches OpenAPI schema
- Check query parameters are valid
- Verify field names are correct
- Check expansion names are valid
Problem: Requests are slow
Solutions:
- Use field selection to reduce payload size
- Reduce
max_resultsif requesting large datasets - Check server logs for errors
- Verify OpenAPI cache is not stale
- Check system resources (CPU, memory)
The X URL Playground provides a complete local simulation of the X API for testing and development. Key features:
- ✅ Complete API Coverage: All X API v2 endpoints supported
- ✅ Stateful Operations: Realistic state management
- ✅ State Persistence: Optional file-based persistence
- ✅ Request Validation: Validates requests like the real API
- ✅ Error Formatting: Matches real API error formats
- ✅ Rate Limiting: Configurable rate limit simulation
- ✅ OpenAPI-Driven: Uses official X API OpenAPI spec
Start Server: playground start
Check Status: playground status
Refresh Spec: playground refresh
Base URL: http://localhost:8080
Config File: ~/.playground/config.json
State File: ~/.playground/state.json
Repository: https://github.com/xdevplatform/playground
Install Latest: go install github.com/xdevplatform/playground/cmd/playground@latest
- Check server logs for detailed error messages
- Export state for debugging:
curl http://localhost:8080/state/export - Check health:
curl http://localhost:8080/health - View endpoints:
curl http://localhost:8080/endpoints - Report issues: https://github.com/xdevplatform/playground/issues
- View releases: https://github.com/xdevplatform/playground/releases
Happy testing! 🚀