Mike's X Developer API Blog
"How about 'Hook into what's happening', y'know for our slogan!"
"Mike that's awful"
"Yeah..."
Hey everyone, thanks for checking out my tutorial.
My name is Mike Rosinsky. I'm a software engineer on the X Developer Platform team, and I wanted to make a blog-style page to showcase some fun demos and other things relating to the API.
This doesn't intend to serve as a replacement for docs, but maybe can give some good insight into what you can create with the APIs that X offers.
xURL - cURL for X
xURL is a CLI tool specifically design for interfacing with the X API.
Some key features include:
- URL shortening
- Authentication handling
- Webhook testing via ngrok
You can check out the full project, including the README here: https://github.com/xdevplatform/xurl
Quick Start
Install the tool with one curl command:
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | sudo bash
Register your auth token:
xurl auth app --bearer-token YOUR_BEARER_TOKEN
and make requests:
xurl --auth app /2/tweets/20
Check out the xurl github README for further tutorials including using OAuth1 + 2, user auth, and other features.
I'll be using xurl in all sample commands in follow-on sections in this blog.
Using Webhooks with the X Developer API
☎️ Don't call us, we'll call you!
What's a Webhook?
Webhooks are an efficient way to receive real-time data from the X Developer API. It's an industry standard practice, and a growing number of X's products currently support webhooks.
Instead of polling the API servers to check if any new data is available on a regular interval, the API will reach out to you automatically!
You register your server's callback URL with X, and when new data becomes available that matches your criteria, the API will reach out to your server, deliver the event, and disconnect.
This is all secured using industry standard encryption, making it an ideal choice for efficient, event-driven data delivery.
A growing number of X's real-time products support webhook delivery. These products include:
- Filtered Stream
- Account Activity API
- X Activity API
Writing a Basic Webhook App
In this section, we'll write a basic app to get started with receiving webhook events from X.
Here's the requests that the app will need to handle:
GETrequests for security challenge checksPOSTrequests for receiving events
After we make the app, we'll need to host it with a public-facing HTTPS URL. I'll show you some options for quick, free methods for getting this stood up quickly so you can test.
The Basic App
Let's start with a basic skeleton for our webhook app. We'll build it incrementally, adding features step by step.
from flask import Flask
from waitress import serve
HOST = "0.0.0.0"
PORT = 8080
app = Flask(__name__)
@app.route('/webhooks', methods=['GET', 'POST'])
def webhook_request():
if request.method == 'GET':
# Stub - we'll implement the security check here
print("Got GET request")
return '', 200
elif request.method == 'POST':
# Stub - we'll implement webhook event handling here
print("Got POST request")
return '', 200
# Got an invalid method
return 'Method Not Allowed', 405
def main():
print(f"Hosting WSGI server on {HOST}:{PORT}")
serve(app, host=HOST, port=PORT)
if __name__=='__main__':
main()
This gives us a basic Flask app with stub endpoints for both GET and POST requests. Now let's implement each part step by step.
Implementing the Security Check (GET Request)
X uses a Challenge-Response Check (CRC) to verify that your webhook endpoint is legitimate and secure. When you register a webhook URL, X will send a GET request with a crc_token parameter. Your app must respond with an HMAC SHA-256 hash of that token using your consumer secret.
Getting Your Consumer Secret
To get your consumer secret, go to your X Developer Portal and navigate to your app's settings. You'll find the Consumer Keys section where you can view and regenerate your API keys:

Copy the "API Key Secret" (also called Consumer Secret) and set it as an environment variable before running your app.
Why This Matters
The CRC check ensures that:
- Your endpoint is accessible and responding
- Only you (with your consumer secret) can validate the webhook
- The connection between X and your server is secure
Implementation
First, let's add the necessary imports and environment variable setup:
from flask import Flask, request, jsonify
from waitress import serve
import base64
import hashlib
import hmac
import os
import json
import sys
app = Flask(__name__)
# Your Twitter consumer secret - set this as an environment variable
CONSUMER_SECRET = os.environ.get("CONSUMER_SECRET")
if CONSUMER_SECRET is None:
print("Missing consumer secret. Ensure CONSUMER_SECRET env var is set.")
sys.exit(1)
HOST = "0.0.0.0"
PORT = 8080
Now let's implement the GET request handler:
@app.route('/webhooks', methods=['GET', 'POST'])
def webhook_request():
# Handle GET request (CRC challenge)
if request.method == 'GET':
crc_token = request.args.get('crc_token')
print(f"CRC Token received: {crc_token}")
if crc_token is None:
print("Error: No crc_token found in the request.")
return json.dumps({'error': 'No crc_token'})
# Creates HMAC SHA-256 hash from incoming token and your consumer secret
sha256_hash_digest = hmac.new(
CONSUMER_SECRET.encode('utf-8'),
msg=crc_token.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Construct response data with base64 encoded hash
response = {
'response_token': 'sha256=' + base64.b64encode(sha256_hash_digest).decode('utf-8')
}
# Returns properly formatted json response
return jsonify(response)
elif request.method == 'POST':
# Stub - we'll implement webhook event handling here
print("Got POST request")
return 'Not implemented yet', 501
# Got an invalid method
return 'Method Not Allowed', 405
How the CRC Check Works
- X sends a challenge: GET request with
crc_tokenparameter - You create an HMAC hash: Using SHA-256 with your consumer secret and the token
- You respond: With
{"response_token": "sha256=<base64_hash>"} - X verifies: Compares your hash with what they expect
Implementing Webhook Events (POST Request)
Now let's implement the POST request handler to receive actual webhook events from X.
Replace the POST stub with this implementation:
@app.route('/webhooks', methods=['GET', 'POST'])
def webhook_request():
# Handle GET request (CRC challenge)
if request.method == 'GET':
# Truncated...
# Handle POST request (Webhook event)
elif request.method == 'POST':
# Use the json library to render and dump the data.
event_data = request.get_json()
if event_data:
print(json.dumps(event_data, indent=2))
else:
# Log if the request body wasn't JSON or was empty
print(f"Body: {request.data.decode('utf-8')}")
# Return 200 OK to acknowledge receipt
return '', 200
# Got an invalid method
return 'Method Not Allowed', 405
Processing Events
The POST handler:
- Parses the JSON payload from the request body
- Logs the event data for debugging and processing
- Returns 200 OK to acknowledge successful receipt
- Handles edge cases like non-JSON payloads
Running the Complete App
Now that we have both GET and POST implemented, let's add a proper main function:
def main():
print("--- Starting Webhook App ---")
print(f"Using CONSUMER_SECRET from environment variable.")
print(f"Running with Waitress WSGI server on {HOST}:{PORT}")
serve(app, host=HOST, port=PORT)
if __name__ == '__main__':
main()
To run the app:
-
Set your consumer secret:
export CONSUMER_SECRET="your_consumer_secret_here" -
Run the app:
python sample_app.py
Next Steps
Your webhook app is now complete! To use it:
- Host it publicly with HTTPS (required for webhooks)
- Register the URL in your X Developer Portal
- Configure your webhook to listen for specific events
- Test with real events as they occur
The app will now properly handle both the security challenge and incoming webhook events from X.
The Complete Code
Here's the complete, working webhook app that combines everything we've built:
from flask import Flask, request, jsonify
from waitress import serve
import base64
import hashlib
import hmac
import os
import json
import sys
app = Flask(__name__)
# Your Twitter consumer secret - set this as an environment variable
CONSUMER_SECRET = os.environ.get("CONSUMER_SECRET")
if CONSUMER_SECRET is None:
print("Missing consumer secret. Ensure CONSUMER_SECRET env var is set.")
sys.exit(1)
HOST = "0.0.0.0"
PORT = 8080
@app.route('/webhooks', methods=['GET', 'POST'])
def webhook_request():
# Handle GET request (CRC challenge)
if request.method == 'GET':
crc_token = request.args.get('crc_token')
print(f"CRC Token received: {crc_token}")
if crc_token is None:
print("Error: No crc_token found in the request.")
return json.dumps({'error': 'No crc_token'})
# Creates HMAC SHA-256 hash from incoming token and your consumer secret
sha256_hash_digest = hmac.new(
CONSUMER_SECRET.encode('utf-8'),
msg=crc_token.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Construct response data with base64 encoded hash
response = {
'response_token': 'sha256=' + base64.b64encode(sha256_hash_digest).decode('utf-8')
}
# Returns properly formatted json response
return jsonify(response)
# Handle POST request (Webhook event)
elif request.method == 'POST':
# Use the json library to render and dump the data.
event_data = request.get_json()
if event_data:
print(json.dumps(event_data, indent=2))
else:
# Log if the request body wasn't JSON or was empty
print(f"Body: {request.data.decode('utf-8')}")
# Return 200 OK to acknowledge receipt
return '', 200
# Got an invalid method
return 'Method Not Allowed', 405
def main():
print("--- Starting Webhook App ---")
print(f"Using CONSUMER_SECRET from environment variable.")
print(f"Running with Waitress WSGI server on {HOST}:{PORT}")
serve(app, host=HOST, port=PORT)
if __name__ == '__main__':
main()
Next Steps
Now that the server is running locally, you'll need to host it to generate a publicly-available HTTPS URL.
This can be done for free using public tools like ngrok, deployed to a cloud environment, or any other means you wish.
In the next section, we'll register the URL with X so we can start receiving events.
Standing up a Temporary Webhook using xURL
Note: This section assumes you have the
xurlCLI tool set up. If you haven't installed and configuredxurlyet, check out the xURL section first.
If you're looking for a quick and easy way to stand up a webhook to use with the X API, the xURL tool offers this functionality.
The xURL webhook abstracts away the need to write your own handlers for the GET and POST requests that X sends to your server. If you're looking for that logic to incorporate into your production app, check out the next section where we write a Python Flask app.
Quick Start
Prerequisites
What you'll need:
- The xURL CLI tool (already installed and configured)
- A free (or higher) account with
ngrok.com - Your ngrok auth token
Setup Steps
1. Register Your OAuth1 Keys
First, ensure your OAuth1 keys are registered with xurl:
xurl auth oauth1
You'll need to provide your consumer key and secret, and access key and secret. Generate these from the X Developer Portal.
2. Start the Webhook Server
Run the xURL webhook command:
xurl webhook start
You'll be prompted for your ngrok auth token so that xURL can create a temporary HTTPS endpoint:
Starting webhook server with ngrok...
Enter your ngrok authtoken (leave empty to try NGROK_AUTHTOKEN env var):
Once you've entered your auth token, your endpoint will be generated and you'll see output like this:
Configuring ngrok to forward to local port: 8080
Ngrok tunnel established!
Forwarding URL: https://d1cb5181df5c.ngrok.app -> localhost:8080
Use this URL for your X API webhook registration: https://d1cb5181df5c.ngrok.app/webhook
Starting local HTTP server to handle requests from ngrok tunnel (forwarded from https://d1cb5181df5c.ngrok.app)...
Using Your Webhook
Leave the terminal window running - any webhook events, including the security check, will be displayed in that window.
To stop the webhook server: Press Ctrl+C in the terminal.
You can now use the generated URL (ending in /webhook) to register a webhook with the X API. See the next section for a tutorial on webhook registration.
Registering a Webhook With X
Registering a webhook involves passing the HTTPS URL for your webhook app to X's servers, so they know where to send events to.
The official docs are here: https://docs.x.com/x-api/webhooks/introduction
Routes
The X API offers the following endpoints to manage your registered webhooks:
| Method | Route | Description |
|---|---|---|
GET | /2/webhooks | List all registered webhooks for your account |
POST | /2/webhooks | Create a webhook (see below for JSON body) |
DELETE | /2/webhooks/:webhook_id | Delete a webhook by ID |
PUT | /2/webhooks/:webhook_id | Manually trigger a security check to re-validate a webhook |
POST | /2/webhooks/replay | Initiate a replay job (See 2.4) |
Note: All sample commands in this section use the
xurlCLI tool. If you haven't set upxurlyet, check out the xURL section first.
Register a Webhook
To register the webhook, you'll use the POST route, along with the following JSON body:
{
"url": "<YOUR WEBHOOK HTTPS URL>"
}
We can use xurl to test this:
xurl --auth app /2/webhooks -X POST -d '{"url": "<YOUR WEBHOOK HTTPS URL>"}'
When this request is sent, X will send a GET request to the provided URL to validate that the webhook belongs to you.
This is the security check we demonstrated in the last section.
On successful validation, the webhook will be created:
"data": {
"created_at": "2025-10-15T20:53:05.000Z",
"id": "1146654567674912769",
"url": "<YOUR WEBHOOK HTTPS URL>",
"valid": true
}
List your Webhooks
Now we can call the GET route to see our registered webhooks:
xurl --auth app /2/webhooks
{
"data":[
{
"created_at":"2025-10-15T20:53:05.000Z",
"id":"1146654567674912769",
"url":"<YOUR WEBHOOK HTTPS URL>",
"valid":true
}
],
"meta":{
"result_count":1
}
}
Re-validating your webhook
At regular intervals (~24hrs), X will attempt to validate that your webhook is still operational.
X does this by sending the GET CRC check again.
If for some reason this validation fails, the webhook will be marked as "valid":false and will no longer receive events.
To re-validate the webhook, you can call the PUT request, which will manually trigger the re-validation process:
xurl --auth app /2/webhooks/1146654567674912769 -X PUT
Providing the validation was successful, our webhook will be re-validated:
{
"data":{
"valid":true
}
}
Replaying Webhook Events
The X webhook suite allows you to replay past events that were either successfully or unsuccessfully delivered to your endpoint.
This is useful for recovering missed events due to downtime.
You can replay up to 24 hours of events (due to data retention restrictions). Note that this functionality is restricted to certain packages.
Route
To initiate replay, you'll use a POST endpoint:
| Method | Route | Description |
|---|---|---|
POST | /2/webhooks/replay | Initiate a replay job |
To use this endpoint, you'll specify the following fields in the JSON body of the request:
{
"webhook_id": "<YOUR WEBHOOK ID>",
"from_date": "YYYYMMDDHHMM",
"to_date": "YYYYMMDDHHMM",
}
All fields are required. The from_date must be within 24 hours, and the to_date must be after the from_date. Both dates must not be in the future.
Note: Both dates must be in UTC time
What is delivered in replay?
Any event, regardless of product (Filtered Stream, AAA, XAA), that is delivered via webhook, either successfully or unsuccessfully, will be attempted to be re-delivered during replay.
Example
You'll check your active webhook IDs using the GET endpoint:
xurl --auth app /2/webhooks
{
"data":[
{
"created_at":"2025-10-15T20:53:05.000Z",
"id":"1146654567674912769",
"url":"<YOUR WEBHOOK HTTPS URL>",
"valid":true
}
],
"meta":{
"result_count":1
}
}
Then you can start a replay job for the past hour of data using your webhook ID.
Keep in mind that the from_date and to_date are in UTC. So if the time right now is 1200UTC on Dec 1, 2025 you can ask for the past hour like so:
xurl --auth app /2/webhooks/replay -X POST -d '{
"webhook_id": "1146654567674912769",
"from_date": "202512011100",
"to_date": "202512011200",
}'
And if successful, you'll see this response:
{
"data":{
"job_id":"1995507200941260800",
"created_at":"2025-12-01T12:00:07.000Z"
}
}
Near instantly, you should see events start to be delivered to your webhook, if any events were attempted to be delivered in that time frame.
The events delivered as part of a replay job will have the special tag in the meta field:
{
"data": {
...
},
"meta": {
"replay": true
}
}
This allows your app to differentiate when an event is from replay vs first-time delivery, since normal events may be delivered at the same time that replay events are coming through.
Replay job completion
When the replay job completes, you'll see the following event be delivered to your webhook:
{
"replay_job_status": {
"webhook_id":"1146654567674912769",
"job_state": "Complete",
"job_state_description": "Job completed successfully",
"job_id": "1995507200941260800"
}
}
Note: The replay completion event may arrive out-of-order due to real-world network latencies between X's proxies that distribute events.
The X Activity API
In this chapter, we'll use the webhook we set up in the previous chapter in conjunction with X's newest real-time data suite, the X Activity API.
About
The X Activity API allows us to subscribe to event types and apply filters. When a matching event is created, it will be delivered to our app.
XAA offers both webhook and streaming support, but we'll be using the webhook option in this example.
Routes
To manage our active subscriptions in XAA, we're offered the following routes:
| Method | Route | Description |
|---|---|---|
GET | /2/activity/subscriptions | List all active subscriptions |
POST | /2/activity/subscriptions | Create a new subscription (see below for JSON body) |
DELETE | /2/activity/subscriptions/:subscription_id | Delete a subscription by ID |
PUT | /2/activity/subscriptions/:subscription_id | Update a subscription (see below for JSON body) |
Note: All sample commands in this section use the
xurlCLI tool. If you haven't set upxurlyet, check out the xURL section first.
Let's run the GET route first just to ensure the route is working and our account has access.
You'll use your bearer token for authentication for all routes:
xurl --auth app /2/activity/subscriptions
{
"data":[
],
"meta": {
"total_subscriptions": 0
}
}
Looks like everything's working. We currently have no active subscriptions.
Getting Your User ID
Before creating a subscription, we need to know our user ID. The X Activity API requires user IDs (not usernames) in the filter criteria.
Let's get our user ID using the User Lookup endpoint:
xurl --auth app /2/users/by/username/YOUR_USERNAME
Replace YOUR_USERNAME with your actual X username (without the @ symbol).
Example response:
{
"data": {
"id": "1234567890",
"name": "Your Name",
"username": "your_username"
}
}
Copy the id value from the response - you'll need it for creating subscriptions.
Creating a Subscription
Now let's create our first subscription. We'll subscribe to our own profile update events first, so we can test for an event coming through.
Subscription Parameters
When creating a subscription, you need to specify:
event_type: The type of event to subscribe to (required)filter: Criteria to filter which events to receive (required)webhook_id: The ID of your registered webhook where events will be delivered (optional, but required for webhook delivery)tag: An optional identifier for your subscription (recommended)
Available Event Types
XAA supports several event types:
ProfileBioUpdate- When a user updates their bioProfilePictureUpdate- When a user changes their profile pictureProfileBannerPictureUpdate- When a user changes their bannerProfileScreennameUpdate- When a user changes their usernameProfileGeoUpdate- When a user updates their locationProfileUrlUpdate- When a user updates their website URL
Creating the Subscription
Let's create a subscription to monitor our own profile bio updates:
xurl --auth app /2/activity/subscriptions -X POST -d '{
"event_type": "ProfileBioUpdate",
"filter": {
"user_id": "YOUR_USER_ID"
},
"webhook_id": "YOUR_WEBHOOK_ID",
"tag": "my bio updates"
}'
Success Response
If successful, you'll receive a response like this:
{
"data": {
"subscription": {
"created_at": "2025-10-07T05:31:56Z",
"event_type": "ProfileBioUpdate",
"filter": {
"user_id": "YOUR_USER_ID"
},
"subscription_id": "1146654567674912769",
"tag": "my bio updates",
"updated_at": "2025-10-07T05:31:56Z",
"webhook_id": "YOUR_WEBHOOK_ID"
}
},
"meta": {
"total_subscriptions": 1
}
}
Testing Your Subscription
Once you've created the subscription, any bio updates to your own profile will be delivered to your webhook endpoint. You should see events arrive in your webhook server logs.
To test: Update your bio on X and watch for the event to arrive at your webhook!
Sample Event Payload
When a profile bio update occurs, you'll receive a webhook event like this:
{
"data": {
"filter": {
"user_id": "YOUR_USER_ID"
},
"event_type": "ProfileBioUpdate",
"tag": "my bio updates",
"payload": {
"before": "vox populi",
"after": "vox dei"
}
}
}
The event includes:
filter: The filter criteria that triggered this eventevent_type: The type of event that occurredtag: The tag you assigned to the subscriptionpayload: The actual change data (before/after values)
You can also list your active subscriptions again to confirm:
xurl --auth app /2/activity/subscriptions
This will now show your active subscription in the response.
Next Steps
Your XAA subscription is now active! Events will be delivered to your webhook as they occur. In production applications, you'll want to:
- Handle multiple event types based on your use case
- Implement proper error handling for failed deliveries
- Monitor subscription health and revalidate when needed
- Clean up unused subscriptions to avoid hitting rate limits
The X Activity API provides powerful real-time monitoring capabilities when combined with webhooks!
Getting New Trends By Keyword in Realtime
This chapter is a brief tutorial on how to use the X Activity API to get new trends emerging on X in realtime, filtered by keywords of your choice.
Note: This feature is currently only for Enterprise customers
Subscribing to Trends
You'll create a subscription to trends using the X Activity API in the same way you did in the last chapter.
This time, you'll use the TrendsNew event type, and you'll specify a keyword in the filter field instead of a user id:
xurl --auth app /2/activity/subscriptions -X POST -d '{
"event_type": "TrendsNew",
"filter": {
"keyword": "Tesla"
},
"tag": "Tesla trends",
"webhook_id": "YOUR_WEBHOOK_ID"
}'
Success Response
Upon success, you'll receive this response:
{
"data": {
"subscription": {
"created_at": "2025-10-07T05:31:56Z",
"event_type": "TrendsNew",
"filter": {
"keyword": "Tesla"
},
"subscription_id": "1146654567674912769",
"tag": "Tesla trends",
"updated_at": "2025-10-07T05:31:56Z",
"webhook_id": "YOUR_WEBHOOK_ID"
}
},
"meta": {
"total_subscriptions": 1
}
}
Getting Trend Events
If you specified a webhook, then you'll start to see trends with a headline containing your keyword right away (as they are generated of course, so it may be a minute until a trend containing your keyword is seen).
Trend events have this form:
{
"data": {
"event_uuid": "1985729017958244577",
"filter": {
"keyword": "news"
},
"event_type": "TrendsNew",
"tag": "news trends",
"payload": {
"headline": "This is some news breaking on X",
"summary": "Some new news broke on X today, which is an awesome place to go for breaking news!"
}
}
}
As the X Activity API matures, more fields will be added to the payload.
Duplicate Trends
If you specified multiple subscriptions for trends, and a single trend event matches multiple subscriptions, it may be delivered multiple times.
Here's an example:
{
"data": {
"event_uuid": "1985729017958244577",
"filter": {
"keyword": "jack"
},
"event_type": "TrendsNew",
"tag": "jack trends",
"payload": {
"headline": "New Jersey Gubernatorial Election Between Jack Ciattarelli and Mikie Sherrill",
"summary": "Voters in New Jersey head to the polls to elect the state's next governor in a contest between Republican Jack Ciattarelli and Democratic incumbent Mikie Sherrill. Polls are open from 6 a.m. to 8 p.m. across the state."
}
}
}
{
"data": {
"event_uuid": "1985729017958244577",
"filter": {
"keyword": "mikie"
},
"event_type": "TrendsNew",
"tag": "mikie trends",
"payload": {
"headline": "New Jersey Gubernatorial Election Between Jack Ciattarelli and Mikie Sherrill",
"summary": "Voters in New Jersey head to the polls to elect the state's next governor in a contest between Republican Jack Ciattarelli and Democratic incumbent Mikie Sherrill. Polls are open from 6 a.m. to 8 p.m. across the state."
}
}
}
You can see there are two separate subscriptions, one on the keyword jack and one on mikie. The headline in this trend contained both keywords, so it was delivered to the webhook twice.
X does some deduplication on its end, but you can use the event_uuid to do deduplication at the application layer as well.
Use Cases
Setting up a trends feed using the X Activity API is a perfect way to integrate a live news feed of events you care about into your application!