How do you set up a Twitter webhook?
Last updated August 24, 2026
Setting up a webhook is two calls. POST to webhook with an https url and the response hands back a wh_ id plus a signing secret shown exactly once, so store it before you close the terminal. Then POST to webhook/{id}/test to fire one signed delivery and read status_code back. Webhook routes are free and burn no credits, unlike the $0.0008 read endpoints.
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).
Registering the destination
POST to webhook with one parameter, url, and nothing else in the body. The response carries id, url, status, secret and created_at, with status coming back active and created_at as an ISO 8601 string. That id is the handle you pass as webhook_id when you create a monitor, so keep it beside the secret. There is no separate enable step and no callback challenge to answer: registration is finished when the response arrives. The documented example returns an id shaped like wh_9f2a1c, an opaque string rather than a number, so store it in a text column. One destination can serve several monitors, which is the ordering the endpoint notes recommend: create the webhook first, then point monitors at its id.
Why the URL has to be publicly routable
The url parameter is rejected unless it resolves to a public address. Localhost, private IP ranges and link-local addresses are all refused at create time, which is the most common reason a first attempt fails. A tunnel aimed at your laptop works only once it gives you a real https hostname that resolves from outside your own network, so 127.0.0.1 or a 10.x host will not register no matter how well the handler behaves. Plain http is not an option either, since the parameter is documented as an https delivery endpoint. Deploy the receiver somewhere reachable before you register it, because the check runs against the address you submit rather than against whether anything answers there.
The secret you only see once
secret appears in the create response and in no other response anywhere on the API. List Webhooks returns id, url, status and created_at for every registered destination and deliberately omits it, and there is no rotate call and no reveal call to fall back on. If you lose the value, the documented remedy is to delete that webhook and register the URL again, which mints a new id alongside a new secret, so every monitor pointed at the old id then needs repointing. Write the value into your secret store in the same code path that reads the response, before anything gets logged. The example is shaped whsec_ followed by hex, so an ordinary string column holds it.
Verifying the HMAC-SHA256 signature
Every delivery is signed HMAC-SHA256 with the secret that webhook was created with. Your handler recomputes the digest over the raw request body, keyed with the stored secret, and compares it against the signature the request carries. Read the body as bytes before any JSON parsing, because re-serializing a parsed object changes whitespace and key order and produces a digest that can never match. Compare the two digests with a constant-time function rather than plain string equality. Reject anything that fails, and do not fall back on trusting fields inside the payload: the signature is the only part of the request that somebody who guessed your URL cannot produce for themselves.
Firing a test delivery
POST to webhook/{id}/test sends one real signed event to the registered URL immediately and reports the outcome in the same response, so there is nothing to poll and no queue to inspect. The body carries id, delivered, status_code and error. delivered is true when your endpoint answered 2xx inside the delivery timeout, status_code is whatever it answered, and error holds a readable reason when delivery failed and null when it did not. status_code is itself null when the request failed before any response came back at all, which is what a DNS failure, a TLS problem or a refused connection looks like from this side of the wire. There is no route that reads the outcome of a past test later, which is exactly why it is returned inline.
What a test send deliberately does not do
A test is a one-shot diagnostic. Unlike a real monitor event it is never queued, never retried and never dead-lettered, so a receiver that was briefly down will not see it turn up later. The route also fires against a webhook that a 410 Gone response has disabled, which exists so you can prove a fix works while no separate re-enable action exists. The payload's event field reads webhook.test rather than tweet.created, letting a handler accept it for signature verification while refusing to treat it as a post. The HTTP status of the test call mirrors the outcome: 200 when delivered is true, 502 when it is false, meaning your endpoint failed rather than ours.
Listing what is registered
GET webhook takes no parameters and returns every destination on the key as a webhooks array plus a count. Each element holds id, url, status and created_at, which is enough to reconcile what your infrastructure believes it registered against what the API actually holds. Call it before creating another one, since nothing stops you registering the same URL twice and ending up with two ids delivering into the same handler. The listing is scoped by the key rather than by a path parameter, so a key with nothing registered gets an empty array and a count of zero instead of an error, which makes it safe to run from a startup check.
Deleting and repointing
DELETE webhook/{id} removes a destination and answers with deleted set to true plus the id you asked about. The monitors aimed at it are not deleted with it. They keep their watched handle, their cursor and their filters, and simply have nowhere to send anything until you repoint them. Repointing is a POST to monitor/{id} carrying a new webhook_id, which changes that field alone and leaves status, domain_filter and include_replies untouched. Do the delete last: because the create call is the only place a secret is ever issued, replacing a destination means create, repoint every monitor, then delete the old id, or deliveries stop while you are still wiring up the new secret.
What the delivery pipeline costs
Nothing. All four webhook routes carry a cost of zero, and so does every monitor route beside them, which is fourteen paths in the Monitoring family and nineteen zero-rated endpoints across the whole catalog. Registering, listing, testing and deleting therefore never draw on your balance, and neither does the delivery of an event. Spending starts where your handler does: a delivery tells you a post id exists, and any call you make afterwards to expand it, read the author or pull the surrounding thread bills at its own rate, with a standard read at $0.0008. Budget the reads your handler triggers rather than the notifications you receive. The $0.50 of signup credit therefore goes entirely to those reads and none of it to the plumbing. That rate is $0.0008 a call, per our own rate card.
The calls that touch a webhook, and what each returns
| Call | Response fields | What to know | Cost |
|---|---|---|---|
| POST webhook | id, url, status, secret, created_at | Only place the secret is ever shown | $0 |
| GET webhook | webhooks array, count | Secret is omitted on purpose | $0 |
| DELETE webhook/{id} | deleted, id | Monitors survive and stop delivering | $0 |
| POST webhook/{id}/test | id, delivered, status_code, error | One shot, never queued or retried | $0 |
| POST monitor | id, handle, status, poll_interval_ms, created_at | Takes webhook_id to attach a destination | $0 |
| POST monitor/{id} | id, status, degraded, updated_at | Repoints webhook_id without touching filters | $0 |
The goal of this document is to provide a mechanism for message authentication using cryptographic hash functions.
Questions and answers
- Where do I find the webhook signing secret again?
- You do not. The secret is issued once in the create response and is absent from List Webhooks and from every other route, and there is no rotate or reveal call to fall back on. If it is gone, delete that webhook and register the URL again to mint a fresh id and a fresh secret, then update whichever monitors pointed at the old id with a webhook_id change so deliveries resume.
- Why was my webhook URL rejected at create time?
- Because it did not resolve to a public address. The create call refuses localhost, private IP ranges and link-local hosts outright, so a tunnel aimed at your laptop keeps failing until it hands you a routable https hostname. The check runs on the address you submit rather than on whether your handler answers, which is why a receiver that works perfectly on your machine still comes back rejected.
- Does receiving events cost anything per delivery?
- No. Creation, listing, testing and deletion are all zero-rated, and the delivery of an event costs nothing either. Nineteen endpoints across the catalog bill nothing at all and this family sits inside that group, so the notification pipeline never moves your balance in either direction. What does cost money is whatever your handler calls next to turn a delivered post id into data you can actually use.
- How do I confirm signature verification works before going live?
- Fire webhook/{id}/test. It sends one genuinely signed event immediately and reports delivered, status_code and error inline, so a handler that rejects the signature shows up as a non-2xx on the spot rather than after a real post lands at three in the morning. Because the payload's event field is webhook.test, you can accept it for verification while keeping it out of whatever your pipeline does with real posts.
- What happens to my monitors if I delete the webhook?
- They survive the deletion. Nothing cascades, so each monitor keeps its handle, its cursor, its domain filter and its reply setting, and simply has no destination to send to. Send a POST to monitor/{id} with a replacement webhook_id and delivery resumes with no other setting touched. The account being watched never needs recreating, which matters, because a monitor cannot be re-pointed at a different handle.
- Can I test a webhook that has already been disabled?
- Yes. The test route fires against a webhook that a 410 Gone response has disabled, and that is deliberate: it is how you prove a fix works while no separate re-enable action exists. Keep in mind the test is never queued, retried or dead-lettered the way a real monitor event is, so a single result tells you the state right now and nothing about any backlog.
- How does my handler tell a test from a real post?
- Read the event field on the payload. A test send carries webhook.test where a real delivery carries tweet.created, so one branch is enough to keep a diagnostic out of your database. Both are signed the same way with the same secret, so the verification path stays shared and only the handling after verification differs, which is the point of giving the two payloads different event values.
- What comes back when my endpoint is down?
- delivered is false, error holds a readable reason, and the test call itself answers HTTP 502 rather than 200, which is the API stating the failure was on your side. status_code carries whatever your server returned, or null when nothing was returned at all, so a null there points at DNS, TLS or a refused connection rather than at anything inside your route handler.
- Can one webhook receive events from several monitors?
- Yes, and the endpoint notes recommend exactly that ordering: create the destination first, then point one or more monitors at its id. The tradeoff is that deleting that destination silences every monitor aimed at it in a single step, so a watch you cannot afford to lose is worth its own registration. Repointing later is one webhook_id update per monitor, nothing more.
- Do I need a webhook before I create a monitor?
- In practice yes. Create Monitor accepts webhook_id as an optional field and does not reject a monitor with none attached, but nothing is delivered until one is set, so the documented order is webhook first and monitor second. If you built them the other way round, a POST to monitor/{id} carrying the webhook_id closes the gap without recreating the watch or resetting its baseline.
Keep reading
Start with $0.50 in free credits
No credit card. Roughly 12,500 tweets to test every endpoint.