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 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:

  • 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

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
  }
}

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:

MethodRouteDescription
GET/2/activity/subscriptionsList all active subscriptions
POST/2/activity/subscriptionsCreate a new subscription (see below for JSON body)
DELETE/2/activity/subscriptions/:subscription_idDelete a subscription by ID
PUT/2/activity/subscriptions/:subscription_idUpdate a subscription (see below for JSON body)

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.

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 bio
  • ProfilePictureUpdate - When a user changes their profile picture
  • ProfileBannerPictureUpdate - When a user changes their banner
  • ProfileScreennameUpdate - When a user changes their username
  • ProfileGeoUpdate - When a user updates their location
  • ProfileUrlUpdate - 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": "Dev Platform @X",
      "after": "Dev Platform @X a"
    }
  }
}

The event includes:

  • filter: The filter criteria that triggered this event
  • event_type: The type of event that occurred
  • tag: The tag you assigned to the subscription
  • payload: 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:

  1. Handle multiple event types based on your use case
  2. Implement proper error handling for failed deliveries
  3. Monitor subscription health and revalidate when needed
  4. Clean up unused subscriptions to avoid hitting rate limits

The X Activity API provides powerful real-time monitoring capabilities when combined with webhooks!