xskill is a prepaid API for agents that need to read and understand X/Twitter posts, threads, conversations, search results, and image media.
Base URLs
- Production API:
https://api.xskill.md - Local development default:
http://127.0.0.1:3000
Set these shell variables before using the examples:
export XSKILL_BASE_URL="${XSKILL_BASE_URL:-https://api.xskill.md}"
export XSKILL_API_KEY="xsk_replace_me"
export X_POST_ID="1234567890123456789"Authentication
Customer endpoints require an xsk_ API key. Prefer the bearer form:
Authorization: Bearer xsk_...x-api-key: xsk_... is also accepted. Never send provider or parser secrets such as TWITTERAPI_IO_API_KEY, GETXAPI_API_KEY, or ANTHROPIC_API_KEY to xskill endpoints.
GET /health, POST /v1/signup, and POST /v1/stripe/webhook do not use customer API-key auth. GET /v1/ops/metrics is an internal endpoint protected by XSK_OPS_TOKEN; never use a customer xsk_ key for ops access.
Copy-Paste Quickstart
Check service health:
curl -sS "$XSKILL_BASE_URL/health"Create a new account and one-time xsk_ key:
curl -sS \
-X POST \
-H "Content-Type: application/json" \
-d '{"inviteCode":"replace-with-invite","name":"Research agent"}' \
"$XSKILL_BASE_URL/v1/signup"Read and summarize a conversation:
curl -sS \
-H "Authorization: Bearer $XSKILL_API_KEY" \
"$XSKILL_BASE_URL/v1/thread?id=$X_POST_ID&mode=conversation&parse=summary"Read one post:
curl -sS \
-H "Authorization: Bearer $XSKILL_API_KEY" \
"$XSKILL_BASE_URL/v1/post?id=$X_POST_ID"Search X:
curl -sS -G \
-H "Authorization: Bearer $XSKILL_API_KEY" \
--data-urlencode "query=from:OpenAI agent" \
--data-urlencode "type=Latest" \
"$XSKILL_BASE_URL/v1/search"Create a $10.00 top-up Checkout Session:
curl -sS \
-X POST \
-H "Authorization: Bearer $XSKILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amountCents":1000}' \
"$XSKILL_BASE_URL/v1/topups/checkout"Response Shape
Successful API responses return a data object. Billable read endpoints also return usage.
{
"data": {},
"usage": {
"cost": {
"currency": "USD",
"estimatedUsd": 0.003,
"itemsRead": 20,
"unitCostUsd": 0.00015,
"upstreamRequests": 1
},
"pricing": {
"currency": "USD",
"operation": "parsed_thread",
"priceCardVersion": "2026-06-25.default",
"priceMicroCredits": 20000,
"priceUsd": 0.02,
"units": {
"tweets": 20
}
},
"provider": "twitterapi.io",
"tweetsRead": 20
}
}usage.provider is the active backend adapter for the call. It is normally twitterapi.io, but may be getxapi during an operator failover. Customer routes and price-card billing stay the same across that provider flip.
Errors use one envelope:
{
"error": {
"code": "invalid_request",
"message": "Request validation failed",
"statusCode": 400
}
}Rate-limited authenticated requests include x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, and, for rejected requests, retry-after.
Pricing
xskill stores balances as integer micro-credits:
1_000_000micro-credits =$1.00$0.02=20_000micro-credits
Default price card:
| Operation | Price |
|---|---|
| Raw post | 4_000 micro-credits |
| Raw search | 300 * tweetsReturned |
| Raw thread | 4_000 + 300 * tweetsRead |
| Parsed thread, Haiku | 14_000 + 300 * tweetsRead |
| Parsed thread, Sonnet | 44_000 + 300 * tweetsRead |
| Vision image OCR/description | 20_000 * images |
The default free tier tops each account balance up to 2_000_000 micro-credits once per UTC month. Free credits are account-scoped, not API-key-scoped, and unused free credits do not stack above the monthly cap.
Top-ups use Stripe Checkout. The minimum top-up is $10.00, which credits 10_000_000 micro-credits after Stripe reports a paid Checkout Session. Signup and checkout surfaces must link the Terms of Service and Pricing and Refund Policy before creating a Checkout Session.
See pricing.md for price-card overrides and reservation behavior.
Endpoints
GET/health
Returns service health. No auth.
Example:
curl -sS "$XSKILL_BASE_URL/health"Response:
{
"environment": "production",
"ok": true,
"service": "xskill-api"
}POST/v1/signup
Creates a new account and returns a one-time xsk_ API key. Public signup is invite-gated with XSK_SIGNUP_INVITE_CODE, invite reuse tracking, and IP throttling via XSK_SIGNUP_RATE_LIMIT_MAX_REQUESTS / XSK_SIGNUP_RATE_LIMIT_WINDOW_MS. Set XSK_DATA_FILE to persist account metadata, API-key hashes, invite-use hashes, balances, credit-grant idempotency, and usage records across single-replica restarts. Without XSK_DATA_FILE, those stores are process-local. Direct deployments do not trust proxy headers by default; hosted deployments behind a proxy that strips inbound forwarding headers should set XSK_TRUST_PROXY=true so throttling keys on the forwarded client IP instead of the load balancer. The key is only returned in this response; store it before leaving the page or terminal.
Request body:
| Name | Required | Values | Default |
|---|---|---|---|
inviteCode | yes | active signup invite code | none |
name | no | 1 to 80 characters | omitted |
Example:
curl -sS \
-X POST \
-H "Content-Type: application/json" \
-d '{"inviteCode":"replace-with-invite","name":"Research agent"}' \
"$XSKILL_BASE_URL/v1/signup"Response:
{
"data": {
"account": {
"createdAt": "2026-06-26T00:00:00.000Z",
"id": "acct_..."
},
"apiKey": {
"accountId": "acct_...",
"createdAt": "2026-06-26T00:00:00.000Z",
"id": "key_...",
"name": "Research agent",
"prefix": "xsk_..."
},
"docs": {
"api": "/api",
"pricing": "/pricing",
"refundPolicy": "/refund-policy",
"skill": "/skill.md",
"terms": "/terms"
},
"key": "xsk_...",
"topUp": {
"amountCents": 1000,
"amountMicroCredits": 10000000,
"endpoint": "/v1/topups/checkout"
}
}
}The hosted landing page uses this endpoint, then calls POST /v1/topups/checkout with the new key after showing the Terms of Service and Pricing and Refund Policy links.
GET/v1/post
Fetches one post by numeric X post ID.
Query parameters:
| Name | Required | Values | Default |
|---|---|---|---|
id | yes | 1 to 25 digits | none |
parse | no | summary, json, tldr | omitted |
model | no | haiku, sonnet | haiku |
vision | no | true, false | false |
Billing:
- No
parse:raw_post. parse=...&model=haiku:parsed_threadfor one tweet, with raw-post fallback reservation.parse=...&model=sonnet:premium_parsed_threadfor one tweet. Requires premium models to be enabled.vision=true: separately billsvision_imagefor photo media analyzed.
Example:
curl -sS \
-H "Authorization: Bearer $XSKILL_API_KEY" \
"$XSKILL_BASE_URL/v1/post?id=$X_POST_ID&parse=tldr&vision=true"Response fields:
data.post: normalized post.data.parsed: present whenparseis requested.data.vision: image OCR/description results whenvision=true.usage.tweetsRead: upstream post reads.usage.pricing: customer price metadata.usage.vision: separate image billing metadata when applicable.
GET/v1/thread
Fetches tweets in a conversation through the provider's conversation_id search path. Use the root conversation ID when possible.
Query parameters:
| Name | Required | Values | Default |
|---|---|---|---|
id | yes | 1 to 25 digits | none |
mode | no | conversation, thread | conversation |
maxPages | no | integer >= 1 | provider default |
maxTweets | no | integer >= 1 | provider default |
parse | no | summary, json, tldr | omitted |
model | no | haiku, sonnet | haiku |
vision | no | true, false | false |
mode=conversation returns all fetched conversation tweets. mode=thread returns the root author's self-reply chain when root-author and reply metadata are available; otherwise it falls back to the full conversation to avoid guessing.
Billing:
- No
parse:raw_thread. parse=...&model=haiku:parsed_thread.parse=...&model=sonnet:premium_parsed_thread. Requires premium models.vision=true: separately billsvision_imagefor photo media analyzed.
The twitterapi.io adapter caps thread fetches at 5 pages / 100 tweets.
Example:
curl -sS \
-H "Authorization: Bearer $XSKILL_API_KEY" \
"$XSKILL_BASE_URL/v1/thread?id=$X_POST_ID&mode=thread&parse=summary&maxPages=2"Response fields:
data.id: conversation ID.data.mode: resolved mode.data.tweets: normalized tweets.data.truncated:truewhen the provider reports additional pages.data.parsed: present whenparseis requested.data.visionanddata.visionTruncated: present whenvision=true.usage.tweetsRead: upstream tweets read.usage.tweetsReturned: tweets returned after mode filtering.
GET/v1/search
Runs provider-backed X advanced search.
Query parameters:
| Name | Required | Values | Default |
|---|---|---|---|
query | yes | non-empty string, max 512 chars | none |
type | no | Latest, Top | Latest |
cursor | no | provider cursor | omitted |
maxPages | no | integer 1..5 | 1 |
maxTweets | no | integer 1..100 | 20 |
Billing: raw_search, settled to 300 * tweetsReturned. The route reserves the provider-estimated reachable result window before provider spend.
Example:
curl -sS -G \
-H "Authorization: Bearer $XSKILL_API_KEY" \
--data-urlencode "query=conversation_id:$X_POST_ID" \
--data-urlencode "type=Latest" \
--data-urlencode "maxTweets=20" \
"$XSKILL_BASE_URL/v1/search"Response fields:
data.query: query sent to the provider.data.type:LatestorTop.data.tweets: normalized tweets.data.pageInfo.nextCursor: cursor for the next page when present.usage.tweetsRead: upstream tweets read.usage.tweetsReturned: tweets returned to the caller.
POST/v1/topups/checkout
Creates a Stripe Checkout Session for prepaid credits. Show or link the Terms of Service and Pricing and Refund Policy before creating a Checkout Session.
Request body:
{
"amountCents": 1000
}amountCents must be an integer of at least 1000.
Example:
curl -sS \
-X POST \
-H "Authorization: Bearer $XSKILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amountCents":1000}' \
"$XSKILL_BASE_URL/v1/topups/checkout"Response:
{
"data": {
"amountCents": 1000,
"amountMicroCredits": 10000000,
"currency": "usd",
"id": "cs_test_...",
"url": "https://checkout.stripe.com/..."
}
}Credits are added only after Stripe sends a paid Checkout webhook.
POST/v1/stripe/webhook
Stripe-only webhook endpoint. No customer API-key auth.
Headers:
Stripe-Signature: ...
Content-Type: application/jsonThe route verifies the raw request body with STRIPE_WEBHOOK_SECRET. Paid checkout.session.completed and checkout.session.async_payment_succeeded events credit the account once using the Checkout Session ID as the idempotency key.
GET/v1/ops/metrics
Internal observability endpoint. Requires the configured ops token:
curl -sS \
-H "Authorization: Bearer $XSK_OPS_TOKEN" \
"$XSKILL_BASE_URL/v1/ops/metrics"The response is a JSON dashboard with real-time in-process usage, cost, revenue, margin, credit grants, recent per-call billing logs, and twitterapi.io prepaid float alert status.
Response fields:
data.totals.revenueUsd,data.totals.costUsd, anddata.totals.marginUsd: revenue vs upstream cost vs margin.data.byEndpointanddata.byOperation: request, unit, revenue, cost, and margin breakdowns.data.byProvider: provider/parser cost breakdown. Parsed calls split twitterapi.io fetch cost from Anthropic parser cost.data.recentUsage: recent billable reservations/settlements.data.float.status:ok,low, orunknown.data.alerts: includestwitterapi_io_float_lowwhenXSK_TWITTERAPI_IO_FLOAT_BALANCE_USDis belowXSK_TWITTERAPI_IO_FLOAT_ALERT_USD.
Error Codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Request validation failed or JSON body is invalid. |
| 400 | invalid_stripe_event | Stripe webhook payload is not a valid xskill top-up event. |
| 400 | invalid_stripe_signature | Stripe signature is missing or invalid. |
| 401 | unauthorized | Missing, invalid, or revoked xsk_ API key. |
| 402 | insufficient_balance | Prepaid balance is too low; top up to continue. |
| 403 | invalid_signup_invite | Signup invite code is invalid. |
| 403 | premium_model_required | model=sonnet was requested but premium models are disabled. |
| 404 | post_not_found | The requested post was not found. |
| 404 | not_found | Route does not exist. |
| 409 | stripe_idempotency_conflict | Stripe top-up idempotency conflict. |
| 413 | invalid_request | Request body is too large. |
| 413 | cost_ceiling_exceeded | Request would exceed the configured per-call cost ceiling. |
| 413 | parse_budget_exceeded | Parse input/output budget is too large. |
| 415 | invalid_request | Request media type is not supported. |
| 429 | rate_limited | Per-key or public signup rate limit exceeded. |
| 500 | internal_error | Unexpected server error. |
| 503 | auth_unavailable | API-key auth is not configured. |
| 503 | billing_unavailable | Credit ledger is not configured or did not return a usage id. |
| 503 | ops_unavailable | Ops metrics are missing or XSK_OPS_TOKEN is not configured. |
| 503 | parser_provider_unavailable | The configured parser provider rejected or failed the parse request. |
| 503 | parser_unavailable | Parse was requested but no parser is configured. |
| 503 | pricing_unavailable | Pricing engine is not configured. |
| 503 | provider_unavailable | Tweet provider is missing or unavailable. |
| 503 | signup_unavailable | Public API-key issuance is not configured. |
| 503 | stripe_unavailable | Stripe Checkout or webhook verification is not configured. |
| 503 | vision_unavailable | Vision was requested but no analyzer is configured or available. |
Normalized Data Notes
Tweet objects can include:
id,text,url,createdAt,conversationId,inReplyToTweetId- author fields such as
id,username,name,verified,profileImageUrl - count fields such as
replyCount,retweetCount,likeCount,quoteCount,bookmarkCount,viewCount media[]withtype,url,previewImageUrl,altText, and optionalvisionOCR/description data
Provider and parser failures are sanitized; responses do not expose upstream API keys, Anthropic keys, raw prompt internals, or customer API-key material.