{
  "data": {
    "id": "0DLSgXAaZoP4YhOHR",
    "actId": "XJyPDggYTy5zS42Q1",
    "userId": "IYgBDiXtHOijAaUel",
    "startedAt": "2026-11-09T06:10:44.990Z",
    "finishedAt": "2026-11-09T06:10:55.618Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 10526,
      "runTimeSecs": 10.526,
      "computeUnits": 0.011695555555555556,
      "imageSizeBytes": 1333851648
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "createdByOrganizationMemberUserId": "QYo53EZkJk0prePWY",
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"Twitter Keywords Scraper\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"keywords\": {\n      \"title\": \"Search keywords\",\n      \"type\": \"array\",\n      \"description\": \"Words, phrases, hashtags or full X search queries to monitor. Each keyword is searched separately, and every result row tells you which keyword found it (searchTerm).\",\n      \"editor\": \"stringList\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"minItems\": 1,\n      \"prefill\": [\n        \"apify\"\n      ],\n      \"default\": []\n    },\n    \"maxTweets\": {\n      \"title\": \"Maximum tweets per keyword\",\n      \"type\": \"integer\",\n      \"description\": \"Stop after this many tweets for each keyword. This is a target, not a guarantee: guest X search returns ~20 posts per answered page, the Phase 6 probes measured 300 unique rows for a 300-row target, and 500/1,000 are unverified.\",\n      \"default\": 20,\n      \"prefill\": 20,\n      \"minimum\": 1,\n      \"maximum\": 1000\n    },\n    \"maxItems\": {\n      \"title\": \"Max tweets per keyword\",\n      \"type\": \"integer\",\n      \"description\": \"Alias of Maximum tweets per keyword. The actor accepts either name.\",\n      \"minimum\": 1,\n      \"maximum\": 1000\n    },\n    \"searchType\": {\n      \"title\": \"Result order\",\n      \"type\": \"string\",\n      \"description\": \"Latest returns the newest posts first. Top is engagement-ranked output: X's logged-out search never serves the real Top timeline, so the actor collects Latest pages and sorts them by like count client-side.\",\n      \"editor\": \"select\",\n      \"default\": \"Top\",\n      \"prefill\": \"Latest\",\n      \"enum\": [\n        \"Top\",\n        \"Latest\"\n      ],\n      \"enumTitles\": [\n        \"Top — engagement-ranked (client-side like-sort of Latest)\",\n        \"Latest — newest first\"\n      ]\n    },\n    \"sort\": {\n      \"title\": \"Result order (alias)\",\n      \"type\": \"string\",\n      \"description\": \"Alias of Result order. The actor accepts either name.\",\n      \"editor\": \"select\",\n      \"enum\": [\n        \"Top\",\n        \"Latest\"\n      ],\n      \"enumTitles\": [\n        \"Top — engagement-ranked (client-side like-sort of Latest)\",\n        \"Latest — newest first\"\n      ]\n    },\n    \"language\": {\n      \"title\": \"Language\",\n      \"type\": \"string\",\n      \"description\": \"Only return tweets written in this language. Leave the default to search all languages.\",\n      \"editor\": \"select\",\n      \"default\": \"Any language\",\n      \"prefill\": \"Any language\",\n      \"enum\": [\n        \"Any language\",\n        \"en\",\n        \"es\",\n        \"zh\",\n        \"hi\",\n        \"ar\",\n        \"bn\",\n        \"pt\",\n        \"ru\",\n        \"ja\",\n        \"de\",\n        \"fr\",\n        \"ko\",\n        \"tr\",\n        \"vi\",\n        \"te\",\n        \"mr\",\n        \"ta\",\n        \"ur\",\n        \"it\",\n        \"th\",\n        \"gu\",\n        \"fa\",\n        \"pl\",\n        \"uk\",\n        \"ml\",\n        \"kn\",\n        \"id\",\n        \"ms\",\n        \"sw\",\n        \"ha\",\n        \"am\",\n        \"my\",\n        \"pa\",\n        \"nl\",\n        \"ro\",\n        \"el\",\n        \"he\",\n        \"sv\",\n        \"hu\",\n        \"cs\"\n      ],\n      \"enumTitles\": [\n        \"Any language\",\n        \"English\",\n        \"Spanish\",\n        \"Chinese\",\n        \"Hindi\",\n        \"Arabic\",\n        \"Bengali\",\n        \"Portuguese\",\n        \"Russian\",\n        \"Japanese\",\n        \"German\",\n        \"French\",\n        \"Korean\",\n        \"Turkish\",\n        \"Vietnamese\",\n        \"Telugu\",\n        \"Marathi\",\n        \"Tamil\",\n        \"Urdu\",\n        \"Italian\",\n        \"Thai\",\n        \"Gujarati\",\n        \"Persian\",\n        \"Polish\",\n        \"Ukrainian\",\n        \"Malayalam\",\n        \"Kannada\",\n        \"Indonesian\",\n        \"Malay\",\n        \"Swahili\",\n        \"Hausa\",\n        \"Amharic\",\n        \"Burmese\",\n        \"Punjabi\",\n        \"Dutch\",\n        \"Romanian\",\n        \"Greek\",\n        \"Hebrew\",\n        \"Swedish\",\n        \"Hungarian\",\n        \"Czech\"\n      ]\n    },\n    \"dateFrom\": {\n      \"title\": \"Start date\",\n      \"type\": \"string\",\n      \"description\": \"Only return tweets posted on or after this date. Leave empty for no lower bound.\",\n      \"editor\": \"datepicker\",\n      \"nullable\": true\n    },\n    \"dateTo\": {\n      \"title\": \"End date\",\n      \"type\": \"string\",\n      \"description\": \"Only return tweets posted on or before this date (inclusive). Leave empty for no upper bound.\",\n      \"editor\": \"datepicker\",\n      \"nullable\": true\n    },\n    \"excludeRetweets\": {\n      \"title\": \"Exclude retweets (reposts)\",\n      \"type\": \"boolean\",\n      \"description\": \"Skip retweets/reposts and keep only original posts.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"excludeReplies\": {\n      \"title\": \"Exclude replies\",\n      \"type\": \"boolean\",\n      \"description\": \"Skip replies and keep only top-level posts.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"onlyVerifiedUsers\": {\n      \"title\": \"Verified accounts only\",\n      \"type\": \"boolean\",\n      \"description\": \"Only return tweets from verified (blue check) accounts.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"onlyImages\": {\n      \"title\": \"Tweets with images only\",\n      \"type\": \"boolean\",\n      \"description\": \"Only return tweets that contain at least one image.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"onlyVideos\": {\n      \"title\": \"Tweets with videos only\",\n      \"type\": \"boolean\",\n      \"description\": \"Only return tweets that contain at least one video or GIF.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"onlyQuotes\": {\n      \"title\": \"Quote tweets only\",\n      \"type\": \"boolean\",\n      \"description\": \"Only return quote tweets (filter:quote).\",\n      \"default\": false,\n      \"prefill\": false,\n      \"sectionCaption\": \"More search filters\",\n      \"sectionDescription\": \"Optional operator-style filters. Operator queries use the X guest search only; the Bing News fallback cannot honour them.\"\n    },\n    \"mentioning\": {\n      \"title\": \"Mentioning account\",\n      \"type\": \"string\",\n      \"description\": \"Only return posts that mention this account, e.g. apify or @apify.\",\n      \"nullable\": true,\n      \"editor\": \"textfield\"\n    },\n    \"author\": {\n      \"title\": \"From account\",\n      \"type\": \"string\",\n      \"description\": \"Only return posts written by this account, e.g. openai or @openai.\",\n      \"nullable\": true,\n      \"editor\": \"textfield\"\n    },\n    \"inReplyTo\": {\n      \"title\": \"In reply to account\",\n      \"type\": \"string\",\n      \"description\": \"Only return replies addressed to this account, e.g. elonmusk or @elonmusk.\",\n      \"nullable\": true,\n      \"editor\": \"textfield\"\n    },\n    \"conversationId\": {\n      \"title\": \"Conversation ID\",\n      \"type\": \"string\",\n      \"description\": \"Only return posts in this conversation (the numeric id of the root post).\",\n      \"nullable\": true,\n      \"editor\": \"textfield\"\n    },\n    \"matchMode\": {\n      \"title\": \"Multi-word match mode\",\n      \"type\": \"string\",\n      \"description\": \"How to combine the words in each keyword: all = X's default AND, any = (word1 OR word2), exact = quoted phrase.\",\n      \"editor\": \"select\",\n      \"default\": \"all\",\n      \"prefill\": \"all\",\n      \"enum\": [\n        \"all\",\n        \"any\",\n        \"exact\"\n      ],\n      \"enumTitles\": [\n        \"All words (AND)\",\n        \"Any word (OR)\",\n        \"Exact phrase\"\n      ]\n    },\n    \"excludeWords\": {\n      \"title\": \"Exclude words\",\n      \"type\": \"array\",\n      \"description\": \"Words or phrases to exclude from every search, compiled to -word / -\\\"two words\\\" tokens.\",\n      \"editor\": \"stringList\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"nullable\": true,\n      \"default\": []\n    },\n    \"includeLanes\": {\n      \"title\": \"Search lanes\",\n      \"type\": \"array\",\n      \"description\": \"Which X search lanes to run: latest, media, media-product, links, replies. Leave empty for the default latest + media + links (replies is added automatically for jobs of 100+ rows). media-product asks for X's product=Media page directly and is opt-in.\",\n      \"editor\": \"stringList\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"nullable\": true,\n      \"default\": []\n    },\n    \"maxWindows\": {\n      \"title\": \"Maximum time windows\",\n      \"type\": \"integer\",\n      \"description\": \"Cap the number of backwards time-window passes per keyword. 0 means no explicit cap (the wall-clock budget still applies).\",\n      \"default\": 0,\n      \"minimum\": 0,\n      \"maximum\": 1000\n    },\n    \"maxPasses\": {\n      \"title\": \"Maximum search passes\",\n      \"type\": \"integer\",\n      \"description\": \"Cap the total number of X search passes (core lanes + time windows) per keyword. 0 means no explicit cap.\",\n      \"default\": 0,\n      \"minimum\": 0,\n      \"maximum\": 5000\n    },\n    \"onlyNew\": {\n      \"title\": \"Only new posts (monitoring)\",\n      \"type\": \"boolean\",\n      \"description\": \"Return only posts newer than the previous run for this keyword. The actor stores a numeric tweet-id watermark plus an overlap window in the key-value store, so scheduled runs do not repeat old rows.\",\n      \"default\": false,\n      \"prefill\": false,\n      \"sectionCaption\": \"Monitoring (only new posts)\",\n      \"sectionDescription\": \"Use these fields when the actor runs on a schedule and should return only fresh posts. X guest search has no since_id, so the actor compares 19-digit snowflake ids numerically and walks an overlap window.\"\n    },\n    \"monitoringEnabled\": {\n      \"title\": \"Enable monitoring (alias)\",\n      \"type\": \"boolean\",\n      \"description\": \"Alias that enables onlyNew when onlyNew itself is not provided. Prefer the Only new posts field.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"monitorId\": {\n      \"title\": \"Monitor ID\",\n      \"type\": \"string\",\n      \"description\": \"Namespace for the saved watermark. Use different ids to keep independent schedules for the same keyword. Defaults to 'default'. The legacy alias 'monitoringStateKey' is also accepted.\",\n      \"nullable\": true,\n      \"editor\": \"textfield\"\n    },\n    \"overlapMinutes\": {\n      \"title\": \"Overlap window (minutes)\",\n      \"type\": \"integer\",\n      \"description\": \"How far back before the previous run to search, so posts published during the last walk are not missed. Already-seen ids are still filtered out.\",\n      \"default\": 10,\n      \"minimum\": 0,\n      \"maximum\": 1440\n    },\n    \"maxAgeDays\": {\n      \"title\": \"Maximum age (days)\",\n      \"type\": \"integer\",\n      \"description\": \"Never walk further back than this many days, even when catching up after a long gap.\",\n      \"default\": 90,\n      \"minimum\": 1,\n      \"maximum\": 3650\n    },\n    \"sinceTime\": {\n      \"title\": \"Since time (backfill)\",\n      \"type\": \"string\",\n      \"description\": \"Optional lower bound for a backfill run: ISO 8601 timestamp or Unix epoch seconds. Uses X's epoch since_time operator, not day-granular since:.\",\n      \"editor\": \"textfield\",\n      \"nullable\": true\n    },\n    \"untilTime\": {\n      \"title\": \"Until time (backfill)\",\n      \"type\": \"string\",\n      \"description\": \"Optional upper bound for a backfill run: ISO 8601 timestamp or Unix epoch seconds. Uses X's epoch until_time operator.\",\n      \"editor\": \"textfield\",\n      \"nullable\": true\n    },\n    \"proxyConfiguration\": {\n      \"title\": \"Proxy configuration\",\n      \"type\": \"object\",\n      \"description\": \"Optional. Use a proxy if you run large jobs or see frequent IP-based rate limits.\",\n      \"editor\": \"proxy\",\n      \"sectionCaption\": \"Advanced\",\n      \"sectionDescription\": \"Optional settings for large or repeated runs.\",\n      \"nullable\": true\n    },\n    \"startDate\": {\n      \"title\": \"Start date (legacy)\",\n      \"type\": \"string\",\n      \"description\": \"Deprecated — use 'Start date' instead. Kept so existing saved inputs keep working. Format: YYYY-MM-DD.\",\n      \"editor\": \"textfield\",\n      \"nullable\": true,\n      \"sectionCaption\": \"Advanced (legacy)\",\n      \"sectionDescription\": \"Deprecated aliases kept for backwards compatibility. Prefer the fields above.\"\n    },\n    \"endDate\": {\n      \"title\": \"End date (legacy)\",\n      \"type\": \"string\",\n      \"description\": \"Deprecated — use 'End date' instead. Kept so existing saved inputs keep working. Format: YYYY-MM-DD.\",\n      \"editor\": \"textfield\",\n      \"nullable\": true\n    }\n  },\n  \"required\": [\n    \"keywords\"\n  ]\n}",
    "readme": "# X/Twitter Keyword & Hashtag Tweet Search\r\n\r\nGive this actor a list of keywords, phrases or hashtags and it pulls back matching X (formerly Twitter) posts — full text, engagement counts, media and the author who wrote each one. No sign-in, cookies or developer credentials needed; it reads the same public search results any visitor sees.\r\n\r\n## What this actor does\r\n\r\n- Runs each entry in `keywords` as its own independent search, so a single job can cover several unrelated topics at once.\r\n- Targets up to 1,000 posts per keyword via `maxTweets`; how many actually come back depends on how much volume that keyword has on X. A 300-row target has been verified (about 297 s); 500/1,000 are not verified, so treat `maxTweets` as a target, not a promise.\r\n- Runs several X search lanes (`latest`, `media`, opt-in `media-product`, `links`, optionally `replies`) and walks backwards through `since_time`/`until_time` windows. Guest search has **no cursor paging**; time-window backfill is the only way past the ~20-row page cap (a `product=Media` page can return 41-45 rows).\r\n- `Latest` returns newest-first. `Top` is **engagement-ranked output, not X's real Top timeline**: X never serves Top to logged-out callers, so the actor collects Latest pages and sorts them by like count client-side.\r\n- Narrows by language, date window, retweets/replies, verification, media presence, quotes, mentions, author, reply target, conversation id, and minimum like/retweet/reply counts.\r\n- Supports multi-word `matchMode` (`all`, `any`, `exact`) and `excludeWords`.\r\n- With `onlyNew` enabled, returns only posts newer than the previous run for the same `monitorId` + keyword. The actor persists a numeric snowflake watermark and an overlap window in the key-value store.\r\n- Stamps every row with `searchTerm`, `source` (`x-search` or `bing-news`), `sourceLane` (`latest` / `media` / `media-product` / `links` / `replies` / `window` / `bing` / `bing-title-only`) and `degraded`.\r\n- Emits a typed `status` row for a keyword that needs attention and one `summary` row per run.\r\n- **Empty fields are omitted:** never `null`, empty strings or empty arrays/objects in the output.\r\n\r\n## Output per post\r\n\r\nCore fields on every post record: `id`, `url`, `text`, `createdAt`, `language` (when X reports one), `searchTerm`, `source`, `sourceLane`, `degraded`, `type: \"post\"` and `collectedAt`.\r\n\r\nEngagement comes through `replyCount`, `retweetCount`, `likeCount`, `quoteCount`, `bookmarkCount`, and `viewCount`, plus a computed `engagementRate` — `(likes + retweets + replies + quotes) ÷ followers` — included only once the author's follower count is known and nonzero. Missing retweets/quotes/bookmarks/views/followers/bio are backfilled from the FxTwitter mirror after the X refresh; X values always win and only missing fields are copied.\r\n\r\nPost-type context: `isReply`, `isRetweet`, `isQuote`, `isNoteTweet`, and, for replies, `conversationId`, `inReplyToTweetId`, `inReplyToUserId`, `inReplyToUsername`, `parentTweet` (a nested summary of the post being replied to, when available). Every post row also carries `recordType: \"post\"`, `sourceUrl` (the canonical post URL) and `scrapedAt`.\r\n\r\nEntities parsed out of the text: `hashtags`, `mentions`, `urls`, `cashtags`.\r\n\r\nAttachments and extras, when present: `media` (one entry per photo/video/GIF with `type`, `url`, `previewImageUrl`, `width`, `height`, `durationSeconds`, `viewCount`, `altText`, `mediaKey`, `canMediaTag`, `focusRects`, `variants` with `url`/`contentType`/`bitrate`), `quotedTweet` / `quotedTweetId`, `poll`, `card`, `possiblySensitive`, `withheldInCountries`, `place`, `postedFrom`, `isEdited`, `editControl` (edit history metadata), `newsActionType`.\r\n\r\nThe author rides along as a nested `author` object: `id`, `username`, `name`, `bio`, `bioLinks`, `location`, `website`, `followers`, `following`, `likesGiven`, `postCount`, `mediaCount`, `createdAt`, `profileImageUrl`, `bannerUrl`, `profileImageShape`, `isBlueVerified`, `verified`, `verifiedType`, `isProtected`, `professionalType`, `superFollowEligible`, `superFollowedBy`, `superFollowing`, `hasGraduatedAccess`, `translatorType` and `pinnedTweetIds`. Fields X does not expose for a post are omitted.\r\n\r\n### Sample record\r\n\r\n```json\r\n{\r\n  \"type\": \"post\",\r\n  \"id\": \"1923456789012345678\",\r\n  \"url\": \"https://x.com/i/status/1923456789012345678\",\r\n  \"text\": \"Just shipped a new feature after three weeks of testing. Feels good. #buildinpublic\",\r\n  \"createdAt\": \"2026-06-02T09:15:00+00:00\",\r\n  \"language\": \"en\",\r\n  \"searchTerm\": \"buildinpublic\",\r\n  \"source\": \"x-search\",\r\n  \"sourceLane\": \"latest\",\r\n  \"degraded\": false,\r\n  \"replyCount\": 34,\r\n  \"retweetCount\": 112,\r\n  \"likeCount\": 1870,\r\n  \"quoteCount\": 9,\r\n  \"bookmarkCount\": 265,\r\n  \"viewCount\": 54300,\r\n  \"isReply\": false,\r\n  \"isRetweet\": false,\r\n  \"isQuote\": false,\r\n  \"hashtags\": [\"buildinpublic\"],\r\n  \"author\": {\r\n    \"id\": \"1092834756\",\r\n    \"username\": \"makerjane\",\r\n    \"name\": \"Jane Builds\",\r\n    \"followers\": 18400,\r\n    \"isBlueVerified\": false\r\n  },\r\n  \"collectedAt\": \"2026-06-02T10:03:41+00:00\"\r\n}\r\n```\r\n\r\n## Output per status row\r\n\r\nWhen a keyword needs attention, the actor pushes one typed record:\r\n\r\n```json\r\n{\r\n  \"type\": \"status\",\r\n  \"status\": \"blocked\",\r\n  \"message\": \"X guest search refused this network right now...\",\r\n  \"searchTerm\": \"apify\",\r\n  \"collectedAt\": \"2026-06-02T10:03:41+00:00\",\r\n  \"source\": \"none\",\r\n  \"sourcesTried\": [\"x-search\", \"bing-news\"],\r\n  \"xAttempts\": 96,\r\n  \"xOk\": 0,\r\n  \"xLastStatus\": 404,\r\n  \"bingDiscovered\": 0,\r\n  \"bingHydrated\": 0,\r\n  \"bingTitleOnly\": 0,\r\n  \"bingRows\": 0,\r\n  \"targetRows\": 20,\r\n  \"uniqueRows\": 0,\r\n  \"budgetSeconds\": 105,\r\n  \"budgetExhausted\": false,\r\n  \"degraded\": true,\r\n  \"partial\": false\r\n}\r\n```\r\n\r\nStatuses:\r\n\r\n| Status | Meaning |\r\n|---|---|\r\n| `noInput` | No keywords were provided. |\r\n| `noResults` | A source answered and returned zero matching posts (or every collected row was already seen by `onlyNew`). |\r\n| `blocked` | No source answered: X refused and Bing was unavailable or skipped because the query uses operators. |\r\n| `partial` | Rows were returned but the target was **not** reached (budget/limits stopped the walk, a full page appeared at the oldest edge, or X was blocked and Bing supplied fewer rows than requested). Reaching the target exactly is `ok`, even when rows are `degraded`. |\r\n| `budgetExhausted` | The per-keyword budget ran out before any source answered. |\r\n| `enrichmentPartial` | Rows were returned, but one or more per-tweet enrichment lookups failed. |\r\n\r\n`xOk` counts only X bursts that returned a parseable page of posts. An HTTP 200 with an empty payload or an empty timeline is treated as a refusal/no-answer, so it cannot mask `blocked` or `budgetExhausted`.\r\n\r\nEvery run also ends with one `type: \"summary\"` row carrying `recordType: \"runSummary\"`, `scrapedAt`, `startedAt`, `finishedAt`, `keywords`, `postRows`, `statusRows`, `bySource`, `sourcesTried`, aggregate `xAttempts` / `xOk` / `bingDiscovered` / `bingHydrated` / `bingTitleOnly`, `degradedKeywords`, `partialKeywords`, `blockedKeywords`, `durationSeconds`, `monitorId` and `onlyNew`. Consumers that count `type: \"post\"` are never confused by status or summary rows.\r\n\r\n## Known hard limits\r\n\r\nMeasured directly (no proxy, no cookies, no browser) in live probes on 2026-11-08:\r\n\r\n- **Guest search is a lottery.** Only ~6% of Latest bursts answered in a 32-attempt probe, and the 300-row backfill used 5,259 attempts / 20 answered bursts (0.38%). The same query can return 20 rows in seconds later, but there is no SLA.\r\n- **No real `Top`.** X serves Top only to signed-in callers, so `Top` output is a client-side like-sort of the Latest rows that were collected.\r\n- **No cursor paging.** One answered page is ~20 rows; `product=Media` can return 41-45. Backfill beyond one page is time-window walking only.\r\n- **300 rows is the measured backfill ceiling.** 300/300 was reached on `bitcoin`; 500 and 1,000 are unverified targets.\r\n- **Bing News is stale and shallow.** It discovers newsworthy posts only (0/65 sampled rows were <24 h old, 53/65 were <7 d) and walks at most 5 RSS pages (~50 unique ids). It cannot honour operator queries.\r\n- **Monitoring is duplicate suppression, not complete coverage.** `onlyNew` plus the overlap window protects the page boundary; posts published between runs can be missed when a keyword produces more than one page per interval.\r\n- **Enrichment depends on one third-party mirror.** FxTwitter fills retweets/quotes/bookmarks/views/author fields; vxtwitter/fxwitter/fixupx were unusable in the same environment. If FxTwitter fails, those fields stay absent; rows are still returned.\r\n\r\n## Input\r\n\r\n| Field | Type | Default | Description |\r\n|---|---|---|---|\r\n| `keywords` | array | – | **Required.** Words, phrases, hashtags or full search queries; each runs separately. |\r\n| `maxTweets` | integer | `20` | Target number of posts per keyword (1–1000). |\r\n| `maxItems` | integer | – | Same as `maxTweets` — use whichever name you prefer. |\r\n| `searchType` | string | `Top` | `Top` = engagement-ranked (client-side like-sort of Latest); `Latest` = newest-first. |\r\n| `sort` | string | – | Same as `searchType`. |\r\n| `language` | string | `Any language` | Restrict to one of 40 common X language codes, e.g. `en`, `es`, `ja`. |\r\n| `dateFrom` | date | – | Earliest post date to include. |\r\n| `dateTo` | date | – | Latest post date to include (inclusive). |\r\n| `excludeRetweets` | boolean | `false` | Drop retweets/reposts, keep only original posts. |\r\n| `excludeReplies` | boolean | `false` | Drop replies, keep only top-level posts. |\r\n| `onlyVerifiedUsers` | boolean | `false` | Keep only posts from verified accounts. |\r\n| `onlyImages` | boolean | `false` | Keep only posts containing at least one image. |\r\n| `onlyVideos` | boolean | `false` | Keep only posts containing at least one video or GIF. |\r\n| `onlyQuotes` | boolean | `false` | Keep only quote tweets. |\r\n| `mentioning` | string | – | Keep posts that mention this account. |\r\n| `author` | string | – | Keep posts written by this account. |\r\n| `inReplyTo` | string | – | Keep replies addressed to this account. |\r\n| `conversationId` | string | – | Keep posts in this conversation. |\r\n| `matchMode` | string | `all` | `all` (AND), `any` (OR) or `exact` (quoted phrase) for multi-word keywords. |\r\n| `excludeWords` | array | – | Words/phrases to exclude (`-word`, `-\"two words\"`). |\r\n| `includeLanes` | array | – | `latest`, `media`, `media-product`, `links`, `replies`. Default is latest + media + links; replies is added for 100+ row jobs. `media-product` asks for X's `product=Media` page directly (opt-in; 41-45 rows when it answers). |\r\n| `maxWindows` | integer | `0` | Cap backwards time-window passes per keyword; `0` = no explicit cap. |\r\n| `maxPasses` | integer | `0` | Cap total X search passes (core + windows); `0` = no explicit cap. |\r\n| `minimumLikes` | integer | – | Floor on like count. |\r\n| `minimumRetweets` | integer | – | Floor on retweet count. |\r\n| `minimumReplies` | integer | – | Floor on reply count. |\r\n| `onlyNew` | boolean | `false` | Return only posts newer than the previous run for this keyword. |\r\n| `monitoringEnabled` | boolean | `false` | Alias that enables `onlyNew` when `onlyNew` is not provided. |\r\n| `monitorId` | string | `default` | Namespace for the saved watermark; use different ids for independent schedules. `monitoringStateKey` is accepted as an alias. |\r\n| `overlapMinutes` | integer | `10` | Search this far before the previous run so late-arriving posts are not missed. |\r\n| `maxAgeDays` | integer | `90` | Never walk further back than this, even after a long gap. |\r\n| `sinceTime` | string | – | Backfill lower bound: ISO 8601 or Unix epoch seconds. |\r\n| `untilTime` | string | – | Backfill upper bound: ISO 8601 or Unix epoch seconds. |\r\n| `proxyConfiguration` | object | – | Optional; useful for large or repeated runs from the same IP. |\r\n| `startDate` / `endDate` | string | – | Older aliases for `dateFrom` / `dateTo` (`YYYY-MM-DD`). |\r\n\r\n### Example: two unrelated topics in one run\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"apify\", \"web scraping\"],\r\n  \"maxTweets\": 50\r\n}\r\n```\r\n\r\n### Example: live brand-mention watch\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"#YourBrand\", \"\\\"your brand\\\"\"],\r\n  \"searchType\": \"Latest\",\r\n  \"language\": \"en\",\r\n  \"maxTweets\": 200,\r\n  \"excludeRetweets\": true,\r\n  \"excludeReplies\": true\r\n}\r\n```\r\n\r\n### Example: scheduled only-new monitoring\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"#YourBrand\", \"@yourbrand\"],\r\n  \"searchType\": \"Latest\",\r\n  \"maxTweets\": 200,\r\n  \"onlyNew\": true,\r\n  \"monitorId\": \"brand-watch\",\r\n  \"overlapMinutes\": 15,\r\n  \"maxAgeDays\": 30\r\n}\r\n```\r\n\r\n### Example: bounded backfill without a cursor\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"artificial intelligence\"],\r\n  \"maxTweets\": 500,\r\n  \"sinceTime\": \"2026-01-01T00:00:00Z\",\r\n  \"untilTime\": \"2026-06-30T23:59:59Z\",\r\n  \"includeLanes\": [\"latest\", \"media\", \"links\"],\r\n  \"maxWindows\": 40\r\n}\r\n```\r\n\r\n### Example: top posts in a date window, above an engagement floor\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"artificial intelligence\"],\r\n  \"maxTweets\": 500,\r\n  \"minimumLikes\": 100,\r\n  \"minimumRetweets\": 20,\r\n  \"dateFrom\": \"2026-01-01\",\r\n  \"dateTo\": \"2026-06-30\"\r\n}\r\n```\r\n\r\n## Use cases\r\n\r\n- **Brand monitoring** — schedule the actor with `onlyNew` to catch new mentions without repeating old rows.\r\n- **Trend and topic tracking** — measure how a subject's volume and engagement shift over weeks or months.\r\n- **Breaking-news watch** — pair `Latest` with a tight date range or `sinceTime`/`untilTime` to follow a fast-moving story.\r\n- **Content research** — surface the highest-engagement posts around a theme for inspiration.\r\n- **Academic or policy research** — assemble a dataset of public discourse on a topic, language or time period.\r\n- **Competitor comparison** — run several competing keywords side by side and compare volume and engagement.\r\n\r\n## FAQ\r\n\r\n**Do I need an X account or developer credentials?**\r\nNo — there's no credential field in the input at all. The actor reads what any logged-out visitor can already see on X's own search.\r\n\r\n**How many posts will I actually get per keyword?**\r\nUp to `maxTweets`, but that's a ceiling, not a promise. Guest search answers intermittently and returns ~20 rows per answered page (41-45 on an answered `product=Media` page). The actor multiplies those pages with lanes and time windows. A 300-row target has been verified; targets above 300 are unverified. An obscure keyword may return only a handful.\r\n\r\n**Why did I get a different count on a second identical run?**\r\nX's logged-out search doesn't expose stable, full pagination, and what it surfaces can shift between requests. If you need more rows, use `onlyNew: false`, narrow the date window, or add lanes rather than assuming something broke.\r\n\r\n**Should I use `Top` or `Latest`?**\r\n`Latest` for anything time-sensitive — breaking news, live mentions, a campaign in progress. `Top` gives you the strongest engagement among the rows the actor could collect, but it is a client-side like-sort of Latest results, not X's real Top timeline (X does not serve Top to guests).\r\n\r\n**How does only-new monitoring work without a cursor?**\r\nThe actor saves `lastMaxId` (numeric 19-digit snowflake), `lastRunAt` and the last 1,000 seen ids in the key-value store under `xkw:<sha1(monitorId + keyword)>`. Each run searches from `lastRunAt - overlapMinutes` (capped by `maxAgeDays`) and keeps rows with an id numerically above the watermark or created inside the overlap window and not already seen. A blocked run never advances the watermark. Bing-only fallback rows are remembered as seen so they are not re-emitted, but they leave the X gap open for the next run.\r\n\r\n**Is a proxy required?**\r\nNo, it's optional. Add one mainly if you're running large or frequent jobs from the same IP and want extra headroom against rate limiting.\r\n\r\n**How current is the data?**\r\nFully live — each run queries X at the moment it executes. Put the actor on a schedule with `onlyNew` for ongoing monitoring. Bing News fallback rows are not real-time: in live probes only 1.2% were under 24 hours old.\r\n\r\n**Why is a row marked `degraded: true`?**\r\nIt came from the Bing News fallback (hydrated or title-only). Those rows are newsworthy-only, can be days or years old, and lack retweets/quotes/views/bookmarks/followers until the enrichment layer fills what it can.\r\n\r\n**How do I get the results into another tool?**\r\nPull the dataset from the run page or the Apify API, wire up a webhook, or connect it to Zapier, Make, Google Sheets, or similar no-code tools.\r\n\r\n**Is this affiliated with X?**\r\nNo. This is an independent, third-party actor that reads publicly available X search results.\r\n",
    "actorDefinition": {
      "actorSpecification": 1,
      "name": "x-twitter-keyword-and-hashtag-tweet-search",
      "version": "1.0",
      "buildTag": "latest",
      "defaultMemoryMbytes": 4096,
      "minMemoryMbytes": 1024,
      "dockerfile": "./Dockerfile",
      "input": {
        "title": "Twitter Keywords Scraper",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "keywords": {
            "title": "Search keywords",
            "type": "array",
            "description": "Words, phrases, hashtags or full X search queries to monitor. Each keyword is searched separately, and every result row tells you which keyword found it (searchTerm).",
            "editor": "stringList",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "prefill": [
              "apify"
            ],
            "default": []
          },
          "maxTweets": {
            "title": "Maximum tweets per keyword",
            "type": "integer",
            "description": "Stop after this many tweets for each keyword. This is a target, not a guarantee: guest X search returns ~20 posts per answered page, the Phase 6 probes measured 300 unique rows for a 300-row target, and 500/1,000 are unverified.",
            "default": 20,
            "prefill": 20,
            "minimum": 1,
            "maximum": 1000
          },
          "maxItems": {
            "title": "Max tweets per keyword",
            "type": "integer",
            "description": "Alias of Maximum tweets per keyword. The actor accepts either name.",
            "minimum": 1,
            "maximum": 1000
          },
          "searchType": {
            "title": "Result order",
            "type": "string",
            "description": "Latest returns the newest posts first. Top is engagement-ranked output: X's logged-out search never serves the real Top timeline, so the actor collects Latest pages and sorts them by like count client-side.",
            "editor": "select",
            "default": "Top",
            "prefill": "Latest",
            "enum": [
              "Top",
              "Latest"
            ],
            "enumTitles": [
              "Top — engagement-ranked (client-side like-sort of Latest)",
              "Latest — newest first"
            ]
          },
          "sort": {
            "title": "Result order (alias)",
            "type": "string",
            "description": "Alias of Result order. The actor accepts either name.",
            "editor": "select",
            "enum": [
              "Top",
              "Latest"
            ],
            "enumTitles": [
              "Top — engagement-ranked (client-side like-sort of Latest)",
              "Latest — newest first"
            ]
          },
          "language": {
            "title": "Language",
            "type": "string",
            "description": "Only return tweets written in this language. Leave the default to search all languages.",
            "editor": "select",
            "default": "Any language",
            "prefill": "Any language",
            "enum": [
              "Any language",
              "en",
              "es",
              "zh",
              "hi",
              "ar",
              "bn",
              "pt",
              "ru",
              "ja",
              "de",
              "fr",
              "ko",
              "tr",
              "vi",
              "te",
              "mr",
              "ta",
              "ur",
              "it",
              "th",
              "gu",
              "fa",
              "pl",
              "uk",
              "ml",
              "kn",
              "id",
              "ms",
              "sw",
              "ha",
              "am",
              "my",
              "pa",
              "nl",
              "ro",
              "el",
              "he",
              "sv",
              "hu",
              "cs"
            ],
            "enumTitles": [
              "Any language",
              "English",
              "Spanish",
              "Chinese",
              "Hindi",
              "Arabic",
              "Bengali",
              "Portuguese",
              "Russian",
              "Japanese",
              "German",
              "French",
              "Korean",
              "Turkish",
              "Vietnamese",
              "Telugu",
              "Marathi",
              "Tamil",
              "Urdu",
              "Italian",
              "Thai",
              "Gujarati",
              "Persian",
              "Polish",
              "Ukrainian",
              "Malayalam",
              "Kannada",
              "Indonesian",
              "Malay",
              "Swahili",
              "Hausa",
              "Amharic",
              "Burmese",
              "Punjabi",
              "Dutch",
              "Romanian",
              "Greek",
              "Hebrew",
              "Swedish",
              "Hungarian",
              "Czech"
            ]
          },
          "dateFrom": {
            "title": "Start date",
            "type": "string",
            "description": "Only return tweets posted on or after this date. Leave empty for no lower bound.",
            "editor": "datepicker",
            "nullable": true
          },
          "dateTo": {
            "title": "End date",
            "type": "string",
            "description": "Only return tweets posted on or before this date (inclusive). Leave empty for no upper bound.",
            "editor": "datepicker",
            "nullable": true
          },
          "excludeRetweets": {
            "title": "Exclude retweets (reposts)",
            "type": "boolean",
            "description": "Skip retweets/reposts and keep only original posts.",
            "default": false,
            "prefill": false
          },
          "excludeReplies": {
            "title": "Exclude replies",
            "type": "boolean",
            "description": "Skip replies and keep only top-level posts.",
            "default": false,
            "prefill": false
          },
          "onlyVerifiedUsers": {
            "title": "Verified accounts only",
            "type": "boolean",
            "description": "Only return tweets from verified (blue check) accounts.",
            "default": false,
            "prefill": false
          },
          "onlyImages": {
            "title": "Tweets with images only",
            "type": "boolean",
            "description": "Only return tweets that contain at least one image.",
            "default": false,
            "prefill": false
          },
          "onlyVideos": {
            "title": "Tweets with videos only",
            "type": "boolean",
            "description": "Only return tweets that contain at least one video or GIF.",
            "default": false,
            "prefill": false
          },
          "onlyQuotes": {
            "title": "Quote tweets only",
            "type": "boolean",
            "description": "Only return quote tweets (filter:quote).",
            "default": false,
            "prefill": false,
            "sectionCaption": "More search filters",
            "sectionDescription": "Optional operator-style filters. Operator queries use the X guest search only; the Bing News fallback cannot honour them."
          },
          "mentioning": {
            "title": "Mentioning account",
            "type": "string",
            "description": "Only return posts that mention this account, e.g. apify or @apify.",
            "nullable": true,
            "editor": "textfield"
          },
          "author": {
            "title": "From account",
            "type": "string",
            "description": "Only return posts written by this account, e.g. openai or @openai.",
            "nullable": true,
            "editor": "textfield"
          },
          "inReplyTo": {
            "title": "In reply to account",
            "type": "string",
            "description": "Only return replies addressed to this account, e.g. elonmusk or @elonmusk.",
            "nullable": true,
            "editor": "textfield"
          },
          "conversationId": {
            "title": "Conversation ID",
            "type": "string",
            "description": "Only return posts in this conversation (the numeric id of the root post).",
            "nullable": true,
            "editor": "textfield"
          },
          "matchMode": {
            "title": "Multi-word match mode",
            "type": "string",
            "description": "How to combine the words in each keyword: all = X's default AND, any = (word1 OR word2), exact = quoted phrase.",
            "editor": "select",
            "default": "all",
            "prefill": "all",
            "enum": [
              "all",
              "any",
              "exact"
            ],
            "enumTitles": [
              "All words (AND)",
              "Any word (OR)",
              "Exact phrase"
            ]
          },
          "excludeWords": {
            "title": "Exclude words",
            "type": "array",
            "description": "Words or phrases to exclude from every search, compiled to -word / -\"two words\" tokens.",
            "editor": "stringList",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "default": []
          },
          "includeLanes": {
            "title": "Search lanes",
            "type": "array",
            "description": "Which X search lanes to run: latest, media, media-product, links, replies. Leave empty for the default latest + media + links (replies is added automatically for jobs of 100+ rows). media-product asks for X's product=Media page directly and is opt-in.",
            "editor": "stringList",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "default": []
          },
          "maxWindows": {
            "title": "Maximum time windows",
            "type": "integer",
            "description": "Cap the number of backwards time-window passes per keyword. 0 means no explicit cap (the wall-clock budget still applies).",
            "default": 0,
            "minimum": 0,
            "maximum": 1000
          },
          "maxPasses": {
            "title": "Maximum search passes",
            "type": "integer",
            "description": "Cap the total number of X search passes (core lanes + time windows) per keyword. 0 means no explicit cap.",
            "default": 0,
            "minimum": 0,
            "maximum": 5000
          },
          "onlyNew": {
            "title": "Only new posts (monitoring)",
            "type": "boolean",
            "description": "Return only posts newer than the previous run for this keyword. The actor stores a numeric tweet-id watermark plus an overlap window in the key-value store, so scheduled runs do not repeat old rows.",
            "default": false,
            "prefill": false,
            "sectionCaption": "Monitoring (only new posts)",
            "sectionDescription": "Use these fields when the actor runs on a schedule and should return only fresh posts. X guest search has no since_id, so the actor compares 19-digit snowflake ids numerically and walks an overlap window."
          },
          "monitoringEnabled": {
            "title": "Enable monitoring (alias)",
            "type": "boolean",
            "description": "Alias that enables onlyNew when onlyNew itself is not provided. Prefer the Only new posts field.",
            "default": false,
            "prefill": false
          },
          "monitorId": {
            "title": "Monitor ID",
            "type": "string",
            "description": "Namespace for the saved watermark. Use different ids to keep independent schedules for the same keyword. Defaults to 'default'. The legacy alias 'monitoringStateKey' is also accepted.",
            "nullable": true,
            "editor": "textfield"
          },
          "overlapMinutes": {
            "title": "Overlap window (minutes)",
            "type": "integer",
            "description": "How far back before the previous run to search, so posts published during the last walk are not missed. Already-seen ids are still filtered out.",
            "default": 10,
            "minimum": 0,
            "maximum": 1440
          },
          "maxAgeDays": {
            "title": "Maximum age (days)",
            "type": "integer",
            "description": "Never walk further back than this many days, even when catching up after a long gap.",
            "default": 90,
            "minimum": 1,
            "maximum": 3650
          },
          "sinceTime": {
            "title": "Since time (backfill)",
            "type": "string",
            "description": "Optional lower bound for a backfill run: ISO 8601 timestamp or Unix epoch seconds. Uses X's epoch since_time operator, not day-granular since:.",
            "editor": "textfield",
            "nullable": true
          },
          "untilTime": {
            "title": "Until time (backfill)",
            "type": "string",
            "description": "Optional upper bound for a backfill run: ISO 8601 timestamp or Unix epoch seconds. Uses X's epoch until_time operator.",
            "editor": "textfield",
            "nullable": true
          },
          "proxyConfiguration": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Optional. Use a proxy if you run large jobs or see frequent IP-based rate limits.",
            "editor": "proxy",
            "sectionCaption": "Advanced",
            "sectionDescription": "Optional settings for large or repeated runs.",
            "nullable": true
          },
          "startDate": {
            "title": "Start date (legacy)",
            "type": "string",
            "description": "Deprecated — use 'Start date' instead. Kept so existing saved inputs keep working. Format: YYYY-MM-DD.",
            "editor": "textfield",
            "nullable": true,
            "sectionCaption": "Advanced (legacy)",
            "sectionDescription": "Deprecated aliases kept for backwards compatibility. Prefer the fields above."
          },
          "endDate": {
            "title": "End date (legacy)",
            "type": "string",
            "description": "Deprecated — use 'End date' instead. Kept so existing saved inputs keep working. Format: YYYY-MM-DD.",
            "editor": "textfield",
            "nullable": true
          }
        },
        "required": [
          "keywords"
        ]
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "X/Twitter Keyword & Hashtag Tweet Search Output",
        "description": "Tweets found for your keywords — text, author, engagement metrics, media and the search term that found each row.",
        "properties": {
          "tweets": {
            "type": "string",
            "title": "Tweets",
            "description": "Dataset containing one row per tweet, typed status rows when a keyword needs attention, and one run summary row.",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
          }
        }
      },
      "readme": "# X/Twitter Keyword & Hashtag Tweet Search\r\n\r\nGive this actor a list of keywords, phrases or hashtags and it pulls back matching X (formerly Twitter) posts — full text, engagement counts, media and the author who wrote each one. No sign-in, cookies or developer credentials needed; it reads the same public search results any visitor sees.\r\n\r\n## What this actor does\r\n\r\n- Runs each entry in `keywords` as its own independent search, so a single job can cover several unrelated topics at once.\r\n- Targets up to 1,000 posts per keyword via `maxTweets`; how many actually come back depends on how much volume that keyword has on X. A 300-row target has been verified (about 297 s); 500/1,000 are not verified, so treat `maxTweets` as a target, not a promise.\r\n- Runs several X search lanes (`latest`, `media`, opt-in `media-product`, `links`, optionally `replies`) and walks backwards through `since_time`/`until_time` windows. Guest search has **no cursor paging**; time-window backfill is the only way past the ~20-row page cap (a `product=Media` page can return 41-45 rows).\r\n- `Latest` returns newest-first. `Top` is **engagement-ranked output, not X's real Top timeline**: X never serves Top to logged-out callers, so the actor collects Latest pages and sorts them by like count client-side.\r\n- Narrows by language, date window, retweets/replies, verification, media presence, quotes, mentions, author, reply target, conversation id, and minimum like/retweet/reply counts.\r\n- Supports multi-word `matchMode` (`all`, `any`, `exact`) and `excludeWords`.\r\n- With `onlyNew` enabled, returns only posts newer than the previous run for the same `monitorId` + keyword. The actor persists a numeric snowflake watermark and an overlap window in the key-value store.\r\n- Stamps every row with `searchTerm`, `source` (`x-search` or `bing-news`), `sourceLane` (`latest` / `media` / `media-product` / `links` / `replies` / `window` / `bing` / `bing-title-only`) and `degraded`.\r\n- Emits a typed `status` row for a keyword that needs attention and one `summary` row per run.\r\n- **Empty fields are omitted:** never `null`, empty strings or empty arrays/objects in the output.\r\n\r\n## Output per post\r\n\r\nCore fields on every post record: `id`, `url`, `text`, `createdAt`, `language` (when X reports one), `searchTerm`, `source`, `sourceLane`, `degraded`, `type: \"post\"` and `collectedAt`.\r\n\r\nEngagement comes through `replyCount`, `retweetCount`, `likeCount`, `quoteCount`, `bookmarkCount`, and `viewCount`, plus a computed `engagementRate` — `(likes + retweets + replies + quotes) ÷ followers` — included only once the author's follower count is known and nonzero. Missing retweets/quotes/bookmarks/views/followers/bio are backfilled from the FxTwitter mirror after the X refresh; X values always win and only missing fields are copied.\r\n\r\nPost-type context: `isReply`, `isRetweet`, `isQuote`, `isNoteTweet`, and, for replies, `conversationId`, `inReplyToTweetId`, `inReplyToUserId`, `inReplyToUsername`, `parentTweet` (a nested summary of the post being replied to, when available). Every post row also carries `recordType: \"post\"`, `sourceUrl` (the canonical post URL) and `scrapedAt`.\r\n\r\nEntities parsed out of the text: `hashtags`, `mentions`, `urls`, `cashtags`.\r\n\r\nAttachments and extras, when present: `media` (one entry per photo/video/GIF with `type`, `url`, `previewImageUrl`, `width`, `height`, `durationSeconds`, `viewCount`, `altText`, `mediaKey`, `canMediaTag`, `focusRects`, `variants` with `url`/`contentType`/`bitrate`), `quotedTweet` / `quotedTweetId`, `poll`, `card`, `possiblySensitive`, `withheldInCountries`, `place`, `postedFrom`, `isEdited`, `editControl` (edit history metadata), `newsActionType`.\r\n\r\nThe author rides along as a nested `author` object: `id`, `username`, `name`, `bio`, `bioLinks`, `location`, `website`, `followers`, `following`, `likesGiven`, `postCount`, `mediaCount`, `createdAt`, `profileImageUrl`, `bannerUrl`, `profileImageShape`, `isBlueVerified`, `verified`, `verifiedType`, `isProtected`, `professionalType`, `superFollowEligible`, `superFollowedBy`, `superFollowing`, `hasGraduatedAccess`, `translatorType` and `pinnedTweetIds`. Fields X does not expose for a post are omitted.\r\n\r\n### Sample record\r\n\r\n```json\r\n{\r\n  \"type\": \"post\",\r\n  \"id\": \"1923456789012345678\",\r\n  \"url\": \"https://x.com/i/status/1923456789012345678\",\r\n  \"text\": \"Just shipped a new feature after three weeks of testing. Feels good. #buildinpublic\",\r\n  \"createdAt\": \"2026-06-02T09:15:00+00:00\",\r\n  \"language\": \"en\",\r\n  \"searchTerm\": \"buildinpublic\",\r\n  \"source\": \"x-search\",\r\n  \"sourceLane\": \"latest\",\r\n  \"degraded\": false,\r\n  \"replyCount\": 34,\r\n  \"retweetCount\": 112,\r\n  \"likeCount\": 1870,\r\n  \"quoteCount\": 9,\r\n  \"bookmarkCount\": 265,\r\n  \"viewCount\": 54300,\r\n  \"isReply\": false,\r\n  \"isRetweet\": false,\r\n  \"isQuote\": false,\r\n  \"hashtags\": [\"buildinpublic\"],\r\n  \"author\": {\r\n    \"id\": \"1092834756\",\r\n    \"username\": \"makerjane\",\r\n    \"name\": \"Jane Builds\",\r\n    \"followers\": 18400,\r\n    \"isBlueVerified\": false\r\n  },\r\n  \"collectedAt\": \"2026-06-02T10:03:41+00:00\"\r\n}\r\n```\r\n\r\n## Output per status row\r\n\r\nWhen a keyword needs attention, the actor pushes one typed record:\r\n\r\n```json\r\n{\r\n  \"type\": \"status\",\r\n  \"status\": \"blocked\",\r\n  \"message\": \"X guest search refused this network right now...\",\r\n  \"searchTerm\": \"apify\",\r\n  \"collectedAt\": \"2026-06-02T10:03:41+00:00\",\r\n  \"source\": \"none\",\r\n  \"sourcesTried\": [\"x-search\", \"bing-news\"],\r\n  \"xAttempts\": 96,\r\n  \"xOk\": 0,\r\n  \"xLastStatus\": 404,\r\n  \"bingDiscovered\": 0,\r\n  \"bingHydrated\": 0,\r\n  \"bingTitleOnly\": 0,\r\n  \"bingRows\": 0,\r\n  \"targetRows\": 20,\r\n  \"uniqueRows\": 0,\r\n  \"budgetSeconds\": 105,\r\n  \"budgetExhausted\": false,\r\n  \"degraded\": true,\r\n  \"partial\": false\r\n}\r\n```\r\n\r\nStatuses:\r\n\r\n| Status | Meaning |\r\n|---|---|\r\n| `noInput` | No keywords were provided. |\r\n| `noResults` | A source answered and returned zero matching posts (or every collected row was already seen by `onlyNew`). |\r\n| `blocked` | No source answered: X refused and Bing was unavailable or skipped because the query uses operators. |\r\n| `partial` | Rows were returned but the target was **not** reached (budget/limits stopped the walk, a full page appeared at the oldest edge, or X was blocked and Bing supplied fewer rows than requested). Reaching the target exactly is `ok`, even when rows are `degraded`. |\r\n| `budgetExhausted` | The per-keyword budget ran out before any source answered. |\r\n| `enrichmentPartial` | Rows were returned, but one or more per-tweet enrichment lookups failed. |\r\n\r\n`xOk` counts only X bursts that returned a parseable page of posts. An HTTP 200 with an empty payload or an empty timeline is treated as a refusal/no-answer, so it cannot mask `blocked` or `budgetExhausted`.\r\n\r\nEvery run also ends with one `type: \"summary\"` row carrying `recordType: \"runSummary\"`, `scrapedAt`, `startedAt`, `finishedAt`, `keywords`, `postRows`, `statusRows`, `bySource`, `sourcesTried`, aggregate `xAttempts` / `xOk` / `bingDiscovered` / `bingHydrated` / `bingTitleOnly`, `degradedKeywords`, `partialKeywords`, `blockedKeywords`, `durationSeconds`, `monitorId` and `onlyNew`. Consumers that count `type: \"post\"` are never confused by status or summary rows.\r\n\r\n## Known hard limits\r\n\r\nMeasured directly (no proxy, no cookies, no browser) in live probes on 2026-11-08:\r\n\r\n- **Guest search is a lottery.** Only ~6% of Latest bursts answered in a 32-attempt probe, and the 300-row backfill used 5,259 attempts / 20 answered bursts (0.38%). The same query can return 20 rows in seconds later, but there is no SLA.\r\n- **No real `Top`.** X serves Top only to signed-in callers, so `Top` output is a client-side like-sort of the Latest rows that were collected.\r\n- **No cursor paging.** One answered page is ~20 rows; `product=Media` can return 41-45. Backfill beyond one page is time-window walking only.\r\n- **300 rows is the measured backfill ceiling.** 300/300 was reached on `bitcoin`; 500 and 1,000 are unverified targets.\r\n- **Bing News is stale and shallow.** It discovers newsworthy posts only (0/65 sampled rows were <24 h old, 53/65 were <7 d) and walks at most 5 RSS pages (~50 unique ids). It cannot honour operator queries.\r\n- **Monitoring is duplicate suppression, not complete coverage.** `onlyNew` plus the overlap window protects the page boundary; posts published between runs can be missed when a keyword produces more than one page per interval.\r\n- **Enrichment depends on one third-party mirror.** FxTwitter fills retweets/quotes/bookmarks/views/author fields; vxtwitter/fxwitter/fixupx were unusable in the same environment. If FxTwitter fails, those fields stay absent; rows are still returned.\r\n\r\n## Input\r\n\r\n| Field | Type | Default | Description |\r\n|---|---|---|---|\r\n| `keywords` | array | – | **Required.** Words, phrases, hashtags or full search queries; each runs separately. |\r\n| `maxTweets` | integer | `20` | Target number of posts per keyword (1–1000). |\r\n| `maxItems` | integer | – | Same as `maxTweets` — use whichever name you prefer. |\r\n| `searchType` | string | `Top` | `Top` = engagement-ranked (client-side like-sort of Latest); `Latest` = newest-first. |\r\n| `sort` | string | – | Same as `searchType`. |\r\n| `language` | string | `Any language` | Restrict to one of 40 common X language codes, e.g. `en`, `es`, `ja`. |\r\n| `dateFrom` | date | – | Earliest post date to include. |\r\n| `dateTo` | date | – | Latest post date to include (inclusive). |\r\n| `excludeRetweets` | boolean | `false` | Drop retweets/reposts, keep only original posts. |\r\n| `excludeReplies` | boolean | `false` | Drop replies, keep only top-level posts. |\r\n| `onlyVerifiedUsers` | boolean | `false` | Keep only posts from verified accounts. |\r\n| `onlyImages` | boolean | `false` | Keep only posts containing at least one image. |\r\n| `onlyVideos` | boolean | `false` | Keep only posts containing at least one video or GIF. |\r\n| `onlyQuotes` | boolean | `false` | Keep only quote tweets. |\r\n| `mentioning` | string | – | Keep posts that mention this account. |\r\n| `author` | string | – | Keep posts written by this account. |\r\n| `inReplyTo` | string | – | Keep replies addressed to this account. |\r\n| `conversationId` | string | – | Keep posts in this conversation. |\r\n| `matchMode` | string | `all` | `all` (AND), `any` (OR) or `exact` (quoted phrase) for multi-word keywords. |\r\n| `excludeWords` | array | – | Words/phrases to exclude (`-word`, `-\"two words\"`). |\r\n| `includeLanes` | array | – | `latest`, `media`, `media-product`, `links`, `replies`. Default is latest + media + links; replies is added for 100+ row jobs. `media-product` asks for X's `product=Media` page directly (opt-in; 41-45 rows when it answers). |\r\n| `maxWindows` | integer | `0` | Cap backwards time-window passes per keyword; `0` = no explicit cap. |\r\n| `maxPasses` | integer | `0` | Cap total X search passes (core + windows); `0` = no explicit cap. |\r\n| `minimumLikes` | integer | – | Floor on like count. |\r\n| `minimumRetweets` | integer | – | Floor on retweet count. |\r\n| `minimumReplies` | integer | – | Floor on reply count. |\r\n| `onlyNew` | boolean | `false` | Return only posts newer than the previous run for this keyword. |\r\n| `monitoringEnabled` | boolean | `false` | Alias that enables `onlyNew` when `onlyNew` is not provided. |\r\n| `monitorId` | string | `default` | Namespace for the saved watermark; use different ids for independent schedules. `monitoringStateKey` is accepted as an alias. |\r\n| `overlapMinutes` | integer | `10` | Search this far before the previous run so late-arriving posts are not missed. |\r\n| `maxAgeDays` | integer | `90` | Never walk further back than this, even after a long gap. |\r\n| `sinceTime` | string | – | Backfill lower bound: ISO 8601 or Unix epoch seconds. |\r\n| `untilTime` | string | – | Backfill upper bound: ISO 8601 or Unix epoch seconds. |\r\n| `proxyConfiguration` | object | – | Optional; useful for large or repeated runs from the same IP. |\r\n| `startDate` / `endDate` | string | – | Older aliases for `dateFrom` / `dateTo` (`YYYY-MM-DD`). |\r\n\r\n### Example: two unrelated topics in one run\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"apify\", \"web scraping\"],\r\n  \"maxTweets\": 50\r\n}\r\n```\r\n\r\n### Example: live brand-mention watch\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"#YourBrand\", \"\\\"your brand\\\"\"],\r\n  \"searchType\": \"Latest\",\r\n  \"language\": \"en\",\r\n  \"maxTweets\": 200,\r\n  \"excludeRetweets\": true,\r\n  \"excludeReplies\": true\r\n}\r\n```\r\n\r\n### Example: scheduled only-new monitoring\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"#YourBrand\", \"@yourbrand\"],\r\n  \"searchType\": \"Latest\",\r\n  \"maxTweets\": 200,\r\n  \"onlyNew\": true,\r\n  \"monitorId\": \"brand-watch\",\r\n  \"overlapMinutes\": 15,\r\n  \"maxAgeDays\": 30\r\n}\r\n```\r\n\r\n### Example: bounded backfill without a cursor\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"artificial intelligence\"],\r\n  \"maxTweets\": 500,\r\n  \"sinceTime\": \"2026-01-01T00:00:00Z\",\r\n  \"untilTime\": \"2026-06-30T23:59:59Z\",\r\n  \"includeLanes\": [\"latest\", \"media\", \"links\"],\r\n  \"maxWindows\": 40\r\n}\r\n```\r\n\r\n### Example: top posts in a date window, above an engagement floor\r\n\r\n```json\r\n{\r\n  \"keywords\": [\"artificial intelligence\"],\r\n  \"maxTweets\": 500,\r\n  \"minimumLikes\": 100,\r\n  \"minimumRetweets\": 20,\r\n  \"dateFrom\": \"2026-01-01\",\r\n  \"dateTo\": \"2026-06-30\"\r\n}\r\n```\r\n\r\n## Use cases\r\n\r\n- **Brand monitoring** — schedule the actor with `onlyNew` to catch new mentions without repeating old rows.\r\n- **Trend and topic tracking** — measure how a subject's volume and engagement shift over weeks or months.\r\n- **Breaking-news watch** — pair `Latest` with a tight date range or `sinceTime`/`untilTime` to follow a fast-moving story.\r\n- **Content research** — surface the highest-engagement posts around a theme for inspiration.\r\n- **Academic or policy research** — assemble a dataset of public discourse on a topic, language or time period.\r\n- **Competitor comparison** — run several competing keywords side by side and compare volume and engagement.\r\n\r\n## FAQ\r\n\r\n**Do I need an X account or developer credentials?**\r\nNo — there's no credential field in the input at all. The actor reads what any logged-out visitor can already see on X's own search.\r\n\r\n**How many posts will I actually get per keyword?**\r\nUp to `maxTweets`, but that's a ceiling, not a promise. Guest search answers intermittently and returns ~20 rows per answered page (41-45 on an answered `product=Media` page). The actor multiplies those pages with lanes and time windows. A 300-row target has been verified; targets above 300 are unverified. An obscure keyword may return only a handful.\r\n\r\n**Why did I get a different count on a second identical run?**\r\nX's logged-out search doesn't expose stable, full pagination, and what it surfaces can shift between requests. If you need more rows, use `onlyNew: false`, narrow the date window, or add lanes rather than assuming something broke.\r\n\r\n**Should I use `Top` or `Latest`?**\r\n`Latest` for anything time-sensitive — breaking news, live mentions, a campaign in progress. `Top` gives you the strongest engagement among the rows the actor could collect, but it is a client-side like-sort of Latest results, not X's real Top timeline (X does not serve Top to guests).\r\n\r\n**How does only-new monitoring work without a cursor?**\r\nThe actor saves `lastMaxId` (numeric 19-digit snowflake), `lastRunAt` and the last 1,000 seen ids in the key-value store under `xkw:<sha1(monitorId + keyword)>`. Each run searches from `lastRunAt - overlapMinutes` (capped by `maxAgeDays`) and keeps rows with an id numerically above the watermark or created inside the overlap window and not already seen. A blocked run never advances the watermark. Bing-only fallback rows are remembered as seen so they are not re-emitted, but they leave the X gap open for the next run.\r\n\r\n**Is a proxy required?**\r\nNo, it's optional. Add one mainly if you're running large or frequent jobs from the same IP and want extra headroom against rate limiting.\r\n\r\n**How current is the data?**\r\nFully live — each run queries X at the moment it executes. Put the actor on a schedule with `onlyNew` for ongoing monitoring. Bing News fallback rows are not real-time: in live probes only 1.2% were under 24 hours old.\r\n\r\n**Why is a row marked `degraded: true`?**\r\nIt came from the Bing News fallback (hydrated or title-only). Those rows are newsworthy-only, can be days or years old, and lack retweets/quotes/views/bookmarks/followers until the enrichment layer fills what it can.\r\n\r\n**How do I get the results into another tool?**\r\nPull the dataset from the run page or the Apify API, wire up a webhook, or connect it to Zapier, Make, Google Sheets, or similar no-code tools.\r\n\r\n**Is this affiliated with X?**\r\nNo. This is an independent, third-party actor that reads publicly available X search results.\r\n",
      "title": "X/Twitter Keyword & Hashtag Tweet Search",
      "description": "Search X (Twitter) by keyword, phrase or hashtag and get tweet text, author, engagement metrics, media and language. Every row says which keyword found it. No login required.",
      "seoTitle": "Twitter (X) Keywords Scraper — Search X Posts",
      "seoDescription": "Search X (Twitter) by keyword, phrase or hashtag: tweet text, author, engagement metrics, media and language. Every row carries its keyword.",
      "categories": [
        "SOCIAL_MEDIA"
      ],
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "views": {
            "tweets": {
              "title": "Tweets",
              "transformation": {
                "fields": [
                  "type",
                  "searchTerm",
                  "text",
                  "author.username",
                  "createdAt",
                  "language",
                  "replyCount",
                  "retweetCount",
                  "likeCount",
                  "quoteCount",
                  "viewCount",
                  "engagementRate",
                  "isRetweet",
                  "isQuote",
                  "isReply",
                  "url",
                  "media",
                  "collectedAt"
                ]
              },
              "display": {
                "component": "table",
                "columns": [
                  {
                    "label": "Type",
                    "format": "text"
                  },
                  {
                    "label": "Keyword",
                    "format": "text"
                  },
                  {
                    "label": "Tweet",
                    "format": "text"
                  },
                  {
                    "label": "Author",
                    "format": "text"
                  },
                  {
                    "label": "Date",
                    "format": "text"
                  },
                  {
                    "label": "Language",
                    "format": "text"
                  },
                  {
                    "label": "Replies",
                    "format": "number"
                  },
                  {
                    "label": "Retweets",
                    "format": "number"
                  },
                  {
                    "label": "Likes",
                    "format": "number"
                  },
                  {
                    "label": "Quotes",
                    "format": "number"
                  },
                  {
                    "label": "Views",
                    "format": "number"
                  },
                  {
                    "label": "Engagement rate",
                    "format": "number"
                  },
                  {
                    "label": "Retweet?",
                    "format": "text"
                  },
                  {
                    "label": "Quote?",
                    "format": "text"
                  },
                  {
                    "label": "Reply?",
                    "format": "text"
                  },
                  {
                    "label": "URL",
                    "format": "link"
                  },
                  {
                    "label": "Media",
                    "format": "text"
                  },
                  {
                    "label": "Data collected at",
                    "format": "text"
                  }
                ]
              }
            }
          }
        }
      },
      "environmentVariables": {}
    },
    "buildNumber": "1.0.5",
    "imageDigest": "8d8153474df34a3718b4b70025f298e44eba40550ac73e12b0fb6f55af0bc920",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "1.0"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.011695555555555556
    }
  }
}