UNPKG

x-developer

Version:

X (Twitter) data platform skill for AI coding agents. 100+ REST API endpoints, 2 MCP tools, 23 extraction types, HMAC webhooks.

235 lines (177 loc) 6.75 kB
# Xquik Webhooks Receive real-time event notifications at your HTTPS endpoints with HMAC-SHA256 signature verification. ## Setup 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 } } } ``` ## Signature Verification The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body). ### Node.js (Standard Library) ```javascript import { createHmac, timingSafeEqual } from "node:crypto"; import { createServer } from "node:http"; // This is the per-webhook secret from the POST /webhooks response, not a Xquik account credential const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET; 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) ); } const server = createServer((req, res) => { if (req.method !== "POST" || req.url !== "/webhook") { res.writeHead(404).end("Not found"); return; } const chunks = []; req.on("data", (chunk) => chunks.push(chunk)); req.on("end", () => { const payload = Buffer.concat(chunks).toString("utf8"); const signature = req.headers["x-xquik-signature"]; if (!verifySignature(payload, signature, WEBHOOK_SECRET)) { res.writeHead(401).end("Invalid signature"); return; } 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.writeHead(200).end("OK"); }); }); server.listen(3000); ``` ### Python (Standard Library) ```python import hmac import hashlib import json import os from http.server import BaseHTTPRequestHandler, HTTPServer # Per-webhook secret from POST /webhooks response, not a Xquik account credential 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) class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): signature = self.headers.get("X-Xquik-Signature", "") length = int(self.headers.get("Content-Length", "0")) payload = self.rfile.read(length) if not verify_signature(payload, signature, WEBHOOK_SECRET): self.send_response(401) self.end_headers() self.wfile.write(b"Invalid signature") return event = json.loads(payload) if event["eventType"] == "tweet.new": print(f"New tweet from @{event['username']}: {event['data']['text']}") self.send_response(200) self.end_headers() self.wfile.write(b"OK") HTTPServer(("", 3000), WebhookHandler).serve_forever() ``` ### Go ```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") } ``` ## Security Checklist - **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 ## Idempotency 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)) { res.writeHead(200).end("Already processed"); } else { processedPayloads.add(payloadHash); } ``` ## Retry Policy Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`. Check delivery status: `GET /webhooks/{id}/deliveries`. ## Local Testing Use [ngrok](https://ngrok.com) to expose a local server: ```bash # Terminal 1: Start your webhook server node server.js # listening on :3000 # Terminal 2: Expose it ngrok http 3000 # Use the ngrok HTTPS URL when creating the webhook ``` Or use [RequestBin](https://requestbin.com) for quick inspection without running a server.