ClimbX API docs
A small REST API for AI agents to publish and schedule X posts and read analytics and voice data on behalf of a ClimbX account. JSON in, JSON out. v1 is intentionally minimal.
Authentication
Every request needs a key in the Authorization header. Create one in ClimbX under Settings → API. The full key is shown once at creation - store it somewhere safe. We keep only a hash and cannot show it again.
Authorization: Bearer climbx_sk_your_keyThe owning account must be on an active plan or trial. A lapsed subscription returns 402 even if the key is still live. Revoke a key any time from the same screen; it stops working on the next call.
Base URL and limits
https://climbx.so/api/v1- Daily post cap: 5 posts per account per day across publish and schedule. Resets at 00:00 UTC. Over the cap returns
429. - No links: posts containing a URL are rejected with
400. Link posts cut reach and cost 13x to publish. - Content type: send
Content-Type: application/jsonon every POST and PATCH. - Media: attach up to 4 images by passing
image_urls(public https URLs) on publish or schedule. Video over the API is a work in progress. - Read rate limit: GET endpoints allow about 60 requests per minute per key. Over it returns
429with aRetry-Afterheader. - Crawling analytics: the analytics and learnings-history endpoints take a
startandendISO window (default last 30 days, max span 366 days). Every response carriesgenerated_atandas_of(the newest metrics snapshot in the window) - poll on a schedule and dedupe onas_ofto build a time series. - Freshness: using the API refreshes your data in the background - new posts, metrics, and learnings - the same way opening the app does, so you never have to visit the web app to keep these endpoints current. Refreshes are throttled, so the first call in a window does the work and later calls read the fresh result.
Publish a post now
/api/v1/postsShips a post to X immediately through the connected account. Attach up to 4 images with image_urls.
| Field | Type | Description | |
|---|---|---|---|
| text | string | required | The post body. 1 to 10,000 chars. No URLs. |
| image_urls | string[] | optional | Up to 4 public https image URLs. Each is fetched and attached. Video is not supported yet. |
curl -X POST https://climbx.so/api/v1/posts \
-H "Authorization: Bearer climbx_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "shipped something today."}'{
"ok": true,
"id": "1799999999999999999",
"url": "https://x.com/i/web/status/1799999999999999999",
"record_id": "9f1c...",
"posts_used_today": 1,
"daily_cap": 5
}List recent posts with metrics
/api/v1/postsReturns the account’s recent published posts and their latest metrics snapshot.
| Field | Type | Description | |
|---|---|---|---|
| limit | number | optional | How many to return. 1 to 100, default 30. |
curl https://climbx.so/api/v1/posts?limit=10 \
-H "Authorization: Bearer climbx_sk_your_key"{
"posts": [
{
"id": "1799...",
"text": "...",
"format": "hot_take",
"posted_at": "2026-05-25T08:30:00Z",
"is_reply": false,
"metrics": {
"impressions": 14200,
"views": 14200,
"likes": 180,
"replies": 24,
"retweets": 12,
"quote_tweets": 3
}
}
]
}Schedule a post
/api/v1/scheduleQueues a post for a future time. ClimbX ships it at the scheduled minute and retries on transient failures. A past time publishes on the next tick.
| Field | Type | Description | |
|---|---|---|---|
| text | string | required | The post body. 1 to 10,000 chars. No URLs. |
| scheduled_for | string | required | ISO 8601 datetime, e.g. 2026-06-01T14:00:00Z. |
| image_urls | string[] | optional | Up to 4 public https image URLs, attached at publish time. No video yet. |
curl -X POST https://climbx.so/api/v1/schedule \
-H "Authorization: Bearer climbx_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "morning thought.", "scheduled_for": "2026-06-01T08:00:00Z"}'{
"ok": true,
"scheduled": {
"id": "3a2b...",
"text": "morning thought.",
"scheduled_for": "2026-06-01T08:00:00Z",
"status": "pending"
},
"posts_used_today": 2,
"daily_cap": 5
}List scheduled posts
/api/v1/scheduleReturns upcoming posts that are still pending or mid-publish.
curl https://climbx.so/api/v1/schedule \
-H "Authorization: Bearer climbx_sk_your_key"{
"scheduled": [
{
"id": "3a2b...",
"text": "morning thought.",
"scheduled_for": "2026-06-01T08:00:00Z",
"status": "pending"
}
]
}Reschedule a pending post
/api/v1/schedule/{id}Moves a still-pending scheduled post to a new time. Works only while the post is pending; once ClimbX has started publishing it, it can’t be changed (returns 409).
| Field | Type | Description | |
|---|---|---|---|
| scheduled_for | string | required | New ISO 8601 datetime. |
curl -X PATCH https://climbx.so/api/v1/schedule/3a2b... \
-H "Authorization: Bearer climbx_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"scheduled_for": "2026-06-02T09:30:00Z"}'{
"ok": true,
"scheduled": {
"id": "3a2b...",
"text": "morning thought.",
"scheduled_for": "2026-06-02T09:30:00Z",
"status": "pending"
}
}Cancel a scheduled post
/api/v1/schedule/{id}Cancels a still-pending scheduled post so it won’t publish. Cancelling does not give back a daily-cap slot - the post counted when you created it.
curl -X DELETE https://climbx.so/api/v1/schedule/3a2b... \
-H "Authorization: Bearer climbx_sk_your_key"{
"ok": true,
"cancelled": { "id": "3a2b...", "status": "cancelled" }
}Performance summary
/api/v1/analyticsHeadline KPIs plus a per-format breakdown over a window. Replies are excluded.
| Field | Type | Description | |
|---|---|---|---|
| days | number | optional | Lookback window. 1 to 90, default 30. |
curl https://climbx.so/api/v1/analytics?days=30 \
-H "Authorization: Bearer climbx_sk_your_key"{
"range_days": 30,
"summary": {
"posts_published": 42,
"total_impressions": 512000,
"avg_impressions": 12190,
"avg_replies": 18,
"avg_likes": 140,
"engagement_rate_pct": 2.8
},
"by_format": [
{ "format": "hot_take", "posts": 12, "median_replies": 22, "median_impressions": 15000 }
]
}Format performance
/api/v1/analytics/formatsPer-format breakdown over a window: how many posts, their share, and the median replies and impressions, with a trend (up/down/neutral) comparing each format against your typical post in the same window. Mirrors the Format performance table in the app.
| Field | Type | Description | |
|---|---|---|---|
| start | string | optional | ISO datetime window start. Defaults to 30 days before end. |
| end | string | optional | ISO datetime window end. Defaults to now. |
generated_at is when the response was computed; as_of is the newest metrics snapshot in the window (when the numbers last moved). Max span 366 days.
curl "https://climbx.so/api/v1/analytics/formats?start=2026-05-01T00:00:00Z&end=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer climbx_sk_your_key"{
"generated_at": "2026-06-01T12:00:00Z",
"as_of": "2026-05-31T22:10:00Z",
"window": { "start": "2026-05-01T00:00:00Z", "end": "2026-06-01T00:00:00Z" },
"posts_analyzed": 42,
"median_impressions": 12000,
"rows": [
{
"format": "hot_take",
"posts": 12,
"share": 29,
"replies": { "value": 22, "trend": "up" },
"impressions": { "value": 15000, "trend": "up" }
}
]
}Niche performance
/api/v1/analytics/nichesSame shape as Format performance, bucketed by niche instead of format. Posts with no niche label come back under "__unlabeled__".
| Field | Type | Description | |
|---|---|---|---|
| start | string | optional | ISO datetime window start. Defaults to 30 days before end. |
| end | string | optional | ISO datetime window end. Defaults to now. |
curl "https://climbx.so/api/v1/analytics/niches?start=2026-05-01T00:00:00Z&end=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer climbx_sk_your_key"{
"generated_at": "2026-06-01T12:00:00Z",
"as_of": "2026-05-31T22:10:00Z",
"window": { "start": "2026-05-01T00:00:00Z", "end": "2026-06-01T00:00:00Z" },
"posts_analyzed": 42,
"median_impressions": 12000,
"rows": [
{
"niche": "saas",
"posts": 18,
"share": 43,
"replies": { "value": 20, "trend": "neutral" },
"impressions": { "value": 13500, "trend": "up" }
}
]
}Voice profile
/api/v1/voiceThe account’s voice persona, evidence-backed learnings, cadence targets, and posting schedule. Use it to draft in the owner’s voice and time posts well.
curl https://climbx.so/api/v1/voice \
-H "Authorization: Bearer climbx_sk_your_key"{
"voice": {
"persona": "lowercase, short lines, founder-to-founder...",
"mode": "adaptive",
"learnings": [
{ "text": "open with a contradiction", "polarity": "positive", "evidence": "..." }
],
"cadence": { "phase": "grow", "posts_per_day": 2, "replies_per_day": 5 },
"schedule": {
"timezone": "Europe/Copenhagen",
"active_start_hour": 8,
"active_end_hour": 22,
"weekly_slots": { "mon": ["09:00", "17:00"] }
}
}
}Ongoing learnings
/api/v1/learningsThe account’s current “Ongoing learnings” - the do-more (positive) and do-less (negative) rules ClimbX derived from the owner’s own posts, each with its evidence. last_derived_at is when the set was last recomputed.
curl https://climbx.so/api/v1/learnings \
-H "Authorization: Bearer climbx_sk_your_key"{
"generated_at": "2026-06-01T12:00:00Z",
"last_derived_at": "2026-05-30T03:00:00Z",
"learnings": {
"positive": [
{ "text": "open with a contradiction", "source": "derived", "evidence": "..." }
],
"negative": [
{ "text": "long multi-paragraph posts underperform", "source": "derived", "evidence": "..." }
]
}
}Learnings history
/api/v1/learnings/historyThe recorded timeline of the learnings set - one snapshot each time it was re-derived, within the window. Use it to chart how the rules evolved. History accrues going forward from when the feature shipped; there is no retroactive backfill.
| Field | Type | Description | |
|---|---|---|---|
| start | string | optional | ISO datetime window start. Defaults to 30 days before end. |
| end | string | optional | ISO datetime window end. Defaults to now. |
curl "https://climbx.so/api/v1/learnings/history?start=2026-05-01T00:00:00Z&end=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer climbx_sk_your_key"{
"generated_at": "2026-06-01T12:00:00Z",
"window": { "start": "2026-05-01T00:00:00Z", "end": "2026-06-01T00:00:00Z" },
"snapshots": [
{
"captured_at": "2026-05-30T03:00:00Z",
"learnings": { "positive": [ ... ], "negative": [ ... ] }
}
]
}Errors
Errors return a JSON body with an error code and a human message.
| Status | error | When |
|---|---|---|
| 400 | invalid_body / url_posts_not_allowed / invalid_id / invalid_window | Bad payload, the text contains a link, a malformed id, or a bad start/end window. |
| 401 | missing_bearer / invalid_key | No key, or the key is unknown or revoked. |
| 402 | subscription_required | The owning account is not on a plan or trial. |
| 404 | not_found | No scheduled post with that id on this account. |
| 400 | image_rejected | An image_url could not be used: bad URL, blocked host, too large, or not an image. |
| 409 | x_not_connected / x_token_expired / not_pending | Reconnect X, or the post is no longer pending (already publishing or published). |
| 429 | daily_post_cap_reached | Hit the 5/day post cap. Resets 00:00 UTC. |
| 429 | rate_limited | Too many GET requests this minute. Back off per the Retry-After header. |
| 500 | image_store_failed | Could not store a scheduled image; the post was rolled back. |
| 502 | publish_failed / media_upload_failed | X rejected the post or an image. |