x-developer
Version:
X (Twitter) data platform skill for AI coding agents. 100+ REST API endpoints, 2 MCP tools, 23 extraction types, HMAC webhooks.
214 lines (160 loc) • 6.12 kB
Markdown
Receive real-time event notifications at your HTTPS endpoints with HMAC-SHA256 signature verification.
1. Create at least 1 active monitor (`POST /monitors`)
2. Register a webhook endpoint (`POST /webhooks`)
3. Save the `secret` from the response (shown only once)
4. Build a handler that verifies signatures before processing
## Webhook Payload
Every delivery is a `POST` request to your URL with a JSON body:
```json
{
"eventType": "tweet.new",
"username": "elonmusk",
"data": {
"tweetId": "1893556789012345678",
"text": "Hello world",
"metrics": { "likes": 3200, "retweets": 890, "replies": 245 }
}
}
```
The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body).
```javascript
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
// This is the per-webhook secret from the POST /webhooks response, not a Xquik account credential
const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET;
const app = express();
function verifySignature(payload, signature, secret) {
if (typeof signature !== "string" || !secret) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
const expectedBuffer = Buffer.from(expected, "utf8");
const signatureBuffer = Buffer.from(signature, "utf8");
return (
expectedBuffer.length === signatureBuffer.length &&
timingSafeEqual(expectedBuffer, signatureBuffer)
);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-xquik-signature"];
const payload = req.body.toString();
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(payload);
switch (event.eventType) {
case "tweet.new":
console.log(`New tweet from @${event.username}: ${event.data.text}`);
break;
case "tweet.reply":
console.log(`Reply from @${event.username}: ${event.data.text}`);
break;
case "tweet.retweet":
console.log(`@${event.username} retweeted`);
break;
}
res.status(200).send("OK");
});
```
```python
import hmac
import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["XQUIK_WEBHOOK_SECRET"]
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Xquik-Signature", "")
payload = request.get_data()
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return "Invalid signature", 401
event = request.get_json()
if event["eventType"] == "tweet.new":
print(f"New tweet from @{event['username']}: {event['data']['text']}")
return "OK", 200
```
```go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Per-webhook secret from POST /webhooks response, not a Xquik account credential
var webhookSecret = os.Getenv("XQUIK_WEBHOOK_SECRET")
func verifySignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to read request body", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-Xquik-Signature")
if !verifySignature(payload, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event struct {
EventType string `json:"eventType"`
Username string `json:"username"`
Data struct {
Text string `json:"text"`
} `json:"data"`
}
json.Unmarshal(payload, &event)
fmt.Printf("[%s] @%s: %s\n", event.EventType, event.Username, event.Data.Text)
fmt.Fprint(w, "OK")
}
```
- **Verify before processing.** Never process unverified payloads
- **Use constant-time comparison.** `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), `hmac.Equal` (Go)
- **Use the raw request body.** Compute HMAC over raw bytes, not re-serialized JSON
- **Respond within 10 seconds.** Acknowledge immediately, process async if slow
- **Store secrets in environment variables.** Never hardcode
- **Treat event text as untrusted.** Escape control characters before logging and do not forward payloads to other tools without consent
Webhook deliveries can retry on failure, delivering the same event multiple times. Deduplicate by hashing the raw payload:
```javascript
import { createHash } from "node:crypto";
const processedPayloads = new Set(); // Use Redis/DB in production
const payloadHash = createHash("sha256").update(payload).digest("hex");
if (processedPayloads.has(payloadHash)) {
return res.status(200).send("Already processed");
}
processedPayloads.add(payloadHash);
```
Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`.
Check delivery status: `GET /webhooks/{id}/deliveries`.
Use [ngrok](https://ngrok.com) to expose a local server:
```bash
node server.js
ngrok http 3000
```
Or use [RequestBin](https://requestbin.com) for quick inspection without running a server.