Skip to content

How do you build a Twitter auto reply bot with an API?

Last updated August 24, 2026

An auto reply bot is a trigger, a decision, and a write. Watching an account with monitor, registering the signed webhook it delivers to, and reading monitor/deliveries are all free routes. Polling user/mentions instead costs $0.0008 a page. Only the reply itself is a premium write: tweet/create bills $0.0016, so one answered mention lands under half a cent.

Every rate here is the pricing TwitterAPIs publishes. The billed rate is $0.0008 per call; $0.04 per 1,000 tweets is derived from it at a full 20-tweet page, which is the default page size rather than a guaranteed yield (source: twitterapis.com/pricing).

Three moving parts, not one program

Every bot in this shape splits the same way. Something notices a post worth answering. Something decides whether this particular post deserves a response and what that response says. Something writes back. Keeping those three separated is what lets you swap a polling trigger for a pushed one later without touching the wording logic or the write path at all, and it is also what makes the thing testable, since the decision half can be exercised against a file of captured events with no network involved and no credits spent. A bot written as one loop tends to grow a dedupe check that only fires on one of its two code paths. Capture a handful of real deliveries to a file early on, because that fixture is what lets you change the wording logic later without spending a single credit.

Trigger one, poll the mentions feed

user/mentions takes a handle and returns the posts naming it, cursor-paginated, and it is a thin wrapper over a to:username search returning the same tweet objects you would get from the search route directly. Polling is simple and needs no public endpoint of your own, which suits a bot running behind a laptop or inside a private network with no inbound route at all. Each page you pull is one standard read, so the tempo you choose is the cost you pay: polling every minute is 1,440 reads a day, polling every five minutes is under 300, and the difference over a month is most of the bot's bill. The cursor also lets you catch up after downtime rather than losing whatever arrived while the poller was off, which a naive poller reading only the first page will miss.

Trigger two, a monitor pushing to your webhook

Register an https destination with the webhook route first. Its response carries a signing secret shown exactly once, so store it immediately because no endpoint will ever return it again, and the URL is rejected if it does not resolve to a public address, which rules out localhost and private ranges during local development. Then point a monitor at the handle you care about. The first poll after creation baselines on that account's newest post, so an account with thousands of existing tweets will not flood you, and include_replies false narrows delivery to original posts only. Creating a monitor can also answer 503 capacity_unavailable, which means retry later rather than that anything is wrong. Deleting a webhook later does not delete the monitors pointed at it; they simply stop delivering until you repoint them at another one.

Verify the signature before trusting a payload

Every delivery is signed HMAC-SHA256 with the secret from webhook creation, so your handler should compute and compare before parsing anything into your own types. Fire webhook/{id}/test while building: it sends one real signed event immediately and returns delivered, status_code and error in the response with no polling required, answering 200 when delivery worked and 502 when your endpoint failed rather than ours. That test payload carries "webhook.test" in its event field rather than "tweet.created", which gives your handler a clean way to tell a diagnostic send apart from a real one and to avoid replying to a fixture during setup. Verify against the raw request body rather than a re-serialised copy of it, or the digest you compute will never match the one you were sent. A test send also works against a webhook a 410 response has disabled, so a fix can be checked before anything else changes.

Decide before you write, and dedupe hard

Real events are queued and retried, unlike the one-shot test send, so the same tweet_id can reach your handler more than once and that is normal behaviour rather than a fault to report. Key an idempotency table on that id and drop repeats before any wording logic runs, which also keeps a retry from spending a second premium write on an answer nobody asked for twice. Write the id down before the reply goes out rather than after, so a crash mid-write leaves you with a possible missing reply instead of a duplicate one, which is by far the cheaper failure to explain to a human afterwards. Key the table on tweet_id rather than on your own event id, since a retry arrives with a fresh delivery id but the same underlying post.

Never let the bot answer itself

Compare the author of every incoming event against the bot's own handle and discard a match immediately, in the same guard as the deduplication check so neither code path can skip it. Without that guard, a reply the bot just posted can arrive as the next trigger and the loop feeds on its own output, which burns credits at the write rate rather than the read rate and is visible publicly while it happens. Setting include_replies to false on the monitor narrows the flow further by holding replies back from delivery, though note replies are still polled and still advance the monitor's cursor, they are simply not sent to you. Note that include_replies has to be an actual boolean: a string or a number is rejected with a 400 rather than coerced, because guessing wrong there looks exactly like the account never posting.

Post the reply

tweet/create carries text plus reply_to holding the numeric id you are answering, and both may travel in the query string or a JSON body. media_ids and quote are available on the same call if the answer needs an image or a quoted post. It acts as the account whose session is registered, never a pooled one, so a bot with no session attached fails at exactly this step after everything upstream looked healthy in testing. The response returns ok, tweet_id and url for the new post, and those are worth recording next to the trigger id: that pairing is your audit trail when somebody asks why the account replied to a particular thread. Assert on ok rather than on the status code, and keep the returned url so a human can open the reply without reconstructing the link by hand.

Measure the lag, and notice a stalled monitor

monitor/deliveries returns your recent events, most recent first, up to 200 per call with a default of 50. Each row splits the wait into detected_lag_ms, the gap between the post's own timestamp and the moment the scheduler enqueued it, and delivery_lag_ms, the queue to POST time, with total_lag_ms summing them. Those are measured numbers rather than a published guarantee, and a null lag field means one timestamp was unavailable for that specific event rather than that something failed. Read them instead of guessing at how responsive the bot feels, because the honest figure is bound by a shared poll interval you do not control. A monitor being served slower than the published interval reports degraded, and that flag is what to alert on rather than a silent gap in your own logs.

What answering a thousand mentions costs

The trigger half can be free. Creating the webhook, creating the monitor, listing either, updating or deleting them, checking health and reading deliveries all sit on the free tier and consume no credits at all. That leaves the write: a thousand replies at $0.0016 comes to $1.60, and nothing else is added to it. Take the polling route instead and you add the reads on top, so checking mentions every five minutes for a month is roughly 8,640 calls at $0.0008, about $6.91, which is the real argument for standing up a public endpoint as soon as one is available to you. Both figures assume one reply per trigger; a bot that answers a whole thread rather than a single post multiplies the write side and nothing else. $0.0008 a call, per our published pricing.

What each part of the loop bills

Part of the loopRouteBilled per call
Register the delivery endpointwebhookFree
Fire one signed test deliverywebhook/{id}/testFree
Watch an account for new postsmonitorFree
Check whether a monitor is still healthymonitor/{id}/healthFree
Inspect delivery latencymonitor/deliveriesFree
Pull a page of mentions insteaduser/mentions$0.0008
Post the replytweet/create$0.0016
Answer privately insteaddm/send$0.0016
The goal of this document is to provide a mechanism for message authentication using cryptographic hash functions.
RFC 2104, HMAC: Keyed-Hashing for Message Authentication. Source

Questions and answers

Push or poll for the trigger?
Push if you can host an https endpoint: watching an account and receiving its posts costs nothing, so the entire notice half of the loop is free. Poll if you cannot expose one, and accept that each page of the mentions feed is a standard read charged at the usual rate. The wording and write halves of the bot are identical either way, so the choice is reversible.
Are the monitoring routes really free?
Yes. Creating a webhook, creating a monitor, listing either one, updating or deleting them, checking health and inspecting recent deliveries all sit on the free tier and consume no credits whatsoever. The billed part of an answered mention is the write itself, so the per-reply arithmetic is one premium write plus whatever the trigger happened to cost you. Only the write, and on the polling design the reads, ever show up on the bill at all.
How do I verify a delivery really came from you?
Every event is signed HMAC-SHA256 with the secret returned once when the webhook was created, so compute and compare before parsing anything. Store that secret at creation time, because no endpoint returns it afterwards and losing it means deleting and recreating the webhook. Use the free test send to prove your verification path works before you start relying on it in production. Verify against the raw request body rather than a re-serialised copy, or the digest will never match.
Will it flood me with old posts when I start?
No. The first poll after a monitor is created baselines on that account's newest post, so there is no backfill at all: an account with thousands of existing tweets sends nothing until it posts again. That makes creating a monitor safe on a busy handle, and it also means a monitor created during a quiet spell will look silent for a while before anything arrives. It also means a fresh monitor is not a way to catch up on posts made before it existed.
How do I stop it answering the same post twice?
Store every tweet_id you have already handled and check that store before doing anything else at all. Deliveries for genuine events are queued and can be retried, so a duplicate arriving is normal rather than a fault worth reporting. Writing the id down before the reply is issued, not after it returns, keeps a crash mid-write from producing two public answers on the same thread. Key the table on tweet_id rather than on the delivery id, which changes between retries.
How do I keep it from replying to itself?
Compare the author of every incoming event against the bot's own handle and discard a match immediately. Without that guard a reply the bot just posted can come back as the next trigger and the loop feeds itself at the premium write rate. Setting include_replies to false on the monitor narrows the flow further by holding replies back from delivery entirely. Both guards belong in the same place so that neither trigger path can skip one of them.
How quickly does an event reach my handler?
Measure it rather than assume. Each row from the deliveries route breaks the wait into detected_lag_ms, the gap between the post's own timestamp and the moment the scheduler queued it, and delivery_lag_ms, the queue to POST time, with total_lag_ms summing the two. The poll interval behind those numbers is shared and is not published as a service guarantee. A null lag field on a row means one timestamp was missing for that event, not that delivery failed.
How do I know a monitor has not quietly stopped?
Read its health. The per-monitor route reports degraded and events_possibly_missed alongside last_tweet_id and last_poll_at, so a stall is visible rather than silent. The account-wide rollup answers in one call with active and paused counts plus 24 hour delivery outcomes, and a key with no monitors gets zeroes back rather than a 404 you have to special-case. Alert on degraded rather than on a gap in your own logs, which is far harder to notice in time.
Can it answer in a direct message instead?
Yes, swap the write for dm/send, which takes recipient_id as a numeric user id plus text and bills at the same premium rate as posting publicly. You need the id rather than the handle, and the account's registered session applies exactly as it does for a public reply. The response returns message_id and conversation_id so the exchange can be threaded later. Resolve the recipient id once and cache it rather than looking it up before every single reply.
What does a thousand replies a month cost?
$1.60 on the push design, because the write is the only billed part and a thousand calls of tweet/create at $0.0016 is the entire bill. Polling adds reads on top of that: checking mentions every five minutes for a month is roughly 8,640 calls at $0.0008, about $6.91, which is the practical argument for hosting a webhook endpoint instead. The trigger design is therefore where the bill is decided, not the wording logic behind it.

Start with $0.50 in free credits

No credit card. Roughly 12,500 tweets to test every endpoint.