x-developer 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ # Xquik Webhooks
2
+
3
+ Receive real-time event notifications at your HTTPS endpoints with HMAC-SHA256 signature verification.
4
+
5
+ ## Setup
6
+
7
+ 1. Create at least 1 active monitor (`POST /monitors`)
8
+ 2. Register a webhook endpoint (`POST /webhooks`)
9
+ 3. Save the `secret` from the response (shown only once)
10
+ 4. Build a handler that verifies signatures before processing
11
+
12
+ ## Webhook Payload
13
+
14
+ Every delivery is a `POST` request to your URL with a JSON body:
15
+
16
+ ```json
17
+ {
18
+ "eventType": "tweet.new",
19
+ "username": "elonmusk",
20
+ "data": {
21
+ "tweetId": "1893556789012345678",
22
+ "text": "Hello world",
23
+ "metrics": { "likes": 3200, "retweets": 890, "replies": 245 }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## Signature Verification
29
+
30
+ The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body).
31
+
32
+ ### Node.js (Express)
33
+
34
+ ```javascript
35
+ import express from "express";
36
+ import { createHmac, timingSafeEqual } from "node:crypto";
37
+
38
+ // This is the per-webhook secret from the POST /webhooks response, not a Xquik account credential
39
+ const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET;
40
+
41
+ function verifySignature(payload, signature, secret) {
42
+ const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
43
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
44
+ }
45
+
46
+ app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
47
+ const signature = req.headers["x-xquik-signature"];
48
+ const payload = req.body.toString();
49
+
50
+ if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
51
+ return res.status(401).send("Invalid signature");
52
+ }
53
+
54
+ const event = JSON.parse(payload);
55
+
56
+ switch (event.eventType) {
57
+ case "tweet.new":
58
+ console.log(`New tweet from @${event.username}: ${event.data.text}`);
59
+ break;
60
+ case "tweet.reply":
61
+ console.log(`Reply from @${event.username}: ${event.data.text}`);
62
+ break;
63
+ case "follower.gained":
64
+ console.log(`@${event.username} gained a follower`);
65
+ break;
66
+ }
67
+
68
+ res.status(200).send("OK");
69
+ });
70
+ ```
71
+
72
+ ### Python (Flask)
73
+
74
+ ```python
75
+ import hmac
76
+ import hashlib
77
+ import os
78
+ from flask import Flask, request
79
+
80
+ app = Flask(__name__)
81
+ # Per-webhook secret from POST /webhooks response, not a Xquik account credential
82
+ WEBHOOK_SECRET = os.environ["XQUIK_WEBHOOK_SECRET"]
83
+
84
+ def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
85
+ expected = "sha256=" + hmac.new(
86
+ secret.encode(), payload, hashlib.sha256
87
+ ).hexdigest()
88
+ return hmac.compare_digest(expected, signature)
89
+
90
+ @app.route("/webhook", methods=["POST"])
91
+ def webhook():
92
+ signature = request.headers.get("X-Xquik-Signature", "")
93
+ payload = request.get_data()
94
+
95
+ if not verify_signature(payload, signature, WEBHOOK_SECRET):
96
+ return "Invalid signature", 401
97
+
98
+ event = request.get_json()
99
+
100
+ if event["eventType"] == "tweet.new":
101
+ print(f"New tweet from @{event['username']}: {event['data']['text']}")
102
+
103
+ return "OK", 200
104
+ ```
105
+
106
+ ### Go
107
+
108
+ ```go
109
+ package main
110
+
111
+ import (
112
+ "crypto/hmac"
113
+ "crypto/sha256"
114
+ "encoding/hex"
115
+ "encoding/json"
116
+ "fmt"
117
+ "io"
118
+ "net/http"
119
+ "os"
120
+ )
121
+
122
+ // Per-webhook secret from POST /webhooks response, not a Xquik account credential
123
+ var webhookSecret = os.Getenv("XQUIK_WEBHOOK_SECRET")
124
+
125
+ func verifySignature(payload []byte, signature, secret string) bool {
126
+ mac := hmac.New(sha256.New, []byte(secret))
127
+ mac.Write(payload)
128
+ expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
129
+ return hmac.Equal([]byte(expected), []byte(signature))
130
+ }
131
+
132
+ func webhookHandler(w http.ResponseWriter, r *http.Request) {
133
+ payload, _ := io.ReadAll(r.Body)
134
+ signature := r.Header.Get("X-Xquik-Signature")
135
+
136
+ if !verifySignature(payload, signature, webhookSecret) {
137
+ http.Error(w, "Invalid signature", http.StatusUnauthorized)
138
+ return
139
+ }
140
+
141
+ var event struct {
142
+ EventType string `json:"eventType"`
143
+ Username string `json:"username"`
144
+ Data struct {
145
+ Text string `json:"text"`
146
+ } `json:"data"`
147
+ }
148
+ json.Unmarshal(payload, &event)
149
+
150
+ fmt.Printf("[%s] @%s: %s\n", event.EventType, event.Username, event.Data.Text)
151
+ fmt.Fprint(w, "OK")
152
+ }
153
+ ```
154
+
155
+ ## Security Checklist
156
+
157
+ - **Verify before processing.** Never process unverified payloads
158
+ - **Use constant-time comparison.** `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), `hmac.Equal` (Go)
159
+ - **Use the raw request body.** Compute HMAC over raw bytes, not re-serialized JSON
160
+ - **Respond within 10 seconds.** Acknowledge immediately, process async if slow
161
+ - **Store secrets in environment variables.** Never hardcode
162
+
163
+ ## Idempotency
164
+
165
+ Webhook deliveries can retry on failure, delivering the same event multiple times. Deduplicate by hashing the raw payload:
166
+
167
+ ```javascript
168
+ import { createHash } from "node:crypto";
169
+
170
+ const processedPayloads = new Set(); // Use Redis/DB in production
171
+
172
+ const payloadHash = createHash("sha256").update(rawPayload).digest("hex");
173
+ if (processedPayloads.has(payloadHash)) {
174
+ return res.status(200).send("Already processed");
175
+ }
176
+ processedPayloads.add(payloadHash);
177
+ ```
178
+
179
+ ## Retry Policy
180
+
181
+ Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`.
182
+
183
+ Check delivery status: `GET /webhooks/{id}/deliveries`.
184
+
185
+ ## Local Testing
186
+
187
+ Use [ngrok](https://ngrok.com) to expose a local server:
188
+
189
+ ```bash
190
+ # Terminal 1: Start your webhook server
191
+ node server.js # listening on :3000
192
+
193
+ # Terminal 2: Expose it
194
+ ngrok http 3000
195
+ # Use the ngrok HTTPS URL when creating the webhook
196
+ ```
197
+
198
+ Or use [RequestBin](https://requestbin.com) for quick inspection without running a server.
@@ -0,0 +1,194 @@
1
+ # Xquik Workflow Examples
2
+
3
+ Code examples for common integration patterns.
4
+
5
+ ## Authentication
6
+
7
+ ```javascript
8
+ const API_KEY = "xq_YOUR_KEY_HERE";
9
+ const BASE = "https://xquik.com/api/v1";
10
+ const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" };
11
+ ```
12
+
13
+ ## Retry with Exponential Backoff
14
+
15
+ Retry only `429` and `5xx`. Never retry `4xx` (except 429). Max 3 retries:
16
+
17
+ ```javascript
18
+ async function xquikFetch(path, options = {}) {
19
+ const baseDelay = 1000;
20
+
21
+ for (let attempt = 0; attempt <= 3; attempt++) {
22
+ const response = await fetch(`${BASE}${path}`, {
23
+ ...options,
24
+ headers: { ...headers, ...options.headers },
25
+ });
26
+
27
+ if (response.ok) return response.json();
28
+
29
+ const retryable = response.status === 429 || response.status >= 500;
30
+ if (!retryable || attempt === 3) {
31
+ const error = await response.json();
32
+ throw new Error(`Xquik API ${response.status}: ${error.error}`);
33
+ }
34
+
35
+ const retryAfter = response.headers.get("Retry-After");
36
+ const delay = retryAfter
37
+ ? parseInt(retryAfter, 10) * 1000
38
+ : baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
39
+
40
+ await new Promise((resolve) => setTimeout(resolve, delay));
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Cursor Pagination
46
+
47
+ Events, draws, extractions, and extraction results use cursor-based pagination. When more results exist, the response includes `hasMore: true` and a `nextCursor` string. Pass `nextCursor` as the `after` query parameter.
48
+
49
+ ```javascript
50
+ async function fetchAllPages(path, dataKey) {
51
+ const results = [];
52
+ let cursor;
53
+
54
+ while (true) {
55
+ const params = new URLSearchParams({ limit: "100" });
56
+ if (cursor) params.set("after", cursor);
57
+
58
+ const data = await xquikFetch(`${path}?${params}`);
59
+ results.push(...data[dataKey]);
60
+
61
+ if (!data.hasMore) break;
62
+ cursor = data.nextCursor;
63
+ }
64
+
65
+ return results;
66
+ }
67
+ ```
68
+
69
+ Cursors are opaque strings. Never decode or construct them manually.
70
+
71
+ ## Complete Extraction Workflow
72
+
73
+ ```javascript
74
+ // Step 1: Estimate cost before running
75
+ const estimate = await xquikFetch("/extractions/estimate", {
76
+ method: "POST",
77
+ body: JSON.stringify({
78
+ toolType: "follower_explorer",
79
+ targetUsername: "elonmusk",
80
+ resultsLimit: 1000,
81
+ }),
82
+ });
83
+
84
+ if (!estimate.allowed) {
85
+ console.log("Extraction would exceed monthly quota");
86
+ return;
87
+ }
88
+
89
+ // Step 2: Create extraction job
90
+ const job = await xquikFetch("/extractions", {
91
+ method: "POST",
92
+ body: JSON.stringify({
93
+ toolType: "follower_explorer",
94
+ targetUsername: "elonmusk",
95
+ resultsLimit: 1000,
96
+ }),
97
+ });
98
+
99
+ // Step 3: Poll until complete
100
+ while (job.status === "pending" || job.status === "running") {
101
+ await new Promise((r) => setTimeout(r, 2000));
102
+ job = await xquikFetch(`/extractions/${job.id}`);
103
+ }
104
+
105
+ // Step 4: Retrieve paginated results (up to 1,000 per page)
106
+ let cursor;
107
+ const allResults = [];
108
+
109
+ while (true) {
110
+ const path = `/extractions/${job.id}${cursor ? `?after=${cursor}` : ""}`;
111
+ const page = await xquikFetch(path);
112
+ allResults.push(...page.results);
113
+
114
+ if (!page.hasMore) break;
115
+ cursor = page.nextCursor;
116
+ }
117
+
118
+ // Step 5: Export as CSV/XLSX/Markdown (50,000 row limit)
119
+ const exportUrl = `${BASE}/extractions/${job.id}/export?format=csv`;
120
+ const csvResponse = await fetch(exportUrl, { headers });
121
+ const csvData = await csvResponse.text();
122
+ ```
123
+
124
+ ## Real-Time Monitoring Setup
125
+
126
+ Complete end-to-end: create monitor, register webhook, handle events.
127
+
128
+ ```javascript
129
+ // 1. Create monitor (free)
130
+ const monitor = await xquikFetch("/monitors", {
131
+ method: "POST",
132
+ body: JSON.stringify({
133
+ username: "elonmusk",
134
+ eventTypes: ["tweet.new", "tweet.reply", "tweet.quote", "follower.gained"],
135
+ }),
136
+ });
137
+
138
+ // 2. Register webhook (free)
139
+ const webhook = await xquikFetch("/webhooks", {
140
+ method: "POST",
141
+ body: JSON.stringify({
142
+ url: "https://your-server.com/webhook",
143
+ eventTypes: ["tweet.new", "tweet.reply"],
144
+ }),
145
+ });
146
+ // IMPORTANT: Save webhook.secret. It is shown only once!
147
+
148
+ // 3. Poll events (alternative to webhooks, free)
149
+ const events = await xquikFetch("/events?monitorId=7&limit=50");
150
+ ```
151
+
152
+ Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `follower.gained`, `follower.lost`.
153
+
154
+ ## Endpoint Guide
155
+
156
+ | Goal | Endpoint | Cost |
157
+ |------|----------|------|
158
+ | **Get a single tweet** by ID/URL | `GET /x/tweets/{id}` | 1 credit |
159
+ | **Get an X Article** by tweet ID | `GET /x/articles/{id}` | 7 credits |
160
+ | **Search tweets** by keyword/hashtag | `GET /x/tweets/search?q=...` | 1 credit/tweet |
161
+ | **Get a user profile** | `GET /x/users/{username}` | 1 credit |
162
+ | **Get user's recent tweets** | `GET /x/users/{id}/tweets` | 1 credit/tweet |
163
+ | **Get user's liked tweets** | `GET /x/users/{id}/likes` | 1 credit/result |
164
+ | **Get user's media tweets** | `GET /x/users/{id}/media` | 1 credit/result |
165
+ | **Get tweet favoriters** | `GET /x/tweets/{id}/favoriters` | 1 credit/result |
166
+ | **Get mutual followers** | `GET /x/users/{id}/followers-you-know` | 1 credit/result |
167
+ | **Check follow relationship** | `GET /x/followers/check?source=A&target=B` | 7 credits |
168
+ | **Get trending topics** | `GET /trends?woeid=1` | 3 credits |
169
+ | **Get radar (trending news)** | `GET /radar?source=hacker_news` | Free |
170
+ | **Get bookmarks** | `GET /x/bookmarks` | 1 credit/result |
171
+ | **Get bookmark folders** | `GET /x/bookmarks/folders` | 1 credit |
172
+ | **Get notifications** | `GET /x/notifications` | 1 credit/result |
173
+ | **Get home timeline** | `GET /x/timeline` | 1 credit/result |
174
+ | **Get DM history** | `GET /x/dm/{userId}/history` | 1 credit/result |
175
+ | **Monitor an X account** | `POST /monitors` | Free |
176
+ | **Poll for events** | `GET /events` | Free |
177
+ | **Receive events via webhook** | `POST /webhooks` | Free |
178
+ | **Run a giveaway draw** | `POST /draws` | 1 credit/entry |
179
+ | **Download tweet media** | `POST /x/media/download` | 1 credit/item |
180
+ | **Extract bulk data** | `POST /extractions` | 1-5 credits/result |
181
+ | **Check credits** | `GET /credits` | Free |
182
+ | **Top up credits** | `POST /credits/topup` | Free |
183
+ | **Compose a tweet** | `POST /compose` | Free |
184
+ | **Post a tweet** | `POST /x/tweets` | 10 credits |
185
+ | **Like / Unlike a tweet** | `POST` / `DELETE /x/tweets/{id}/like` | 10 credits |
186
+ | **Retweet** | `POST /x/tweets/{id}/retweet` | 10 credits |
187
+ | **Follow / Unfollow** | `POST` / `DELETE /x/users/{id}/follow` | 10 credits |
188
+ | **Send a DM** | `POST /x/dm/{userId}` | 10 credits |
189
+ | **Update profile** | `PATCH /x/profile` | 10 credits |
190
+ | **Upload media** | `POST /x/media` | 10 credits |
191
+ | **Community actions** | `POST /x/communities`, join/leave | 10 credits |
192
+ | **Telegram integration** | `POST /integrations` | Free |
193
+ | **Automation flows** | `POST /automations` | Free |
194
+ | **Support tickets** | `POST /support/tickets` | Free |