X API DM: 43 ways one send gets refused
The X API v2 DM endpoints now return 43 named refusal types, all on the same 403. What each one means, the 1,440-a-day ceiling, and what reading an inbox costs.
On this page · 9 sections
- Quick answer
- Nine routes, three of which send
- 43 refusals, one status code
- The spec will not tell you which refusal an endpoint can return
- The ceiling is 1,440, and one report says 1
- Reading an inbox costs more than writing to it
- Payload gaps worth knowing before you parse
- The encrypted inbox is a different API
- Should you build DM automation on this
The short version
- ▸On 26 October 2026 X gave its DM refusals names. The live API description now carries 43 dm-* problem types, out of 61 problem types for the whole v2 surface, and they all arrive as the same HTTP 403. Your code reads the type field or it learns nothing.
- ▸Most of the 43 describe the sending or receiving account, not your request: sender not verified, sender phone required, sender read only, recipient not accepting, safety limited, safety rejected. That list is the closest thing X has published to the rules that decide whether a DM is deliverable at all.
- ▸The documented send ceiling is 1,440 per 24 hours, and it is the same number per app and per user, so authorising a second account does not raise your app's total. One developer posted rate-limit headers showing a limit of 1 per 24 hours in February 2026 and got no reply.
- ▸Reading is the expensive half. DM Event: Read is $0.010 per event, /2/dm_events is not on the Owned Reads table even though it only ever returns your own inbox, and a full-rate poll runs $15 every 15 minutes. The dm.received webhook costs the same per event with no polling.
- ▸The spec declares no mapping from endpoint to possible refusal, no HTTP status on any of the 43 types, and no maximum length on text while shipping a dm-text-too-long refusal. Expect to discover the rules by hitting them.
Quick answer
The X API DM endpoints, /2/dm_conversations and /2/dm_events, got a lot more honest on 26 October 2026. A refused send still returns HTTP 403, but the body now carries one of 43 named dm-* problem types, and those 43 make up most of the error vocabulary of the entire v2 API. Read as a set, they are the first published description of what X checks before it lets a DM through, and most of the checks are about the sending account rather than your request.
Last updated: November 2026
Everything below is checked against the live API description at api.x.com/2/openapi.json, version 2.174, read on 9 November 2026, plus X's rate-limit and pricing pages the same day.
Nine routes, three of which send
X (formerly Twitter) exposes a smaller DM surface than its reputation suggests.
| Method | Route | What it does |
|---|---|---|
| POST | /2/dm_conversations/with/:participant_id/messages | Send to one person |
| POST | /2/dm_conversations/:dm_conversation_id/messages | Send to an existing thread |
| POST | /2/dm_conversations | Open a group conversation |
| GET | /2/dm_events | Every event for the authorised user |
| GET | /2/dm_conversations/:id/dm_events | One conversation by id |
| GET | /2/dm_conversations/with/:participant_id/dm_events | One conversation by person |
| DELETE | /2/dm_events/:id | Delete one of your own messages |
| GET | /2/dm_conversations/media/:dm_id/:media_id/:resource_id | Download an attachment |
| POST | /2/users/:id/dm/block and /unblock | Block a user from DMing you |
Two details in that table bite people.
POST /2/dm_conversations is not the general-purpose create route its name suggests. In the live spec its conversation_type is an enum with exactly one permitted value, Group, and participant_ids has minItems: 2. You cannot open a one-to-one conversation with it, and you cannot open a group with a single other person. The cap is maxItems: 49, so the API's own group ceiling is you plus 49.
The media download route is the only DM route in the spec that does not also accept UserToken, the OAuth 1.0a scheme. It lists OAuth2UserToken with dm.read and nothing else. An OAuth 1.0a integration can therefore read a DM event that has an attachment and cannot fetch the attachment.
Scopes are mildly absurd elsewhere too. Sending needs three: dm.write, plus users.read and tweet.read, neither of which has anything to do with messaging. Deleting needs dm.write and dm.read together. The wider pattern of which scope sits on which call is in our notes on X API OAuth.
43 refusals, one status code
Before the October change, a blocked DM came back as a 403 with type: about:blank and the text "This operation is not permitted." The changelog is blunt about what that was: refusals that X could not explain safely were indistinguishable from refusals it simply had not bothered to name.
Now count them in the live description. The Problem schema is a discriminated union keyed on type, with 61 members. 43 of those 61 are dm-*. More than two thirds of the named failure modes of the whole X API v2 are a Direct Message being turned down.
They sort into four groups.
Your request is malformed. dm-text-empty, dm-text-too-long, dm-bad-attachment, dm-bad-media, dm-bad-url, dm-bad-cta, dm-bad-quick-reply, dm-bad-custom-profile, dm-unsupported-message, dm-conversation-name-too-long, dm-too-few-participants, dm-too-many-participants.
The conversation is wrong. dm-conversation-not-found, dm-not-found, dm-not-owner, dm-sender-not-in-conversation, dm-participant-not-found, dm-participant-not-added, dm-request-in-progress, dm-duplicate, dm-not-delivered.
The recipient will not take it. dm-recipient-not-accepting, dm-recipient-cannot-receive, dm-recipient-not-found, dm-recipient-unavailable, dm-recipient-restricts-groups, dm-recipient-blocks-sender, dm-recipient-encrypted-unavailable, dm-participant-restricted, dm-participant-unavailable.
The sender is the problem. dm-sender-not-verified, dm-sender-phone-required, dm-sender-read-only, dm-sender-dms-closed, dm-sender-declined-dms, dm-sender-blocks-recipient, dm-sender-not-permitted, dm-sender-unavailable, dm-sender-encrypted-unavailable, dm-inactive-device, dm-safety-limited, dm-safety-rejected, dm-rate-limited.
That fourth group is the interesting one, because nothing in X's product documentation says a sending account can need to be verified or have a phone number attached before the API will deliver a message. The refusal taxonomy says it. dm-safety-limited and dm-safety-rejected are two distinct outcomes, which implies a reversible state and a terminal one. dm-inactive-device suggests the account's own client activity is an input.
Treat the list as a map of what X evaluates, not as a promise about what it does. None of these 43 schemas carries a description, a title string or an HTTP status value in the spec. The changelog supplies the status, once, in prose: still 403.
The spec will not tell you which refusal an endpoint can return
Here is the gap that will cost you a retry policy.
Every DM operation in version 2.174 declares exactly two responses: its success code, and default pointing at the whole 61-member Problem union. There is no per-endpoint list. A generated client gives you a union of 61 types on every call and no way to know that dm-recipient-restricts-groups can only come from a group route.
So the triage has to be hand-written, and it splits three ways:
- Retry later:
dm-rate-limited,dm-not-delivered,dm-request-in-progress. - Never retry, drop the recipient: the whole recipient group, plus
dm-sender-blocks-recipient. - Stop the worker and page someone: the sender group.
dm-safety-limitedon one send means the next thousand are also going to fail, and hammering them is the behaviour that turns a limit into an enforcement action under X's automation rules.
A blank 403 now carries information it did not carry three weeks ago. If type is about:blank with "This operation is not permitted", X has decided the real reason is not safe to tell you, which in practice means it concerns the recipient. Retrying teaches you nothing.
The ceiling is 1,440, and one report says 1
Sends are capped at 15 per 15 minutes and 1,440 per 24 hours per user. The per-app cap is also 1,440 per 24 hours.
Read those two numbers together, because the arithmetic is the whole story: 1,440 a day is one DM a minute, and the per-app and per-user figures are identical. Getting a second user to authorise your app does not raise your app's daily total. For any outbound use case the app limit is the real ceiling, and it is small.
| Route | Per app | Per user |
|---|---|---|
| The three send routes | 1,440/24hrs | 15/15min, 1,440/24hrs |
| The four lookup routes | not listed | 15/15min |
DELETE /2/dm_events/:id | 4,000/24hrs | 300/15min, 1,500/24hrs |
POST /2/users/:id/dm/block | 25/15min, 1,000/24hrs | 10/15min, 400/24hrs |
You can delete more messages per day than you are allowed to send, which is a strange shape for a messaging API and a useful one if you are cleaning up.
Then there is what developers actually observe. In February 2026 someone posted their 429 headers for the send routes: x-app-limit-24hour-limit: 1 and x-user-limit-24hour-limit: 1, against a documented 1,440. That thread has no staff reply as of today. A longer 2025 thread on the same symptom got two staff questions in April 2025 and then silence, and the second half of that report, a send rejected with "You are not a participant in this conversation" on a thread where the request had been accepted, is exactly what dm-sender-not-in-conversation now names. Naming a bug is not fixing it.
Practical consequence: read x-app-limit-24hour-remaining and x-user-limit-24hour-remaining off every response and trust them over the documentation. The general x-rate-limit-remaining header is a different, much larger bucket and will look healthy while the DM bucket is empty. How the per-app and per-user windows interact across the rest of the API is covered in X API rate limits.
Reading an inbox costs more than writing to it
The rate card prices DM work in two rows. Sending is DM Interaction: Create at $0.015 per request. Reading is DM Event: Read at $0.010 per resource, and a resource is one event.
Send side, the ceiling does your budgeting for you: 1,440 requests at $0.015 is $21.60 a day, and you cannot spend more than that through one app no matter what you build.
Read side has no such mercy. GET /2/dm_events takes max_results up to 100, and the limit is 15 requests per 15 minutes per user. Poll at the ceiling and that is 1,500 events, or $15, every 15 minutes. Sixty dollars an hour to watch an inbox.
Two things make it worse than it looks.
/2/dm_events is not on the Owned Reads table. That table drops the price to $0.001 per resource for twelve routes that return your own data, including your own followers, your own bookmarks and your own mutes. The DM lookup endpoints only ever return the authenticated user's own conversations. They are self-data by definition, and they bill at ten times the self-data rate.
And daily deduplication is doing less for you here than elsewhere. Resources are deduplicated inside a UTC day, so re-reading the same event is free that day. New messages are new resources. A busy inbox is a recurring cost, not a one-off.
Stop polling. That is the fix X implies and never states. Webhook delivery through the Activity API bills dm.received at $0.010 per event, the same unit price, and lists dm.sent, dm.read and dm.indicate_typing as not billed. You pay for inbound messages either way. Polling adds a 15-requests-per-15-minutes ceiling, a latency floor and a pile of charges for events you already have. Our walkthrough of the X Activity API covers subscribing by user id.
One honesty note, because it matters to a cost model: nothing published says whether a refused send is billed. The write row is priced per request, not per delivered message. And GET /2/usage/tweets, the only usage endpoint, returns Post consumption only, so there is no API-side ledger for DM spend at all. Measure a real send against the Developer Console balance before you promise anyone a number. How the rest of the card behaves is in X API pricing.
Payload gaps worth knowing before you parse
Four small things in the live description that will surprise you at runtime.
text has no maximum. minLength: 1 and nothing else, on all three send bodies, while dm-text-too-long sits in the union. The limit is real and unpublished.
The group-create message object requires nothing. The two message routes use anyOf to demand text or attachments. The group route's nested message object has no required property at all, so {"conversation_type":"Group","participant_ids":["1","2"],"message":{}} is schema-valid and will be refused as dm-text-empty.
dm_event.fields has 7 values and omits the three you most want. attachments, created_at, dm_conversation_id, entities, event_type, id, text. Not sender_id, not participant_ids, not referenced_posts, which are exactly the three things that are expansions instead. While you are in that parameter list: the expansions enum declares attachments.media_keys twice, in an array marked uniqueItems: true. The spec violates its own constraint.
participant_ids is not on your messages. Since 3 June 2025, MessageCreate events carry no participant_ids, and join and leave events list only the users who joined or left rather than the whole room. The DmEvent schema still declares the field unconditionally, so a generated type tells you it is there. For one-to-one threads you can recover both sides from the conversation id, which is the two user ids joined by a hyphen. For groups you have to track membership yourself from the join and leave events, which is also why the default event_types filter includes all three.
Lookup reaches back 30 days. Nothing on the pay-per-use surface goes further.
The encrypted inbox is a different API
If your reads come back empty while the messages are plainly visible in the app, the conversation is encrypted. The v2 DM endpoints see plaintext only, and they return an empty result rather than an error, which looks exactly like a pagination bug.
X staff confirmed this on 3 February 2026 and said XChat support for the API was being explored. What shipped instead was a separate product, the /2/chat endpoint family, with client-side key management and its own much lower ceilings. A March 2026 report of GET /2/dm_events/:id returning 200 {} for upgraded conversations is still sitting there with no reply. The spec's dm-recipient-encrypted-unavailable and dm-sender-encrypted-unavailable types are the new, named version of that wall. We covered the encrypted route end to end in X Chat API.
Should you build DM automation on this
Three questions settle it.
Is your volume under 1,440 a day? If not, this API cannot do the job and no tier on the pay-per-use card raises that number. Transactional sends, support replies and onboarding nudges fit. Outbound campaigns do not, which is also the behaviour the automation rules treat as spam. We go through what X's own rules actually say in what is an automated DM.
Are you reading more than you write? Then your cost is the read side, and the answer is webhooks, not a poll loop. Price the inbox before the outbox.
Does the sending account pass checks you cannot see? The refusal list says verification state, phone number, read-only status and safety state all gate delivery. Test on the real account you intend to send from, early, because dm-sender-not-verified on day one is cheaper to find than on launch day.
If what you want is an account that keeps posting and replying while you are away from the keyboard, DMs are an odd place to start: the whole surface is capped at one message a minute. X-Autopilot works the public side of that problem, running on your own Mac in your own browser session, so there is no shared cloud IP pool and no delegate account in the path. That is a smaller detection surface than a hosted scheduler, not an absent one, and driving engagement through a browser sits in a gray area under X's automation rules rather than being something X endorses. The DM endpoints above are fully sanctioned. They are just very, very narrow.
Frequently asked
Answers indexed by Google + AI assistants.
What is the X API DM endpoint?+
There are nine routes. You send with POST /2/dm_conversations/with/:participant_id/messages for a one-to-one message, POST /2/dm_conversations/:dm_conversation_id/messages for an existing thread, and POST /2/dm_conversations to open a group. You read with GET /2/dm_events and two per-conversation variants, delete with DELETE /2/dm_events/:id, download attachments through a media route, and block or unblock DMs from a user with POST /2/users/:id/dm/block and /unblock. All of them need OAuth user context with dm.read or dm.write.
How many DMs can I send through the X API per day?+
X documents 1,440 per 24 hours per app and 15 per 15 minutes plus 1,440 per 24 hours per user on all three send routes. 1,440 a day is one a minute, which is the 15 per 15 minutes window sustained, so the two limits agree. Deleting is less restricted than sending: DELETE /2/dm_events/:id allows 300 per 15 minutes and 1,500 per 24 hours per user.
Why does the X API return 403 when I send a DM?+
Since 26 October 2026 the 403 body carries a type field naming the refusal, such as https://api.x.com/2/problems/dm-rate-limited or dm-recipient-not-accepting. X says detail names the cause only when that was already specific, or when the wording does not reveal a private fact about the recipient. Refusals it cannot explain safely keep type about:blank and the text This operation is not permitted, so a blank 403 is now meaningful: it is the deliberately unexplained class.
How much does the X API DM cost?+
Sending is billed as DM Interaction: Create at $0.015 per request, so the 1,440-a-day ceiling caps a single app at $21.60 a day. Reading is DM Event: Read at $0.010 per event returned. Nothing published says whether a refused send is billed, and GET /2/usage/tweets reports only Post consumption, so there is no programmatic ledger for DM spend.
Can the X API read encrypted DMs?+
No. The v2 DM lookup endpoints see plaintext conversations only, and an encrypted thread comes back empty rather than erroring. X staff confirmed on 3 February 2026 that the DM endpoints do not support XChat messages and said support was being explored. A March 2026 report of GET /2/dm_events/:id returning 200 with an empty body for upgraded conversations still has no reply. Encrypted threads are the separate Chat API.
How far back can I read DMs with the X API?+
30 days. X's DM lookup documentation states that events from up to 30 days ago are available through these endpoints. There is no archival DM endpoint on the pay-per-use surface, so anything older has to come from your own stored copy or from a user's account archive download.
- X developer changelog, 'Improved Direct Message refusal error details', 26 October 2026: the 403 is unchanged, the problem type now identifies the refusal, detail names the cause only when already specific or when it does not reveal a private fact about the recipient, and unexplainable refusals stay about:blank (accessed 9 November 2026)
- X live OpenAPI description at api.x.com/2/openapi.json, version 2.174: 43 dm-* members of the 61-entry Problem discriminator mapping, every DM operation declaring only a success code plus a default Problem, conversation_type with Group as its only enum value, participant_ids minItems 2 and maxItems 49, text with minLength 1 and no maximum, the 7-value dm_event.fields enum, attachments.media_keys listed twice in a uniqueItems expansions enum, and the media download route accepting OAuth2UserToken only (accessed 9 November 2026)
- X developer docs, 'X API Rate Limits': 1,440/24hrs per app and 15/15min plus 1,440/24hrs per user on the three DM send routes, 15/15min per user on the four lookup routes, 4,000/24hrs per app on DM delete, and 25/15min per app on the DM block routes (accessed 9 November 2026)
- X developer docs, 'Pricing': DM Event: Read at $0.010 per resource, DM Interaction: Create at $0.015 per request, dm.received webhook deliveries at $0.010 with dm.sent and dm.read not billed, and an Owned Reads table of twelve routes that does not include any DM endpoint (accessed 9 November 2026)
- X Developers forum, 'Upcoming Change to X API DM Events Endpoints Behavior' by @taycaldwell, posted 30 April 2025 and shipped 3 June 2025: MessageCreate events no longer carry participant_ids, and join and leave events list only the users who joined or left (accessed 9 November 2026)
- X Developers forum, 'Unexpected Rate Limit on DM Endpoints (1 call, per user, per 24 hours)', posted 22 February 2026: 429 responses carrying x-app-limit-24hour-limit 1 and x-user-limit-24hour-limit 1 against documented limits of 1,440, with no staff reply as of 9 November 2026
- X Developers forum, 'X API BUG: I found out why we can't access DMs after Conversation Upgraded', posted 29 January 2026 with an X staff reply on 3 February 2026 stating that the X API v2 DM endpoints do not currently support XChat encrypted messages (accessed 9 November 2026)
Product designer and indie hacker. Runs the agent on his own X account every day and writes up what the data shows, including when it's inconvenient.
Follow on X →