Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  • GET requests for security challenge checks
  • POST requests 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:

Finding your consumer secret in the X Developer Portal

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

  1. X sends a challenge: GET request with crc_token parameter
  2. You create an HMAC hash: Using SHA-256 with your consumer secret and the token
  3. You respond: With {"response_token": "sha256=<base64_hash>"}
  4. 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:

  1. Parses the JSON payload from the request body
  2. Logs the event data for debugging and processing
  3. Returns 200 OK to acknowledge successful receipt
  4. 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:

  1. Set your consumer secret:

    export CONSUMER_SECRET="your_consumer_secret_here"
    
  2. Run the app:

    python sample_app.py
    

Next Steps

Your webhook app is now complete! To use it:

  1. Host it publicly with HTTPS (required for webhooks)
  2. Register the URL in your X Developer Portal
  3. Configure your webhook to listen for specific events
  4. 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 xurl CLI tool set up. If you haven’t installed and configured xurl yet, 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:

MethodRouteDescription
GET/2/webhooksList all registered webhooks for your account
POST/2/webhooksCreate a webhook (see below for JSON body)
DELETE/2/webhooks/:webhook_idDelete a webhook by ID
PUT/2/webhooks/:webhook_idManually trigger a security check to re-validate a webhook
POST/2/webhooks/replayInitiate a replay job (See 2.4)

Note: All sample commands in this section use the xurl CLI tool. If you haven’t set up xurl yet, 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:

MethodRouteDescription
POST/2/webhooks/replayInitiate 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

MethodRouteDescription
GET/2/activity/streamConnect to the activity stream (persistent HTTP)
GET/2/activity/subscriptionsList all active subscriptions
POST/2/activity/subscriptionsCreate a new subscription
DELETE/2/activity/subscriptions/:subscription_idDelete a subscription by ID
PUT/2/activity/subscriptions/:subscription_idUpdate 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 typeDescriptionPrivacyFilter
profile.update.bioUser updates their profile bioPublicuser_id
profile.update.profile_pictureUser updates their profile picturePublicuser_id
profile.update.banner_pictureUser updates their profile bannerPublicuser_id
profile.update.screennameUser updates their display namePublicuser_id
profile.update.handleUser updates their handlePublicuser_id
profile.update.geoUser updates their profile locationPublicuser_id
profile.update.urlUser updates their profile website URLPublicuser_id
profile.update.verified_badgeUser updates their verified badgePublicuser_id
profile.update.affiliate_badgeUser updates their affiliate badgePublicuser_id

Follow events

Event typeDescriptionPrivacyFilter
follow.followUser follows another userMultiuser_id, optional direction
follow.unfollowUser unfollows another userMultiuser_id, optional direction

Direction filter: For follow.follow and follow.unfollow, the filter may include direction: "inbound" or "outbound" alongside user_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 typeDescriptionPrivacyFilter
spaces.startUser starts a SpacePublicuser_id
spaces.endUser ends a SpacePublicuser_id

Legacy DM events

Unencrypted, legacy direct messages.

Event typeDescriptionPrivacyFilter
dm.receivedUser receives an unencrypted DMPrivateuser_id
dm.sentUser sends an unencrypted DMPrivateuser_id
dm.readUser reads a DM or read receipt for the filtered userPrivateuser_id
dm.indicate_typingUser is typing a message to the filtered userPrivateuser_id

Chat events (encrypted / XChat)

Event typeDescriptionPrivacyFilter
chat.receivedUser receives an encrypted direct messagePrivateuser_id
chat.sentUser sends an encrypted direct messagePrivateuser_id
chat.conversation_joinUser joins an encrypted chat conversationPrivateuser_id
chat.indicate_typingUser is typing in an encrypted chatPrivateuser_id
chat.readUser read state in encrypted chatPrivateuser_id

The last two rows are in development; availability and payload fields may change—watch X developer docs.

News events

Event typeDescriptionPrivacyFilter
news.newNew Grok-curated trends and headlinesPublickeyword

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 tierMaximum subscriptions
Self-serve1,000
Enterprise50,000
Partner100,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 eventsPrivate events
Relationship to your appData X treats as already public; your app does not need that user’s personal OAuth grantData X treats as private; that user must explicitly authorize your app before you can subscribe
OAuth modelOAuth 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 ruleFilter by user_id (or keyword for news.new) per docsFilter 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 a profile.update.bio subscription, and inspect webhook payloads.
  • Subscribing to private events — configure user OAuth and call the subscription API with xurl --auth oauth2 or xurl --auth oauth1 for private event types.
  • News by keywordnews.new with a keyword filter (Enterprise / Partner).
  • Direction filterdirection (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 matched
  • event_type: Which event fired
  • tag: The tag you set when creating the subscription
  • payload: Change-specific data (here, before/after bio text)

List subscriptions again to confirm:

xurl --auth app /2/activity/subscriptions

Next steps

  1. Handle multiple event types as needed for your product
  2. Handle delivery failures and retries responsibly
  3. Monitor subscription health and rotate credentials on schedule
  4. 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

  1. An X app with XAA access and a registered webhook (same as the public flow).
  2. 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.
  3. User tokens registered in xurl for --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 Keywordnews.new is public and typically uses --auth app (Enterprise / Partner); it is a separate tutorial because the filter is a keyword, not user_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 by user_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.