# xmark - full documentation for AI agents chat with your X (Twitter) bookmarks. xmark pulls your saved tweets, embeds them with OpenAI, and answers questions in plain english using Claude, with citations linking back to the original tweet. website: https://www.xmark.dev api: https://api.xmark.dev docs: https://www.xmark.dev/docs llms.txt: https://api.xmark.dev/llms.txt openapi: https://api.xmark.dev/openapi.json status: https://www.xmark.dev/api/health ## what xmark is - a personal search-and-chat layer over your X bookmarks - semantic retrieval (pgvector + OpenAI text-embedding-3-small) over the full bookmark contents - Claude (claude-sonnet-4-6) answers grounded in your bookmarks with [N] citations - a developer platform with a REST API at api.xmark.dev, scoped API keys, and an MCP server (@jclvsh/xmark) ## what xmark is not - not a bookmark manager (read-only mirror of X bookmarks; remove unbookmarks on X too) - not an X analytics dashboard (no engagement insights or follower metrics) - not a public search engine (bookmarks are private and RLS-gated per user) ## quick start 1. sign in with Google at /auth/signup 2. connect your X account on /bookmarks (separate OAuth flow for bookmark read access) 3. click sync: xmark pulls all bookmarks and embeds new ones 4. open /chat and ask questions in plain english 5. responses cite each referenced bookmark by [N]: click the number to open the original tweet ## authentication flows xmark uses two distinct OAuth flows: 1. **Google OAuth (sign-in)**: creates the xmark account itself. Supabase Auth handles the session. 2. **X (Twitter) OAuth 2.0 with PKCE (bookmark access)**: separate flow that grants xmark read access to your bookmarks. The X token is encrypted (AES-256-GCM) and stored in connected_accounts. Tokens auto-refresh; rotated refresh tokens are saved automatically. for the developer API at api.xmark.dev, use API keys (Authorization: Bearer nt_live_*). create keys at /settings/api-keys. native clients (the xmark iOS app) instead sign in as the user and send the Supabase access JWT as the bearer token. ## pricing free tier: 20 chat messages/month, 1 sync/day, full feature access (quotas return 402/429 on overage). pro: $5/month (or $50/year), unlimited chat and sync. the same plan covers the CLI, MCP server, and developer REST API. there is no separate API tier. ## use cases xmark works well for: 1. **research assistant**: "what did I save about [topic]?" → semantic search returns top tweets with citations. 2. **knowledge-base assembly**: extract bookmarks into structured notes (Obsidian, Logseq, Notion) by topic or time window. 3. **briefing prep**: "summarize what I saved this month" → chat endpoint with month filter. 4. **citation extraction**: "find the tweet that backed claim X" → search + cite the original tweet URL. 5. **personal-history retrieval**: "that thread about Y from a few months ago" → semantic search beats keyword recall. xmark is NOT a good fit for: bulk timeline scraping, public-tweet search (use Twitter API directly), real-time monitoring (bookmarks sync on-demand). ## MCP server xmark publishes an MCP (Model Context Protocol) server for use inside Claude Code, Cursor, or any MCP-compatible client. ``` # install npx -y -p @jclvsh/xmark xmark-mcp # or run the CLI npx -y -p @jclvsh/xmark xmark ``` set `XMARK_API_KEY` (issued from /settings/api-keys) before running. tools exposed: - `sync_bookmarks`: trigger a fresh sync from X - `search_bookmarks`: semantic search query - `chat`: ask a natural-language question with citations - `list_bookmarks`: paginated bookmark list - `get_bookmark`: fetch a single bookmark - `delete_bookmark`: remove a bookmark (local-only, or also on X with unbookmark) - `list_conversations`: past chat threads - `get_conversation_messages`: a conversation's message history - `delete_conversation`: permanently delete a conversation - `get_usage`: plan + quota usage ## how xmark compares to alternatives - **native X bookmarks UI**: chronological list, keyword-only. xmark adds semantic search, chat, citations, and a developer API. - **Pocket / Reader**: read-it-later for articles. xmark is X-bookmarks-only with embedding-based search and chat. - **Readwise**: highlights from books/PDFs/articles. xmark covers a different surface (your X bookmarks) and adds chat. - **Glasp**: social highlighting on the web. xmark is private semantic search over your own X library. - **Composio / generic agent toolkits**: broad multi-platform tools. xmark is a single-surface specialist: bookmarks + chat. ## FAQ ### getting started Q: what is xmark? A: xmark lets you chat with your X (Twitter) bookmarks. you sign in, connect your X account, sync your bookmarks, and ask questions in plain english. xmark uses semantic search over the full bookmark contents (text, quoted tweets, linked articles, image alt text) to answer. Q: how do I sign up? A: go to /auth/signup and sign in with Google. once you're in, head to /bookmarks and connect your X account via the 'connect' button. xmark uses two separate OAuth flows: Google for the xmark account itself, X for read access to your bookmarks. Q: do I need an X (Twitter) account? A: yes. xmark reads your X bookmarks via the X API, so you need an X account with bookmarks already saved. if you've never bookmarked anything on X, there's nothing for xmark to chat with. Q: what plan do I need? A: xmark is a single $5/month plan. no free tier, no upgrades. one price covers unlimited bookmark sync, unlimited chat messages, and access to all features. ### bookmarks sync Q: how do I sync my bookmarks? A: open /bookmarks and click 'sync'. xmark pulls your bookmarks from X via the X API and embeds new ones with OpenAI's text-embedding-3-small for semantic search. the first sync may take a minute if you have hundreds of bookmarks; subsequent syncs only process new ones. Q: is sync automatic? A: no. sync is manual: you click the button when you want fresh bookmarks. this keeps API usage predictable and avoids hitting X's rate limits unnecessarily. Q: what gets synced? A: the full tweet text (including long-form note tweets), author info, engagement metrics, quoted tweets, image alt text, linked article metadata (title + description), and topic annotations. all of this becomes searchable in chat. Q: X says my account 'needs to be reconnected', what now? A: X's OAuth tokens expire and rotate. when xmark can't refresh your token, you'll see a 'reconnect' prompt on /bookmarks or /settings. click reconnect to re-authorize. xmark detects rotated refresh tokens and saves them automatically going forward. Q: can I delete a bookmark? A: yes. clicking the remove icon on a bookmark unbookmarks it on X AND removes it from xmark in one step. both must succeed: leaving X bookmarked while removing locally would let the next sync re-create it. ### chat Q: how does chat work? A: you ask a question, xmark embeds it with OpenAI, retrieves the most relevant bookmarks via pgvector cosine similarity, then asks Claude (claude-sonnet-4-6) to answer using those bookmarks as context. responses cite each bookmark by number - [1], [2] - so you can click through to the original tweet. Q: how many bookmarks does chat consider? A: chat retrieves up to 800 of your most relevant bookmarks per query, ranked by semantic similarity to your question. if you have more, the lowest-ranked are excluded for that specific query, not deleted. Q: why is chat citing the same tweet multiple times? A: Claude is told to cite every bookmark it references with a number. if a single bookmark is the best match for several parts of an answer, you'll see the same number repeated. clicking any instance of [N] opens the original tweet. Q: do you train models on my bookmarks? A: no. your bookmarks are sent to OpenAI for embeddings (one-shot, not retained per OpenAI's API terms) and to Anthropic for chat answers (one-shot, not retained per Anthropic's API terms). xmark stores embeddings + bookmark text in your private Supabase row, gated by row-level security. ### privacy and data Q: where is my data stored? A: in xmark's Supabase Postgres database, encrypted at rest. row-level security policies ensure only your authenticated session can read your bookmarks and conversations. nobody else - no admin, no other user - can see your data. Q: what happens to my X access token? A: X tokens are encrypted with AES-256-GCM before being written to the database. only the running app can decrypt them; database backups don't expose plaintext tokens. Q: how do I delete my account? A: go to /settings and click 'delete account'. xmark soft-deletes your profile + bookmarks + conversations and hard-deletes your active credentials (X OAuth tokens, API keys). signing back in within the retention window restores your account; after that, the soft-deleted rows are purged. ### billing Q: what does the $5/month plan include? A: unlimited bookmark sync, unlimited chat messages, full access to every feature. the plan is the only paid tier. there are no upsells or per-message overages. Q: how do I cancel? A: go to /settings → billing and click 'manage subscription'. you'll be redirected to Stripe's customer portal where you can cancel at the end of the current period. cancellation stops the next charge; you keep access until the period ends. Q: do you offer refunds? A: if you cancel within the first 7 days of a new subscription, email josh@jclvsh.art and we'll refund your most recent payment. after that, cancellations stop future charges but the current period isn't refunded. ### troubleshooting Q: sync is stuck or won't start A: xmark uses a Redis lock to prevent concurrent syncs for the same account. if a previous sync hung, the lock auto-expires after 30 seconds: wait, then click sync again. if it still won't start, your X token may have expired (see the reconnect prompt). Q: chat says 'subscription required' but I just paid A: Stripe webhooks usually arrive within seconds of checkout, but rarely take a minute. refresh /chat after 60 seconds; if you still see the message, contact support with your Stripe email and we'll reconcile the subscription manually. Q: I'm getting rate-limited by X A: xmark uses X's pay-per-use API tier with strict rate limits. if you hammer the sync button or run many parallel sessions, X may briefly 429 you. wait a few minutes and try again. xmark treats 429 as 'X is healthy, just busy'. it's not a permanent error. --- # developer API reference ## base URL ``` https://api.xmark.dev ``` legacy host `https://xmark.dev/api/v1/*` still responds for backwards compatibility, but `api.xmark.dev` is canonical. ## authentication all requests require a bearer token: ``` Authorization: Bearer nt_live_YOUR_API_KEY ``` create API keys at https://www.xmark.dev/settings/api-keys. native clients signed in via Supabase may instead send the user's access JWT as the bearer token. it authenticates as the account owner with all scopes. ## scopes API keys carry scoped permissions. grant the minimum needed. | scope | description | |-------|-------------| | read | read-only access to owned resources | | write | create, update, and delete owned resources (soft-deletes treated as writes) | | generate | AI/ML or other expensive compute operations | | send | outbound communication (email, DM, post, webhook delivery) | | admin | manage API keys and webhook endpoints | default scopes (if none specified): `read`, `write`, `generate`, `send`. scopes use the canonical five-verb vocabulary only (`read`, `write`, `generate`, `send`, `admin`); legacy values are fully retired. ## endpoints ### bookmarks read, search, and sync the authenticated user's X bookmarks. #### GET /bookmarks paginated list of the caller's bookmarks, newest first **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "tweet_id": "1234567890", "text": "full tweet text, including long-form note tweets", "author": { "id": "string | absent", "username": "jclvsh", "name": "josh", "profile_image_url": "https://... | null" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/jclvsh/status/1234567890", "public_metrics": { "like_count": 42, "retweet_count": 7 }, "media": [ { } ] } ], "pagination": { "page": 1, "pageSize": 50, "total": 1234, "totalPages": 25 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### GET /bookmarks/{id} fetch a single bookmark **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | bookmark id | **response (200):** ```json { "success": true, "data": { "id": "uuid", "tweet_id": "1234567890", "text": "full tweet text, including long-form note tweets", "author": { "id": "string | absent", "username": "jclvsh", "name": "josh", "profile_image_url": "https://... | null" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/jclvsh/status/1234567890", "public_metrics": { "like_count": 42, "retweet_count": 7 }, "media": [ { } ] } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 500 SERVER_001: internal server error #### DELETE /bookmarks/{id} remove a bookmark. local-only by default (the next sync re-creates anything still bookmarked on X); pass unbookmark=true to also remove it on X **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | bookmark id | | unbookmark | boolean | no | also remove the bookmark on X via the stored OAuth token (default: false) | **response (200):** ```json { "success": true, "data": { "removed": true } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 400 SERVER_002: x account needs to be reconnected (unbookmark=true only) - 500 SERVER_001: internal server error #### POST /bookmarks/search semantic search over the caller's bookmark library (pgvector + OpenAI embeddings) **scope:** `read` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | query | string (1-2000) | yes | natural-language search query | | limit | integer (1-50) | no | max results (default: 10) | **response (200):** ```json { "success": true, "data": [ { "bookmark": { "id": "uuid", "tweet_id": "1234567890", "text": "...", "author": { "username": "jclvsh", "name": "josh" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/..." }, "score": 0.83 } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 500 SERVER_001: internal server error #### POST /bookmarks/sync pull the latest bookmarks from the connected X account, embed new ones, and store them. large libraries sync in chunks: call again while done is false. when X rate-limits the crawl (10 req / 15 min) the response carries retryAfterSeconds: wait that long before the next call, and the sync resumes where it stopped **scope:** `write` **response (200):** ```json { "success": true, "data": { "count": 1234, "newCount": 12, "removedCount": 3, "durationMs": 8200, "done": true } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_002: x account needs to be reconnected - 409 SERVER_012: a sync is already in progress - 429 BILLING_006: free-tier daily sync limit reached (1/day) - 500 SERVER_001: internal server error ### chat ask natural-language questions about bookmarks; Claude answers with citations grounded in them. #### POST /chat submit a question. xmark retrieves the most relevant bookmarks via semantic search and has Claude generate an answer with inline citations. returns a single JSON response **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | question | string (1-8000) | yes | the question to ask | | conversationId | UUID | no | continue an existing conversation | | maxCitations | integer (1-20) | no | cap the returned citations (all citations are returned when omitted) | **response (200):** ```json { "success": true, "data": { "answer": "string", "citations": [ { "bookmark_id": "uuid", "tweet_url": "https://x.com/...", "author": "jclvsh", "snippet": "..." } ], "conversation_id": "uuid" } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 402 BILLING_006: free-tier monthly chat limit reached (20/month) - 429 BILLING_006: free-tier concurrent-conversation limit reached - 500 SERVER_001: internal server error **notes:** - native clients use POST /chat/stream instead: its body is { message, conversationId? } (NOT this route's question/maxCitations shape), answered as a text/event-stream of JSON frames (meta, delta, error, done). streaming is outside the OpenAPI codegen contract ### conversations list, read, and delete past chat conversations. #### GET /conversations paginated list of the caller's conversations, most recently active first **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "title": "string | null", "created_at": "ISO 8601", "updated_at": "ISO 8601" } ], "pagination": { "page": 1, "pageSize": 50, "total": 3, "totalPages": 1 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### GET /conversations/{id}/messages the conversation's message history, oldest first. assistant messages carry the IDs of the bookmarks retrieved for that answer **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | conversation id | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "role": "user", "content": "string", "retrieved_bookmark_ids": null, "created_at": "ISO 8601" }, { "id": "uuid", "role": "assistant", "content": "string", "retrieved_bookmark_ids": ["uuid"], "created_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 500 SERVER_001: internal server error #### DELETE /conversations/{id} permanently delete a conversation and its messages **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | conversation id | **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 500 SERVER_001: internal server error ### usage plan and quota usage for the authenticated user. #### GET /usage the caller's plan, chat-message usage this month, syncs today, bookmark count, and whether an X account is connected. limit and remaining are null on unlimited plans **scope:** `read` **response (200):** ```json { "success": true, "data": { "plan": "free", "x_connected": true, "bookmarks": { "total": 1234 }, "chat": { "used": 4, "limit": 20, "remaining": 16 }, "syncs_today": { "used": 1, "limit": 1, "remaining": 0 } } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### insights bookmark analytics: velocity, top authors/topics, early calls, and follow recommendations. #### GET /insights headline stats, weekly bookmark velocity, top authors and topics, 'early calls' (bookmarks saved early that gained traction), and follow recommendations. early calls + recommendations are tier-limited (free: 3, paid: 10) **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### notifications the caller's notification inbox. #### GET /notifications paginated list of the caller's notifications, newest first **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### PATCH /notifications/{id} mark a notification read **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | notification id | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 500 SERVER_001: internal server error #### DELETE /notifications/{id} delete a notification **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | notification id | **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found - 500 SERVER_001: internal server error #### POST /notifications/read-all mark all of the caller's unread notifications read **scope:** `write` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### connections connect and disconnect the caller's X account (native OAuth). #### POST /connections/x/connect start native X OAuth: returns an authorization URL to open in an ASWebAuthenticationSession. the backend callback exchanges the code server-side and redirects to the app scheme **scope:** `write` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### DELETE /connections/x disconnect the X account (revoke + clear stored OAuth tokens). reconnect via /connections/x/connect **scope:** `write` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### account profile, onboarding, settings, and notification preferences. #### GET /me the authenticated user's profile + onboarding state (used for post-login routing) **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### POST /onboarding/complete mark onboarding complete **scope:** `write` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### POST /onboarding/heard-from record how the user heard about xmark (the onboarding self-report). write-once: recorded is false when an answer is already on file **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | source | "tiktok" | "instagram" | "x" | "reddit" | "youtube" | "google" | "chatgpt" | "friend" | "other" | yes | one of the allowlisted sources | **response (200):** ```json {"data":{"recorded":true}} ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_002: source not in the allowlist - 500 SERVER_001: internal server error #### DELETE /account delete the account (soft-delete data, cancel any subscription) **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### GET /account/duplicate an older xmark account connected to the same X identity as this one, if any (a second sign-in provider on iOS is the usual cause); the duplicate key is absent when there is nothing to merge **scope:** `read` **response (200):** ```json {"data":{"duplicate":{"source_user_id":"b8a5e6b0-8f67-45f2-b296-e99696775675","platform":"twitter","platform_username":"glowspin","bookmark_count":4399,"last_synced_at":"2026-07-26T22:42:14Z","connected_at":"2026-07-26T22:42:10Z"}}} ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### POST /account/merge fold the older account into this one - its bookmarks, folders, chats, and wrapped move here (tweets you already have are kept as-is) and the older account is retired **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | source_user_id | uuid | yes | the account reported by GET /account/duplicate | **response (200):** ```json {"data":{"source_user_id":"b8a5e6b0-8f67-45f2-b296-e99696775675","platform_username":"glowspin","moved_bookmarks":1196,"duplicate_bookmarks":3203,"moved_folders":0,"merged_folders":0,"moved_conversations":0,"moved_snapshots":0}} ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 403 AUTH_002: that account does not share an x identity with this one - 500 SERVER_001: internal server error **notes:** - the older account must hold the same X identity you are connected with right now - that authorization is the ownership proof - billing does not merge: an older account with a live paid subscription is refused until it is cancelled or moved #### GET /notification-settings the caller's email-notification preference **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### PATCH /notification-settings update the email-notification preference **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | email_enabled | boolean | yes | whether email notifications are on | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 500 SERVER_001: internal server error #### GET /settings the caller's profile settings (auto-cleanup window, public-profile toggle, username) **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### PATCH /settings update profile settings **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | auto_delete_bookmarks_after_days | integer | no | auto-delete window: null (off), 90, 180, or 365 | | public_bookmarks_enabled | boolean | no | enable the public /u/ profile (requires a username first) | | username | string | no | 3-20 chars [a-z0-9_-], unique | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 500 SERVER_001: internal server error ### billing subscription management and Apple receipt verification. #### POST /billing/portal a billing-management URL: a pre-authenticated Stripe customer portal for subscribers, or the web billing page **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### POST /billing/apple/verify verify a StoreKit 2 signed transaction server-side and grant the plan **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | signed_transaction | string | yes | the StoreKit 2 signed transaction JWS | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 500 SERVER_001: internal server error ### referrals the 'share your wrapped on X for 2 months free' referral. #### GET /referrals the caller's referral / wrapped-share bonus status **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error #### POST /referrals/share post the wrapped summary to the connected X account and grant the one-time bonus **scope:** `send` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | message | string | no | optional custom share text | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### wrapped the caller's 'xmark wrapped' aggregate stats. #### GET /wrapped wrapped stats: totals, top authors, top topics, languages **scope:** `read` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 SERVER_001: internal server error ### API keys manage API keys programmatically. key-management endpoints require the admin scope. #### GET /keys list the caller's API keys, newest first **scope:** `admin` **response (200):** ```json { "data": [ { "id": "uuid", "name": "string", "key_prefix": "xm_live_abc1", "scopes": ["read", "write"], "last_used_at": "ISO 8601 | null", "revoked_at": "ISO 8601 | null", "created_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token #### POST /keys create a new API key: the raw key is returned once **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string (1-100) | yes | key name | | scopes | string[] | no | subset of read/write/generate/send/admin (defaults to ['read','write'] when omitted - pass the full list explicitly if the key needs 'generate' or 'send') | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body #### GET /keys/me introspect the calling API key (no scope required; JWT principals get 404) **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### GET /keys/:id fetch a specific key's metadata **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### PATCH /keys/:id rename a key **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string (1-100) | yes | new key name | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 404 SERVER_003: resource not found #### DELETE /keys/:id revoke a key (204 No Content) **scope:** `admin` **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### POST /keys/:id/rotate rotate a key: revoke it and mint a replacement with the same name and scopes **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found ### webhooks developer webhook endpoints with signed deliveries. all endpoints require the admin scope. #### GET /webhooks list the caller's webhook endpoints, newest first **scope:** `admin` **response (200):** ```json { "data": [ { "id": "uuid", "url": "https://example.com/hooks/xmark", "events": ["*"], "active": true, "created_at": "ISO 8601", "updated_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token #### POST /webhooks create a webhook endpoint: the signing secret is returned once **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | string (https url) | yes | delivery endpoint | | events | string[] | yes | event types to deliver, or ["*"] for all | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body #### GET /webhooks/:id fetch a webhook endpoint **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### PATCH /webhooks/:id update url, events, or active **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | string (https url) | no | new delivery endpoint | | events | string[] | no | new event list | | active | boolean | no | pause/resume deliveries | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_005: invalid json body - 404 SERVER_003: resource not found #### DELETE /webhooks/:id delete a webhook endpoint (204 No Content) **scope:** `admin` **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### GET /webhooks/:id/deliveries paginated delivery log, newest first **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### POST /webhooks/:id/rotate-secret rotate the signing secret: the new secret is returned once **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found #### POST /webhooks/:id/test fire a signed test event and report the delivery outcome **scope:** `admin` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 SERVER_003: resource not found ### changelog machine-readable release history - public, no API key required. #### GET /changelog release history newest-first: version, date, type (feature/fix/breaking/docs), summary, and migration notes on breaking entries. public - the one-line X-Notice header on every response is the latest entry's summary; this is the structured history behind it **response (200):** ```json { "success": true, "data": [ { "version": "1.0.0", "date": "2026-07-21", "type": "feature", "summary": "..." } ] } ``` ## error codes all errors return: `{ success: false, error: { code, message, requestId } }` the `X-Request-ID` response header carries the same id for log correlation. ### authentication | code | HTTP | description | |------|------|-------------| | AUTH_001 | 401 | missing or invalid authorization header | | AUTH_002 | 403 | API key missing required scope | | AUTH_004 | 401 | invalid or revoked API key | ### billing | code | HTTP | description | |------|------|-------------| | BILLING_001 | 402 | no active subscription | | BILLING_006 | 402 | plan usage limit reached (402 on chat quota, 429 on sync rate) | | BILLING_007 | 402 | subscription required for this operation | | BILLING_008 | 403 | current plan does not include this feature | ### rate limiting | code | HTTP | description | |------|------|-------------| | RATE_001 | 429 | too many requests - back off and retry | ### validation & server | code | HTTP | description | |------|------|-------------| | SERVER_001 | 500 | internal server error | | SERVER_002 | 400 | bad request (malformed body or missing field) | | SERVER_003 | 404 | resource not found | | SERVER_004 | 405 | method not allowed | | SERVER_005 | 400 | validation failed | | SERVER_008 | 500 | database error | | SERVER_012 | 409 | conflict (e.g. concurrent modification) | ### webhooks | code | HTTP | description | |------|------|-------------| | HOOK_001 | 401 | invalid webhook signature | | HOOK_002 | 500 | webhook processing failed | | HOOK_004 | 408 | webhook delivery timed out | ## changelog machine-readable history at `GET /changelog` (public, no key). recent releases: - 0.17.0 (2026-09-14, breaking): POST /account/merge no longer returns moved_projects (deprecated in 0.16.0, where it was already a constant 0) — migration: stop reading moved_projects from the merge response; every other field is unchanged - 0.16.0 (2026-09-13, fix): the template's project.* webhook events, which xmark never emitted, are no longer subscribable (POST /webhooks rejects them); POST /account/merge keeps moved_projects in the response as a deprecated constant 0 for clients built against 0.13-0.15 - 0.15.0 (2026-09-04, fix): query-parameter defaults are emitted with their real types (page 1, pageSize 50, unbookmark false as numbers/booleans, not strings) via the shared type emitter; no endpoint or response changes - clients regenerated from the spec get typed defaults - 0.14.0 (2026-08-25, feature): POST /onboarding/heard-from records the native onboarding's self-reported acquisition source, write-once on the profile (the same dimension the web onboarding select feeds) - 0.13.0 (2026-08-25, feature): GET /account/duplicate reports an older account connected to the same X identity; POST /account/merge folds it into the current one (bookmarks, folders, chats, projects, wrapped) and retires it; GET /me returns AUTH_005 when the signed-in profile was merged away - 0.12.0 (2026-07-21, feature): GET /changelog (this endpoint) + llms.txt changelog section - 0.11.0 (2026-07-21, feature): GET /usage reports remaining on chat + syncs_today (null = unlimited) - 0.10.1 (2026-07-21, feature): X-Xmark-Notice header on every response + Deprecation/Sunset machinery