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. For user (non–app-only) requests, xurl selects the credential profile with --auth oauth2 (OAuth 2.0 user context) or --auth oauth1 (OAuth 1.0a user access), depending on how you registered tokens—not a separate --auth user flag.
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
This part of the book builds on the webhook you registered earlier. Here we use X’s X Activity API (XAA) to subscribe to activity events and receive them on your app in near real time.
About
The X Activity API lets you subscribe to event types with filters (for example a user_id or a keyword). When a matching event occurs, X delivers it to your integration—typically over a webhook or a persistent HTTP stream (GET /2/activity/stream). These docs focus on webhooks.
Note: XAA does not deliver posts. For real-time post delivery, use the Filtered Stream endpoint.
Public vs private for XAA is the same axis as app vs user auth: public streams use OAuth 2.0 app Bearer tokens; private streams require explicit user OAuth (OAuth 2.0 user context or OAuth 1.0a user access). The next chapter lists event types, subscription limits, and spells that model out in one place.
Routes
| Method | Route | Description |
|---|---|---|
GET | /2/activity/stream | Connect to the activity stream (persistent HTTP) |
GET | /2/activity/subscriptions | List all active subscriptions |
POST | /2/activity/subscriptions | Create a new subscription |
DELETE | /2/activity/subscriptions/:subscription_id | Delete a subscription by ID |
PUT | /2/activity/subscriptions/:subscription_id | Update a subscription |
Continue to Event types and authentication for the full event catalog and auth model, then Subscribing to public events for a hands-on walkthrough with xurl --auth app.
Event types and authentication
This page lists supported event types and explains how they line up with authentication. For XAA, whether an event is public or private is the same question as whether you may use app-only credentials or must use user credentials. The privacy class tells you which OAuth style X expects (See section below).
Supported event types
Most filters use user_id (numeric user ID). The exception is news.new, which uses a keyword filter.
The Privacy column is how X classifies the event for XAA: Public (app Bearer is the usual subscription path, subject to tier rules), Private (user OAuth required), or Multi (both shapes exist, for example a path that is public under Enterprise app auth and a path that is private under pay-per-user / user context, per product configuration).
Profile events
| Event type | Description | Privacy | Filter |
|---|---|---|---|
profile.update.bio | User updates their profile bio | Public | user_id |
profile.update.profile_picture | User updates their profile picture | Public | user_id |
profile.update.banner_picture | User updates their profile banner | Public | user_id |
profile.update.screenname | User updates their display name | Public | user_id |
profile.update.handle | User updates their handle | Public | user_id |
profile.update.geo | User updates their profile location | Public | user_id |
profile.update.url | User updates their profile website URL | Public | user_id |
profile.update.verified_badge | User updates their verified badge | Public | user_id |
profile.update.affiliate_badge | User updates their affiliate badge | Public | user_id |
Follow events
| Event type | Description | Privacy | Filter |
|---|---|---|---|
follow.follow | User follows another user | Multi | user_id, optional direction |
follow.unfollow | User unfollows another user | Multi | user_id, optional direction |
Direction filter: For
follow.followandfollow.unfollow, the filter may includedirection:"inbound"or"outbound"alongsideuser_id. Only follow events support this today. See Direction filter.
Multi here means XAA supports more than one auth posture: Some trusted packages may subscribe with application-only access (public-style entitlement for that tier), while pay-per-user flows treat the same event as private and require user OAuth for the filtered account. Confirm the matrix for your package on docs.x.com.
Spaces events
| Event type | Description | Privacy | Filter |
|---|---|---|---|
spaces.start | User starts a Space | Public | user_id |
spaces.end | User ends a Space | Public | user_id |
Legacy DM events
Unencrypted, legacy direct messages.
| Event type | Description | Privacy | Filter |
|---|---|---|---|
dm.received | User receives an unencrypted DM | Private | user_id |
dm.sent | User sends an unencrypted DM | Private | user_id |
dm.read | User reads a DM or read receipt for the filtered user | Private | user_id |
dm.indicate_typing | User is typing a message to the filtered user | Private | user_id |
Chat events (encrypted / XChat)
| Event type | Description | Privacy | Filter |
|---|---|---|---|
chat.received | User receives an encrypted direct message | Private | user_id |
chat.sent | User sends an encrypted direct message | Private | user_id |
chat.conversation_join | User joins an encrypted chat conversation | Private | user_id |
chat.indicate_typing | User is typing in an encrypted chat | Private | user_id |
chat.read | User read state in encrypted chat | Private | user_id |
The last two rows are in development; availability and payload fields may change—watch X developer docs.
News events
| Event type | Description | Privacy | Filter |
|---|---|---|---|
news.new | New Grok-curated trends and headlines | Public | keyword |
news.new is Enterprise and Partner tier only at this time.
XAA will add more event types over time (for example social interactions, content engagement, and monetization); check X developer documentation for the latest list.
Subscription limits
| Package tier | Maximum subscriptions |
|---|---|
| Self-serve | 1,000 |
| Enterprise | 50,000 |
| Partner | 100,000 |
Public vs private events = app vs user auth
X labels each XAA event type as public or private in line with the main app. That label is not a separate concept from authentication: it tells you whose OAuth grant must back your requests.
Private events are activities that are not universally visible on X (for example DMs and encrypted chat). Accessing them requires explicit user authorization: the relevant user must have signed in and approved your app, with the right permissions. In practice that means OAuth 2.0 user context (“3-legged” OAuth) or OAuth 1.0a user access—a token tied to that user, not just your app’s identity. You may only create subscriptions for private streams for users who have authorized your application, and you will use user-scoped credentials in xurl with --auth oauth2 (OAuth 2.0 user context) or --auth oauth1 (OAuth 1.0a user access), after registering the matching profile. See the xurl README for setup; scopes are defined in the current X developer docs.
Public events are activities that are already visible in public on the platform (for example many profile updates). Subscribing and receiving those streams is done with OAuth 2.0 application-only authentication: a Bearer token for your app from the developer portal—no end-user login for that token. In this book that is the xurl pattern xurl auth app --bearer-token … then --auth app (see the xURL chapter).
So: private ⟺ user OAuth (OAuth2 user context or OAuth1 user access); public ⟺ OAuth2 app Bearer for the subscription and delivery patterns X documents for XAA. Calling subscription management endpoints themselves still typically uses your app credentials; the distinction above is what X requires for the event family you target and for which users you may filter on for private data.
Caveat: Some event types can be accessed with either user OAuth or app-level OAuth, depending on how X exposes them for your product. Even then, application-only (Bearer) access is not always available on every package: for certain events—for example follow.follow / follow.unfollow—X may reserve app-only subscriptions to higher or “elevated” tiers, while lower tiers still work if the relevant user has authorized your app with user context. Treat the public/private split above as the privacy model; treat tier and contract as a separate gate for whether app auth is actually enabled for a given event_type on your developer account. Confirm both in the current XAA docs and your portal entitlements.
The event tables above mix both families (for example profile vs DM/chat). For the authoritative public/private list and any nuance, use X’s Event privacy and authentication section for XAA on docs.x.com.
| Public events | Private events | |
|---|---|---|
| Relationship to your app | Data X treats as already public; your app does not need that user’s personal OAuth grant | Data X treats as private; that user must explicitly authorize your app before you can subscribe |
| OAuth model | OAuth 2.0 app-only (Bearer token) | OAuth 2.0 user context or OAuth 1.0a user access (user explicitly grants your app) |
xurl | --auth app | --auth oauth2 or --auth oauth1 (after configuring user tokens) |
| Typical subscription rule | Filter by user_id (or keyword for news.new) per docs | Filter only for users who have authorized your app |
Next
- Subscribing to public events — verify access with
xurl --auth app, look up a user id, create aprofile.update.biosubscription, and inspect webhook payloads. - Subscribing to private events — configure user OAuth and call the subscription API with
xurl --auth oauth2orxurl --auth oauth1for private event types. - News by keyword —
news.newwith akeywordfilter (Enterprise / Partner). - Direction filter —
direction(inbound/outbound) for follow subscriptions.
Subscribing to public events
This walkthrough assumes you have completed the X Activity API introduction and read Event types and authentication. It uses a public event type (profile.update.bio) with OAuth 2.0 application-only credentials (xurl --auth app). Commands use the xurl CLI; if you have not installed it yet, follow the xURL chapter first.
For private event types (DMs, chat, and so on) you must use user OAuth instead—see Subscribing to private events.
Verify access
Confirm your app can call the subscriptions API:
xurl --auth app /2/activity/subscriptions
{
"data":[
],
"meta": {
"result_count": 0
}
}
If you see a JSON body like the above (possibly with existing subscriptions), your bearer token and project access are working. For GET /2/activity/subscriptions, meta.result_count is the number of subscription objects returned in data (not total_subscriptions).
Getting your user ID
Most public XAA filters use user_id (numeric user ID, not username). The exception is news.new, which uses a keyword filter—see News by Keyword.
Look up your user id with the User Lookup endpoint:
xurl --auth app /2/users/by/username/YOUR_USERNAME
Replace YOUR_USERNAME with your X handle without @.
Example response:
{
"data": {
"id": "1234567890",
"name": "Your Name",
"username": "your_username"
}
}
Copy the id value for the subscription below.
Creating a subscription
Subscribe to your own profile bio updates so you can trigger a test event.
Subscription payload
When creating a subscription, you typically send:
event_type: The event to subscribe to (required)filter: Match criteria (required)—often{ "user_id": "…" }webhook_id: Where to deliver events (optional, but required for webhook delivery)tag: Optional label for your own bookkeeping (recommended)
Create the subscription:
xurl --auth app /2/activity/subscriptions -X POST -d '{
"event_type": "profile.update.bio",
"filter": {
"user_id": "YOUR_USER_ID"
},
"webhook_id": "YOUR_WEBHOOK_ID",
"tag": "my bio updates"
}'
Success response
{
"data": {
"subscription": {
"created_at": "2025-10-07T05:31:56Z",
"event_type": "profile.update.bio",
"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
After the subscription is created, change your profile bio on X. Your webhook server should log a delivery.
Sample webhook payload
{
"data": {
"filter": {
"user_id": "YOUR_USER_ID"
},
"event_type": "profile.update.bio",
"tag": "my bio updates",
"payload": {
"before": "vox populi",
"after": "vox dei"
}
}
}
Fields:
filter: The subscription filter that matchedevent_type: Which event firedtag: The tag you set when creating the subscriptionpayload: Change-specific data (here, before/after bio text)
List subscriptions again to confirm:
xurl --auth app /2/activity/subscriptions
Next steps
- Handle multiple event types as needed for your product
- Handle delivery failures and retries responsibly
- Monitor subscription health and rotate credentials on schedule
- Delete unused subscriptions and stay under your tier cap (see Subscription limits)
Continue with Subscribing to private events for user-authenticated subscriptions, or News by Keyword for news.new and keyword filters.
Subscribing to private events
Private XAA event types (legacy DMs, encrypted chat, and others marked Private in Event types and authentication) require explicit user authorization: OAuth 2.0 user context or OAuth 1.0a user access for the user whose activity you are allowed to subscribe to. Application-only Bearer tokens are not sufficient for creating or receiving those subscriptions on behalf of that user.
This page shows the same subscription HTTP shape as Subscribing to public events, but with xurl --auth oauth2 or xurl --auth oauth1 so the request runs under a user credential that has granted your app the right scopes. App-only traffic continues to use --auth app (see the xURL chapter).
Prerequisites
- An X app with XAA access and a registered webhook (same as the public flow).
- A real user (often you, while testing) who signs in with X and approves your app, granting the permissions X requires for the private event family you care about.
- User tokens registered in
xurlfor--auth oauth2(OAuth 2.0 user context) or--auth oauth1(OAuth 1.0a user access), whichever you use.
The xurl README documents how to configure OAuth 2.0 user flows and OAuth 1.0a user access; follow that for your stack (PKCE / callback URL / token storage). Exact CLI subcommands and flags can change between xurl releases—use the README as source of truth. The xURL chapter covers installing the tool and app bearer registration; extend that setup with user credentials before continuing.
Who the user_id filter refers to
For private streams, the user_id in your filter must be a user who has authorized your app. In the common “subscribe to my own DMs or chat” case, that is the same user whose OAuth 2.0 or OAuth 1.0a profile you select with --auth oauth2 or --auth oauth1.
Example: subscribe with user auth
Below, the authenticated user is subscribing to their own incoming encrypted messages (chat.received). Replace placeholders with your webhook id, user id, and ensure your app has the chat/DM scopes X documents for XAA. The example uses OAuth 2.0 user context; if you use OAuth 1.0a user access instead, swap in --auth oauth1.
xurl --auth oauth2 /2/activity/subscriptions -X POST -d '{
"event_type": "chat.received",
"filter": {
"user_id": "AUTHORIZED_USER_ID"
},
"webhook_id": "YOUR_WEBHOOK_ID",
"tag": "my chat inbox"
}'
If your token or scopes are wrong, X returns an OAuth or permission error instead of a 200 subscription body—treat that as a configuration issue, not a generic “XAA down” failure.
Success response
The JSON shape matches the public case: data.subscription with subscription_id, event_type, filter, webhook_id, and tag.
{
"data": {
"subscription": {
"created_at": "2025-10-07T05:31:56Z",
"event_type": "chat.received",
"filter": {
"user_id": "AUTHORIZED_USER_ID"
},
"subscription_id": "1146654567674912769",
"tag": "my chat inbox",
"updated_at": "2025-10-07T05:31:56Z",
"webhook_id": "YOUR_WEBHOOK_ID"
}
},
"meta": {
"total_subscriptions": 1
}
}
Listing with user context
Listing with user auth
When you call GET /2/activity/subscriptions with the same user profile you used to create private subscriptions—for example xurl --auth oauth2 or xurl --auth oauth1—the response includes only the subscriptions that were created under that specific user’s authorization. You do not see every subscription visible to your app under --auth app.
That makes user-scoped GET useful for debugging or UI that should show “my subscriptions” for the signed-in account, without mixing in subscriptions that another user (or your app bearer) created.
xurl --auth oauth2 /2/activity/subscriptions
As with Subscribing to public events, list responses use meta.result_count for how many subscription objects appear in data.
Next
- News by Keyword —
news.newis public and typically uses--auth app(Enterprise / Partner); it is a separate tutorial because the filter is akeyword, notuser_id.
Getting News By Keyword in Realtime
This chapter is a brief tutorial on how to use the X Activity API to get news emerging on X in realtime, filtered by keywords of your choice.
Note: This feature is currently only for Enterprise customers
Subscribing to News
You create a subscription the same way as in Subscribing to public events: POST /2/activity/subscriptions with event_type, filter, and webhook_id. For when to use app vs user credentials, see Event types and authentication.
This time, you’ll use the news.new 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": "news.new",
"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": "news.new",
"filter": {
"keyword": "Tesla"
},
"subscription_id": "1146654567674912769",
"tag": "Tesla news",
"updated_at": "2025-10-07T05:31:56Z",
"webhook_id": "YOUR_WEBHOOK_ID"
}
},
"meta": {
"total_subscriptions": 1
}
}
Getting News Events
If you specified a webhook, then you’ll start to see news 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).
News events have this form:
{
"data": {
"event_uuid": "1985729017958244577",
"filter": {
"keyword": "spacex"
},
"event_type": "news.new",
"tag": "spacex news",
"payload": {
"headline": "This is some spacex news breaking on X",
"summary": "Spacex just launched a rocket"
}
}
}
As the X Activity API matures, more fields will be added to the payload.
Duplicate News
If you specified multiple subscriptions for news, and a single news event matches multiple subscriptions, it may be delivered multiple times.
Here’s an example:
{
"data": {
"event_uuid": "1985729017958244577",
"filter": {
"keyword": "jack"
},
"event_type": "news.new",
"tag": "jack news",
"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": "news.new",
"tag": "mikie news",
"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 news 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 news feed using the X Activity API is a perfect way to integrate a live feed of events you care about into your application!
Direction filter
Some XAA event types accept an optional direction field inside the subscription filter, next to user_id. It restricts events to which side of the relationship the filtered user is on (for example “someone followed them” vs “they followed someone else”).
Today, only follow events support this field: follow.follow and follow.unfollow. Other event_type values either ignore direction or reject the subscription—check the current X developer docs if you are unsure.
Values
"inbound"— activity where another account follows or unfollows the user identified byuser_id."outbound"— activity where that user follows or unfollows another account.
Example request
POST /2/activity/subscriptions
Body (use inbound or outbound):
{
"event_type": "follow.follow",
"filter": {
"user_id": "<id>",
"direction": "inbound"
}
}
For webhook delivery, include webhook_id (and optionally tag) the same way as in Subscribing to public events.
With xurl
xurl --auth app /2/activity/subscriptions -X POST -d '{
"event_type": "follow.follow",
"filter": {
"user_id": "YOUR_USER_ID",
"direction": "inbound"
},
"webhook_id": "YOUR_WEBHOOK_ID",
"tag": "follows inbound"
}'
Use "direction": "outbound" when you want the opposite direction. Follow subscriptions may require Enterprise (or other elevated) entitlements for app-only auth; see Event types and authentication.