# info.summary
Read, label, filter and export your saved X (Twitter) posts.

# info.description
The Tweetsmash REST API returns the X (Twitter) posts a user has saved to Tweetsmash, as JSON with unrolled threads and labels, and lets a caller label, filter and export them.

It reads only the authenticated account's own saved posts. It cannot search public X, and it cannot post, reply, like or follow.

Authentication is a Bearer API key created at https://www.tweetsmash.com/integrations/api. See https://www.tweetsmash.com/auth.md.

The same account is also reachable over the Model Context Protocol at https://mcp.tweetsmash.com/api/mcp — see https://www.tweetsmash.com/.well-known/mcp/server-card.json.

Versioning: the version lives in the path (/v1). Inside a version we will not remove or rename a field, change a field's type, remove an endpoint, or add a required parameter. New fields, new optional parameters, new error codes and new enum values are non-breaking and can appear at any time. A deprecated endpoint keeps working and starts answering with Deprecation and Sunset headers plus a Link rel="deprecation" pointing at the replacement, with at least 180 days between the two dates. See https://www.tweetsmash.com/developers#versioning.

# servers[].description
Production. Reads and writes the authenticated account.

# servers[].description
Sandbox. Same shapes, fixture data, no API key required. Reads and writes nothing. Send X-Tweetsmash-Sandbox-Error to force a specific failure.

# tags[].description
Turn a selection of saved posts into a file.

# paths./bookmarks.get.description
Retrieve all the bookmarks that you've created. You can use the query parameters to filter and paginate results. When a saved post is (or quotes) an X Article, `tweet_details.article` contains the article object (title, plain_text, cover image and related metadata); it is `null` for regular posts.

# paths./bookmarks.get.parameters[].description
The maximum number of bookmarks to return. Default is 20, maximum is 100.

# paths./bookmarks.get.parameters[].description
Cursor for pagination. Use the next_cursor value from a previous response.

# paths./bookmarks.get.parameters[].description
Filter by unread status. Default is true.

# paths./bookmarks.get.parameters[].description
Filter by archived status. Default is true (hides archived bookmarks).

# paths./bookmarks.get.parameters[].description
Filter by media type (e.g., 'article', 'video', 'image').

# paths./bookmarks.get.parameters[].description
Filter bookmarks posted after this date/time (ISO 8601 format).

# paths./bookmarks.get.parameters[].description
Filter bookmarks posted before this date/time (ISO 8601 format).

# paths./bookmarks.get.parameters[].description
Filter bookmarks created after this date/time (ISO 8601 format).

# paths./bookmarks.get.parameters[].description
Filter bookmarks created before this date/time (ISO 8601 format).

# paths./bookmarks.get.parameters[].description
Field to sort by (e.g., 'created_at', 'title').

# paths./bookmarks.get.parameters[].description
Value of the sort field from the last item of the previous page, used for stable sorting with pagination.

# paths./bookmarks.get.parameters[].description
Search query to filter bookmarks by title or content.

# paths./bookmarks.get.parameters[].description
Semantic search term for finding relevant bookmarks using vector search.

# paths./bookmarks.get.responses.200.content.application/json.example.data[].tweet_details.text
friendly reminder: productizing AI agents is the biggest zero-to-one play on the internet right now.

step 1

start with the workflow. open someone's "how-to" loom, watch for any section where the mouse repeats itself for more than 30 seconds. that is your extraction point. i call this step looking for drudge. 

step 2

add the context. scrape the internal FAQ, the tribal acronyms, the edge-case screenshots that a general LLM will never see. that proprietary mess formatted into plain text or json is your first moat. sometimes you need an audience for this. it helps. usually i create an audience/community to start.

step 3

wire up an agent on an LLM or agent platform: claude, gpt-4o, gemini, lindy, string dot com. keep the prompt brutal-simple: "given this context and that input, return this output." test until the agent's average response time beats the human baseline by 10×. keep refining prompt, it will get longer, longer and more valuable. never share that prompt with anyone.

step 4

wrap the agent in the thinnest UI possible. one drop zone, one action button, one log window that says "done." hide the settings; hide the model; hide the magic. outcome is the only feature. make a brand people fall in love with.

step 5

price the saved hours. take the median hourly cost of the person who used to run the workflow, multiply by the time you shave off each week, and anchor the subscription somewhere south of pain but north of hobby.

step 6

ship video proof before a landing page. record old way vs. new way in a split-screen clip; caption it; post it where the operators hang out. every "how did you do that?" reply is a warm lead. you can use platforms like lindy ai etc to do cold dm/email automation.

step 7

loop usage back into the context file. new ticket? new edge case? append it. your agent improves without code, and the context corpus becomes asset #2.

step 8

layer distribution inside the product. add a quiet "powered by" footer on every output, or trigger a shareable report by default. users spread the gospel while you sleep.

step 9

when churn stabilizes and support tickets hit near-zero, bolt on adjacent micro-agents each with its own promise, its own subscription tier, no extra complexity in the UI.

step 10

ignore vanity metrics. screenshots of ARR can be faked (and are constantly). My favorite was watching someone screenshot test payments. Literally fake money lol. 

Anyways, time returned to a paying customer cannot be faked. compound that saved time across niches until you own a stack of invisible employees.

you own a portfolio of little products that save people time and you get paid for it. the market for this is in the billions.

do this on repeat and you won't need funding, permission, or glossy charts.

you will be free and be creating impact.

this is probably one of the biggest opportunities out there.

# paths./bookmarks.get.responses.402.description
The account is on a plan that does not include this endpoint.

# paths./labels.get.description
Retrieve all labels associated with your account and their usage counts.

# paths./labels/add.post.description
Add labels to specific tweets to organize your bookmarks.

# paths./labels/add.post.responses.200.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result rather than executed again.

# paths./labels/add.post.requestBody.content.application/json.schema.properties.tweet_ids.description
Non-empty array of bookmark (tweet) ids to label.

# paths./labels/add.post.requestBody.content.application/json.schema.properties.label_id.description
Id of an existing label. Either label_id or label_name is required.

# paths./labels/add.post.requestBody.content.application/json.schema.properties.label_name.description
Name of a label; created if it does not exist. Either label_id or label_name is required.

# paths./labels/remove.post.responses.200.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result rather than executed again.

# paths./labels/remove.post.requestBody.content.application/json.schema.properties.tweet_ids.description
Non-empty array of bookmark (tweet) ids to remove the label from.

# paths./bookmarks/{id}.get.description
Retrieve a single bookmark with its FULL content. By default the response includes the X Article body, the unrolled thread, and notes; use the `include` query parameter to narrow or add replies. The extra content is returned under `full_content`.

# paths./bookmarks/{id}.get.parameters[].description
Comma-separated list of extra content to include: article,thread,notes,replies. Defaults to article,thread,notes.

# paths./labels/add-by-filter.post.description
Add one label to EVERY bookmark matching a filter, in a single request — no need to list and loop. Returns how many bookmarks matched and were tagged.

# paths./labels/add-by-filter.post.responses.200.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result rather than executed again.

# paths./labels/add-by-filter.post.responses.402.description
The account is on a plan that does not include this endpoint.

# paths./labels/add-by-filter.post.responses.402.content.application/json.example.error
A paid plan is required to modify labels.

# paths./labels/add-by-filter.post.requestBody.content.application/json.schema.properties.label_id.description
Id of an existing label. Either label_id or label_name is required.

# paths./labels/add-by-filter.post.requestBody.content.application/json.schema.properties.label_name.description
Name of a label; created if it does not exist. Either label_id or label_name is required.

# paths./labels/add-by-filter.post.requestBody.content.application/json.schema.properties.search_query.description
Free-text query the bookmarks must match.

# paths./labels/add-by-filter.post.requestBody.content.application/json.schema.properties.filter.description
Filter dimension to match on, e.g. author.

# paths./labels/add-by-filter.post.requestBody.content.application/json.schema.properties.tags.description
Existing labels the bookmarks must already carry.

# paths./views.get.description
List all of your Smart Views (saved filtered views). Each view has a slug used to read its bookmarks or manage it. POST to the same path (name, base_query) to create one.

# paths./views/{slug}.get.description
Get a single Smart View by slug. PUT the same path to update it (name, base_query, triage_buckets, default_sort); DELETE to remove it.

# paths./views/{slug}/bookmarks.get.description
List the bookmarks inside a Smart View. Use the `bucket` query parameter to read a specific tab/bucket of the view. Paginated like /v1/bookmarks.

# paths./views/{slug}/bookmarks.get.parameters[].description
Which tab/bucket of the view to read. Defaults to all.

# paths./exports/pdf.post.description
Start an export job over an explicit `tweet_ids` array, a filter (`search_query` / `filter` / `value` / `tags`), or nothing at all (the whole library). Returns 202 with a `job_id` and a `Location` to poll; the finished file also appears in the Export Center at `page`.

# paths./exports/pdf.post.responses.202.description
Accepted. The export runs in the background; poll the Location for the download link.

# paths./exports/pdf.post.responses.202.headers.Location.description
Relative URL of the job resource to poll.

# paths./exports/pdf.post.responses.202.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result.

# paths./exports/pdf.post.responses.202.content.application/json.example.meta.message
PDF export started. Poll the location for the download link, or check the Export Center.

# paths./exports/pdf.post.responses.400.description
Nothing matched, so there is nothing to export.

# paths./exports/pdf.post.responses.400.content.application/json.example.error
No bookmarks matched — nothing to export.

# paths./exports/pdf.post.requestBody.content.application/json.schema.properties.tweet_ids.description
Explicit bookmark ids to export. When omitted, the filter fields below select the bookmarks instead.

# paths./exports/pdf.post.requestBody.content.application/json.schema.properties.search_query.description
Free-text query selecting the bookmarks to export.

# paths./exports/pdf.post.requestBody.content.application/json.schema.properties.filter.description
Filter dimension to select on, e.g. author.

# paths./exports/pdf.post.requestBody.content.application/json.schema.properties.deselected.description
Bookmark ids to exclude from an otherwise matching selection.

# paths./exports/csv.post.description
Start an export job over an explicit `tweet_ids` array, a filter (`search_query` / `filter` / `value` / `tags`), or nothing at all (the whole library). Returns 202 with a `job_id` and a `Location` to poll; the finished file also appears in the Export Center at `page`.

# paths./exports/csv.post.responses.202.description
Accepted. The export runs in the background; poll the Location for the download link.

# paths./exports/csv.post.responses.202.headers.Location.description
Relative URL of the job resource to poll.

# paths./exports/csv.post.responses.202.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result.

# paths./exports/csv.post.responses.202.content.application/json.example.meta.message
CSV export started. Poll the location for the download link, or check the Export Center.

# paths./exports/csv.post.responses.400.description
Nothing matched, so there is nothing to export.

# paths./exports/csv.post.responses.400.content.application/json.example.error
No bookmarks matched — nothing to export.

# paths./exports/csv.post.requestBody.content.application/json.schema.properties.tweet_ids.description
Explicit bookmark ids to export. When omitted, the filter fields below select the bookmarks instead.

# paths./exports/csv.post.requestBody.content.application/json.schema.properties.search_query.description
Free-text query selecting the bookmarks to export.

# paths./exports/csv.post.requestBody.content.application/json.schema.properties.filter.description
Filter dimension to select on, e.g. author.

# paths./exports/csv.post.requestBody.content.application/json.schema.properties.deselected.description
Bookmark ids to exclude from an otherwise matching selection.

# paths./exports/json.post.description
Start an export job over an explicit `tweet_ids` array, a filter (`search_query` / `filter` / `value` / `tags`), or nothing at all (the whole library). Returns 202 with a `job_id` and a `Location` to poll; the finished file also appears in the Export Center at `page`.

# paths./exports/json.post.responses.202.description
Accepted. The export runs in the background; poll the Location for the download link.

# paths./exports/json.post.responses.202.headers.Location.description
Relative URL of the job resource to poll.

# paths./exports/json.post.responses.202.headers.Idempotency-Replayed.description
Present and true when this response was replayed from a stored idempotent result.

# paths./exports/json.post.responses.202.content.application/json.example.meta.message
JSON export started. Poll the location for the download link, or check the Export Center.

# paths./exports/json.post.responses.400.description
Nothing matched, so there is nothing to export.

# paths./exports/json.post.responses.400.content.application/json.example.error
No bookmarks matched — nothing to export.

# paths./exports/json.post.requestBody.content.application/json.schema.properties.tweet_ids.description
Explicit bookmark ids to export. When omitted, the filter fields below select the bookmarks instead.

# paths./exports/json.post.requestBody.content.application/json.schema.properties.search_query.description
Free-text query selecting the bookmarks to export.

# paths./exports/json.post.requestBody.content.application/json.schema.properties.filter.description
Filter dimension to select on, e.g. author.

# paths./exports/json.post.requestBody.content.application/json.schema.properties.deselected.description
Bookmark ids to exclude from an otherwise matching selection.

# paths./exports/{job_id}.get.description
Read the state of an export started by one of the /exports/* endpoints. While the job is running the response carries `status: "pending"` and a `Retry-After` header. Once it finishes, `download_url` is a signed link that expires, so follow it promptly rather than storing it.

# paths./exports/{job_id}.get.responses.200.content.application/json.example.meta.message
Export ready. download_url is a signed link and expires.

# paths./labels/batch.post.summary
Apply many label operations in one request

# paths./labels/batch.post.description
Takes up to 50 label operations so an agent tidying a library does not make fifty round trips. Each operation reports its own result in request order. The status is 200 when every operation succeeded and 207 when at least one failed — a partial failure never discards the operations that worked.

# paths./labels/batch.post.requestBody.content.application/json.schema.properties.operations.items.properties.tweet_ids.description
Non-empty array of bookmark (tweet) ids.

# paths./labels/batch.post.requestBody.content.application/json.schema.properties.operations.items.properties.label_id.description
Existing label id. On `add`, either label_id or label_name is required.

# paths./labels/batch.post.requestBody.content.application/json.schema.properties.operations.items.properties.label_name.description
Label name; created if it does not exist on `add`. Required on `remove`.

# paths./labels/batch.post.responses.200.headers.Idempotency-Replayed.description
Present and true when replayed from a stored idempotent result.

# paths./labels/batch.post.responses.207.description
At least one operation failed. The rest were still applied.

# paths./labels/batch.post.responses.400.description
operations is missing, empty, or longer than 50.

# paths./labels/batch.post.responses.400.content.application/json.example.error
operations must be a non-empty array of { op, tweet_ids, label_name | label_id }.

# components.securitySchemes.bearerAuth.description
API key created at https://www.tweetsmash.com/integrations/api, sent as `Authorization: Bearer <key>`.

# components.schemas.ApiResponse.properties.next_cursor.description
Pass back as `cursor` to fetch the next page.

# components.schemas.ApiResponse.properties.meta.description
Non-payload extras such as a human-readable message or paging counts.

# components.schemas.ApiError.properties.code.description
Machine-readable error code. Do not retry on UNAUTHORIZED.

# components.schemas.ApiError.properties.upgrade_url.description
Where the user upgrades to unlock this endpoint.

# components.responses.PaymentRequired.description
The account is on a plan that does not include this endpoint.

# components.responses.IdempotencyConflict.description
The Idempotency-Key was already used with a different request body.

# components.responses.IdempotencyConflict.content.application/json.example.error
This Idempotency-Key was already used with a different request body.

# components.parameters.IdempotencyKey.description
A caller-generated UUID identifying one logical write. The first request with a given key is executed and its response stored for 24 hours; a repeat of the same key returns the stored response with Idempotency-Replayed: true instead of doing the work twice. Reusing a key with a different body is rejected with 422.

# components.parameters.SandboxError.description
Sandbox server only. Forces the sandbox to answer with a specific failure so a client can test its error handling without an account.