tweetsmash-logo

Developer portal

Everything saved in a Tweetsmash library is reachable by software: a REST API at https://api.tweetsmash.com/v1, an MCP server at https://mcp.tweetsmash.com/api/mcp, and a keyless sandbox that answers with the same shapes so you can build the integration before you point it at anyone's real bookmarks.

Quickstart

Three requests, in order. The first needs nothing at all.

1. Call the sandbox, no key needed

curl https://api.tweetsmash.com/v1/sandbox/bookmarks

Same envelope, same pagination meta, same field names as the real endpoint. Nothing behind it is real.

2. Create a key and call the real thing

Generate a key on the API integrations page, then send it as a bearer token.

curl https://api.tweetsmash.com/v1/bookmarks?limit=5 \
  -H "Authorization: Bearer $TWEETSMASH_API_KEY"
{
  "status": true,
  "data": [ { "id": "...", "text": "...", "author": { "username": "..." } } ],
  "meta": { "count": 5, "next_cursor": "5" }
}

3. Point an agent at it

Add the MCP server to any client that speaks the Model Context Protocol and the same library shows up as tools. Configuration is in the MCP section below.

API keys

Keys are created, listed and revoked on the API integrations page of your account. A key is shown once at creation; store it somewhere you can read it back, because we cannot show it to you again. Send it on every request as Authorization: Bearer <key>.

Revoking a key on that page takes effect immediately. If a key is missing or revoked the API answers 401 with a WWW-Authenticate challenge pointing at the protected-resource metadata, so an agent can discover how to authenticate without being told.

One honest limitation: a key today carries every permission the account has. Per-key scopes are not implemented yet, and the metadata document says so rather than advertising scopes that are not enforced. Treat a Tweetsmash key as full account access and store it accordingly.

The full machine-readable handshake — discover, register, use, revoke — is documented at auth.md.

Sandbox

https://api.tweetsmash.com/v1/sandbox mirrors the documented public surface and answers from fixtures. It takes no key, reads no account and writes nothing. Every response carries X-Tweetsmash-Environment: sandbox and a meta.sandbox: true flag, so a client can never mistake it for production.

Available there: GET /bookmarks, GET /bookmarks/:id, GET /labels, POST /labels/add, POST /labels/remove, POST /labels/batch, GET /views, GET /views/:slug, GET /views/:slug/bookmarks, the three POST /exports/* routes and GET /exports/:job_id.

Rehearsing failures

You cannot write a retry path you have never seen fire. Send X-Tweetsmash-Sandbox-Error and the sandbox returns that failure on demand, with the real status code, the real error code and the real headers.

curl https://api.tweetsmash.com/v1/sandbox/bookmarks \
  -H "X-Tweetsmash-Sandbox-Error: rate_limited" -i

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-Tweetsmash-Environment: sandbox

{ "status": false, "code": "RATE_LIMITED", "error": "Rate limited (simulated)" }

Supported values: unauthorized, subscription_required, filter_not_allowed, pagination_not_allowed, invalid_request, not_found, rate_limited, server_error. An unknown value returns 400 listing the ones that work.

MCP server

The MCP server exposes the same library as tools — list, search, label, build Smart Views, trigger exports, read digests. It speaks streamable HTTP and authenticates with the same API key.

{
  "mcpServers": {
    "tweetsmash": {
      "url": "https://mcp.tweetsmash.com/api/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}

For clients that only launch local processes, the same server ships on npm:

npx -y @tweetsmash/mcp

Every tool carries MCP annotations, so a client knows before it calls whether a tool only reads, whether it destroys data, and whether it reaches outside your library. The full tool list with those annotations is in the server card. Setup walkthrough: X Bookmarks MCP server.

Reference and machine-readable specs

Human reference: API documentation. Everything else is written for software:

Errors

Every failure returns the same envelope: status: false, a stable machine code, and error holding a sentence written for a person. Branch on code, never on error — the sentence can change, the code will not.

{ "status": false, "code": "SUBSCRIPTION_REQUIRED",
  "error": "A paid plan is required to modify labels.",
  "upgrade_url": "https://www.tweetsmash.com/pricing" }
CodeHTTPWhat to do
UNAUTHORIZED401The key is missing, malformed or revoked. Check the Authorization header.
SUBSCRIPTION_REQUIRED402The endpoint needs a paid plan. The response carries upgrade_url.
FILTER_NOT_ALLOWED402Filtering is a paid capability. Retry without the filter, or upgrade.
PAGINATION_NOT_ALLOWED402Paging past the free window needs a paid plan.
VECTOR_SEARCH_NOT_ALLOWED402Semantic search needs a paid plan and a built vector store.
INVALID_REQUEST400A parameter is missing or the wrong shape. The error sentence names it.
NOT_FOUND404No such bookmark, label or Smart View on this account.
RATE_LIMITED429Slow down and retry after the Retry-After header.
INTERNAL_ERROR500Our fault. Safe to retry; use an Idempotency-Key on writes.

Every 402 also carries a top-level upgrade_url. An agent that hits a paywall should surface that link to its user rather than retrying.

Idempotent writes

Send an Idempotency-Key header on any POST, PUT or PATCH. The first request with a given key is executed and its response stored for 24 hours; a repeat of the same key returns that stored response and sets Idempotency-Replayed: true instead of doing the work twice.

curl -X POST https://api.tweetsmash.com/v1/labels/add \
  -H "Authorization: Bearer $TWEETSMASH_API_KEY" \
  -H "Idempotency-Key: 6f3a9c2e-1b7d-4a55-9f10-2c8e5b0d4471" \
  -H "Content-Type: application/json" \
  -d '{"tweet_ids":["1000000000000000001"],"label_name":"ai"}'

Reusing a key with a different body is a bug on the caller's side, so it is rejected with 422 rather than silently replayed. Keys are scoped to your account. Use a fresh UUID per logical operation, and reuse it only when retrying that same operation.

Long-running exports

Exports do not block. POST /exports/pdf, /exports/csv and /exports/json answer 202 Accepted with a Location header, a Retry-After, and a poll_url in the body. Poll GET /exports/{job_id} until status is completed, then read download_url.

POST /v1/exports/pdf  ->  202 Accepted
Location: /v1/exports/job_01H...
Retry-After: 5

GET /v1/exports/job_01H...  ->  200 { "status": "pending" }
GET /v1/exports/job_01H...  ->  200 { "status": "completed", "download_url": "..." }

Honour Retry-After. Download links are short-lived; fetch the file when the job completes rather than storing the URL.

Batch operations

POST /labels/batch takes up to 50 label operations in one request so an agent tidying a library does not make fifty round trips. Each operation reports its own result, and a request where some operations failed returns 207 Multi-Status rather than pretending the whole thing succeeded or the whole thing failed.

POST /v1/labels/batch
{
  "operations": [
    { "op": "add",    "tweet_ids": ["100..."], "label_name": "ai" },
    { "op": "remove", "tweet_ids": ["200..."], "label_name": "todo" }
  ]
}

207 Multi-Status
{
  "status": true,
  "data": {
    "results": [
      { "index": 0, "op": "add", "ok": true, "affected": 1, "data": [ ... ] },
      { "index": 1, "op": "remove", "ok": false,
        "error": { "code": "INVALID_REQUEST", "message": "label_name is required." } }
    ],
    "succeeded": 1, "failed": 1
  },
  "meta": { "message": "1 of 2 operations failed." }
}

Limits

There is no request-rate limit on the API today, and we would rather say that than publish RateLimit-* headers describing a ceiling that does not exist. What does gate you is your plan: free accounts get a small number of API calls and cannot use filtering, pagination beyond the first window, or semantic search. Those refusals arrive as 402 with the code that names which capability was blocked, never as a silent empty list.

Be a good client anyway: keep concurrency modest, back off on 5xx, and honour Retry-After if you ever receive it. If we introduce rate limiting we will ship the standard headers and announce it under the policy below.

Versioning and deprecation

The API is versioned in the path. /v1 is the current and only version.

What will not break inside a version

Within /v1 we will not remove a field, rename a field, change the type of a field, remove an endpoint, or add a required request parameter. Treat these as non-breaking and build to tolerate them: new fields appearing in a response, new optional request parameters, new error codes, new enum values, and changes to the human-readable error sentence.

How a deprecation is announced

A deprecated endpoint keeps working and starts answering with two headers: Deprecation carrying the date the deprecation took effect, and Sunset carrying the date it will stop working, both as HTTP dates. A Link header with rel="deprecation" points at the note explaining what to move to.

Deprecation: Wed, 01 Oct 2026 00:00:00 GMT
Sunset: Sun, 29 Mar 2027 00:00:00 GMT
Link: <https://www.tweetsmash.com/developers#versioning>; rel="deprecation"

The minimum window between Deprecation and Sunset is 180 days. Removing a whole version means a new path prefix, the old prefix marked deprecated on the same terms, and email to every account with a key that called it in the previous 90 days. No endpoint is ever removed without a Sunset header having been served first.

Support

Questions, a bug in the API, or something in these docs that does not match what the server actually did: support@tweetsmash.com. Include the endpoint, the error code and roughly when it happened.

API reference · Bookmarks API · MCP server · Pricing · Terms