Skip to main content
RapidDev - Software Development Agency
API AutomationsX (Twitter)

How to Automate X (Twitter) DM Campaigns using the API

X's DM API (POST /2/dm_conversations/with/:participant_id/messages) lets you send direct messages programmatically — but Basic tier ($200/month) is effectively limited to 1 DM request per 24 hours due to a known, unresolved bug since August 2025. Pro tier ($5,000/month) works as documented at 15 requests per 15 minutes. The platform cap is 500 DMs per day per account regardless of tier. Know the true cost before building.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced6 min read6–10 hoursX (Twitter)Last updated May 2026RapidDev Engineering Team
TL;DR

X's DM API (POST /2/dm_conversations/with/:participant_id/messages) lets you send direct messages programmatically — but Basic tier ($200/month) is effectively limited to 1 DM request per 24 hours due to a known, unresolved bug since August 2025. Pro tier ($5,000/month) works as documented at 15 requests per 15 minutes. The platform cap is 500 DMs per day per account regardless of tier. Know the true cost before building.

Quick facts about this guide
FactValue
PlatformX (Twitter)
Rate limitsPro: 15 req/15 min; Basic: 1 req/24h (bug); Platform cap: 500 DMs/day
DifficultyAdvanced
Time required6–10 hours
Last updatedMay 2026

API Quick Reference

Auth

Rate limit

Pro: 15 req/15 min; Basic: 1 req/24h (bug); Platform cap: 500 DMs/day

Format

SDK

REST only

API overview

Base URLhttps://api.x.com/2

Authentication

  1. 1Go to developer.x.com and create or open your Project and App
  2. 2Enable OAuth 2.0 User Authentication — set Type of App to 'Web App, Automated App or Bot'
  3. 3Add dm.write, dm.read, users.read, and offline.access to your requested scopes
  4. 4Set a callback URL for the PKCE authorization flow
  5. 5Generate code_verifier and code_challenge (S256 method), redirect user to /i/oauth2/authorize
  6. 6Exchange authorization code for access and refresh tokens at POST /2/oauth2/token
  7. 7The offline.access scope ensures you receive a refresh token for long-running campaigns
typescript
1import os
2import requests
3
4# Exchange auth code for tokens (PKCE flow)
5def exchange_code_for_tokens(code, code_verifier, client_id, redirect_uri):
6 resp = requests.post(
7 'https://api.x.com/2/oauth2/token',
8 data={
9 'code': code,
10 'grant_type': 'authorization_code',
11 'client_id': client_id,
12 'redirect_uri': redirect_uri,
13 'code_verifier': code_verifier
14 }
15 )
16 return resp.json() # access_token, refresh_token, expires_in: 7200
17
18# Refresh before 2-hour expiry
19def refresh_token(refresh_token_val, client_id):
20 resp = requests.post(
21 'https://api.x.com/2/oauth2/token',
22 data={
23 'grant_type': 'refresh_token',
24 'refresh_token': refresh_token_val,
25 'client_id': client_id
26 }
27 )
28 return resp.json()

Security notes

  • DM campaigns require the user's personal access token — your app token cannot send DMs on behalf of users
  • offline.access scope is required to receive a refresh token for long-running campaigns
  • Never store DM content in logs — treat DM campaign messages as sensitive data
  • Users can revoke your app's DM access at any time through X settings — check for 401 errors and re-auth
  • Pro tier credentials are separate from Basic tier credentials — ensure you are using the correct environment

Key endpoints

POST/2/dm_conversations/with/:participant_id/messages

Send a direct message to a specific user. Creates a new conversation if none exists between sender and recipient. Requires user-context OAuth 2.0 with dm.write scope.

ParameterTypeRequiredDescription
participant_idstringrequiredNumeric Twitter user ID of the recipient (not username)
textstringrequiredDM text content
attachmentsarrayoptionalArray of media_id objects (upload via POST /2/media/upload first)

Request

json
1POST https://api.x.com/2/dm_conversations/with/98765432/messages
2Authorization: Bearer USER_ACCESS_TOKEN
3Content-Type: application/json
4
5{
6 "text": "Hi {first_name}, thanks for following! Here's your exclusive early access code: EARLY2026"
7}

Response

json
1{
2 "data": {
3 "dm_conversation_id": "123456789-98765432",
4 "dm_event_id": "128341038123309056"
5 }
6}
POST/2/dm_conversations

Create a new group DM conversation. Use this for group messaging (up to 50 participants). Less common than one-to-one DMs for outreach campaigns.

ParameterTypeRequiredDescription
conversation_typestringrequiredMust be 'Group' for group conversations
participant_idsarrayrequiredArray of numeric user IDs
messageobjectrequiredMessage object with text field

Request

json
1POST https://api.x.com/2/dm_conversations
2Authorization: Bearer USER_ACCESS_TOKEN
3Content-Type: application/json
4
5{
6 "conversation_type": "Group",
7 "participant_ids": ["98765432", "11223344"],
8 "message": {
9 "text": "Welcome to our exclusive group!"
10 }
11}

Response

json
1{
2 "data": {
3 "dm_conversation_id": "GroupConversation123",
4 "dm_event_id": "128341038123309057"
5 }
6}
GET/2/dm_conversations/with/:participant_id/dm_events

Retrieve DM events for a conversation with a specific user. On Basic tier, this endpoint is effectively limited to 1 request per 24 hours despite documentation stating 75,000/month. This is the endpoint affected by the August 2025 bug.

ParameterTypeRequiredDescription
participant_idstringrequiredNumeric Twitter user ID
dm_event.fieldsstringoptionalFields: id,text,created_at,sender_id,dm_conversation_id
max_resultsintegeroptional1–100 events per page

Request

json
1GET https://api.x.com/2/dm_conversations/with/98765432/dm_events?dm_event.fields=id%2Ctext%2Ccreated_at%2Csender_id&max_results=10
2Authorization: Bearer USER_ACCESS_TOKEN

Response

json
1{
2 "data": [
3 {
4 "id": "128341038123309056",
5 "text": "Hi, thanks for following!",
6 "created_at": "2026-04-20T10:15:30.000Z",
7 "sender_id": "123456789"
8 }
9 ]
10}

Step-by-step automation

1

Understand tier limitations before building

Why: The single most important thing to know before building a DM campaign tool: Basic tier ($200/month) is effectively broken for DM reads as of August 2025. The dm_conversations/with/:id/dm_events endpoint returns only 1 response per 24 hours despite X's documentation claiming 75,000/month. This bug has been reported extensively in the developer community but remains unresolved. DM sends (POST) may work at higher volume than reads, but testing is inconsistent.

Verify your tier's actual DM rate limits before writing campaign code. Send a single test DM, then try to read the conversation thread. If you are on Basic and hitting the 1 req/24h limit, you will need to either upgrade to Pro ($5,000/month) or redesign your campaign to not rely on DM read operations.

curl
1# Test: attempt to send a single DM
2curl -s -X POST 'https://api.x.com/2/dm_conversations/with/TEST_USER_ID/messages' \
3 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
4 -H 'Content-Type: application/json' \
5 -d '{"text": "Test DM from API"}'
6
7# Check rate limit headers on the response
8curl -v -X GET 'https://api.x.com/2/dm_conversations/with/TEST_USER_ID/dm_events?max_results=1' \
9 -H 'Authorization: Bearer USER_ACCESS_TOKEN' 2>&1 | grep -i 'x-rate-limit'

Pro tip: Check developer.x.com/discussions for 'DM rate limit Basic 2025' to see current status of the bug fix. As of May 2026, it remains unresolved.

Expected result: You see the actual rate limits on your tier. If you hit 429 on the first DM read on Basic, the bug is confirmed and you should design around it or upgrade.

2

Build the campaign recipient list from followers or mentions

Why: DM campaigns require knowing each recipient's numeric user ID. You cannot send DMs to usernames — only to user IDs. Typical sources: your followers list (GET /2/users/{id}/followers), users who mentioned you (GET /2/tweets/search/recent with mentions query), or an imported list.

Fetch user IDs from your followers or a curated list. For privacy compliance, only DM users who have opted in or interacted with your account. Store user IDs with personalization data (name, last interaction) in your campaign database. Paginate using pagination_token.

curl
1# Fetch your followers' user IDs (paginated, 1000 per page max)
2curl -s 'https://api.x.com/2/users/YOUR_USER_ID/followers?user.fields=id%2Cname%2Cusername&max_results=1000' \
3 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
4
5# Get next page using pagination_token
6curl -s 'https://api.x.com/2/users/YOUR_USER_ID/followers?max_results=1000&pagination_token=TOKEN' \
7 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Pro tip: Reading 1,000 followers costs 1 API call on Basic (15,000 reads/month total budget). With 500 DMs/day platform cap and $0.005/read cost on pay-per-use, plan your reads carefully.

Expected result: A list of follower objects with id, name, username fields. These IDs are used as participant_id in the DM send endpoint.

3

Send personalized DMs with rate limit compliance

Why: DM sends must respect both API rate limits and the 500 DM/day platform cap. On Pro tier, the rate limit is 15 requests per 15 minutes (1 per minute). Build in at least 65-second delays between DMs to stay safely under. Personalize each message to avoid appearing as spam.

For each recipient in your campaign list, substitute their name and any other personalization fields into the message template. Send via POST /2/dm_conversations/with/:id/messages. Log each DM with timestamp, recipient ID, and success/failure status. Respect a 65-second minimum delay between sends on Pro tier.

curl
1# Send a personalized DM
2curl -s -X POST 'https://api.x.com/2/dm_conversations/with/98765432/messages' \
3 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
4 -H 'Content-Type: application/json' \
5 -d '{
6 "text": "Hi Jane! Thanks for following @yourbrand. As one of our early supporters, here is your exclusive discount code: EARLYBIRD20"
7 }'

Pro tip: On Pro tier, the 15 req/15 min rate limit means you can send at most 500 DMs in about 9 hours. At 65-second intervals, you hit the 500 platform cap in exactly 9.02 hours — perfect alignment.

Expected result: DMs sent to each recipient at a compliant rate. Log file records every attempt. Campaign stops automatically at the 500/day platform cap.

4

Handle campaign failures and monitor deliverability

Why: DMs can fail for multiple reasons: recipient has restricted DMs to followers-only, account is suspended, or your account is rate-limited. Each failure type requires a different response. Monitoring deliverability (sent vs. delivered) helps optimize future campaigns.

After sending, check the response for error codes. A 403 typically means the recipient's DM settings don't allow messages from non-followers. A 404 means the user account doesn't exist or was suspended. Track these as permanent failures and remove from future campaigns.

curl
1# Check what errors look like for restricted DMs
2# (Run this against an account that has DMs restricted)
3curl -v -X POST 'https://api.x.com/2/dm_conversations/with/RESTRICTED_USER_ID/messages' \
4 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
5 -H 'Content-Type: application/json' \
6 -d '{"text": "Test"}' 2>&1 | grep -E '< HTTP|{'

Pro tip: Track your deliverability rate (successful sends / attempted sends). A rate below 60% suggests your recipient list has too many accounts with restricted DMs — consider refining targeting to engaged users only.

Expected result: Each DM attempt returns a structured result with success status and error classification. Permanent failures (DM restricted, suspended) are logged and excluded from future campaigns.

Complete working code

Complete DM campaign runner that fetches recipients, sends personalized messages with rate limiting, logs results, and handles errors. Designed for Pro tier ($5,000/month) with 65-second delays between sends.

Error handling

Cause

On Basic tier ($200/month), GET /2/dm_conversations/with/:id/dm_events returns 429 after 1 request per 24 hours. This is a documented bug since August 2025, not a genuine rate limit. DM sends (POST) may work at higher volume but are inconsistent on Basic.

Fix

Cause

The recipient has their DM settings configured to allow messages only from followers, or has blocked your account.

Fix

Cause

The recipient's account has been suspended, deactivated, or the user ID is incorrect.

Fix

Cause

OAuth 2.0 access token expired (2-hour lifetime).

Fix

Cause

Exceeded the 500 DM per day platform cap. All tiers share this limit.

Fix

Rate limits & throttling

ScopeLimitWindow

Security checklist

  • Store OAuth 2.0 access and refresh tokens in environment variables or encrypted secrets
  • Request offline.access scope to enable refresh tokens for long-running campaigns
  • Never log DM content — log only recipient IDs, timestamps, and success/failure status
  • Maintain a sent-DMs log to prevent sending duplicate messages to the same user
  • Check for 401 (token revoked) on every DM send and handle re-authorization gracefully
  • Implement a daily DM cap counter with automatic shutdown at 490 (safety buffer before 500)
  • Do not send DMs to users who have not opted in or engaged with your account — X can suspend accounts for unsolicited DM spam
  • Document your DM campaign in your app settings as required by X Developer Policy

Automation use cases

Best practices

  • Test your tier's actual DM rate limits before building — Basic tier has a documented bug that makes bulk DM automation practically impossible without Pro
  • Keep DM content genuinely useful and personalized — X can suspend accounts for DM spam, and users can report unsolicited messages
  • Build your campaign to send at most 490 DMs per day, leaving a 10-DM buffer before the 500 platform cap
  • Refresh your recipient list before each campaign run — follower lists change, and sending to deactivated accounts wastes budget and raises error rates
  • Personalize at minimum with the recipient's name — identical messages to hundreds of users is the primary trigger for spam detection
  • Add a campaign opt-out mechanism — include instructions for users to DM back 'STOP' and maintain a suppression list

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building an X (Twitter) DM campaign tool using the X API v2. I need to: 1) Fetch my follower list using GET /2/users/{id}/followers, 2) Send personalized DMs via POST /2/dm_conversations/with/:participant_id/messages, 3) Respect the 500 DMs/day platform cap and 65-second delays between sends for Pro tier, 4) Log each sent DM with recipient ID and timestamp to avoid duplicates, 5) Handle 403 (DM restricted), 404 (user not found), and 429 (rate limited) errors. Write this in Python.

Lovable / V0 Prompt

Build a DM campaign management dashboard for X (Twitter). It should show: campaign progress (sent/total/failed), a list of recipients with status badges, daily DM count vs 500 cap, estimated campaign completion time, and a pause/resume button. Store campaign data in Supabase. Include a recipient import via CSV.

Frequently asked questions

Can I send bulk DMs on the Basic tier ($200/month)?

In practice, no. As of August 2025, the dm_conversations/with/:id/dm_events endpoint is effectively limited to 1 request per 24 hours on Basic tier despite X documenting 75,000/month. This bug remains unresolved as of May 2026. DM sends (POST) may work at higher volume but have inconsistent behavior. For reliable bulk DM automation, Pro tier ($5,000/month) is the only practical option.

What is the maximum number of DMs I can send per day?

500 DMs per day per account — this is an account-level platform cap that applies to all tiers including Pro and Enterprise. This cannot be increased. To reach more users, you would need multiple X accounts, each with its own campaign. X Premium accounts (users, not developers) have a 10x higher cap, but that applies to the account itself, not the API.

What happens if a user has DMs restricted to followers only?

The API returns a 403 Forbidden response. You cannot override this setting — the user must change their own DM privacy settings. Mark these users as 'dm_restricted' in your campaign log and do not retry. A common workaround is to first follow the user and wait for a follow-back before attempting to DM them, but this significantly slows campaign execution.

How do I get the numeric user ID for a recipient when I only know their username?

Use GET /2/users/by/username/:username to look up a user ID from their username. This costs $0.005 per read on pay-per-use. For campaigns targeting your own followers, use GET /2/users/{id}/followers to get both user IDs and usernames simultaneously, avoiding the lookup step.

Will X suspend my account for sending automated DMs?

Potentially yes, if the DMs are unsolicited, repetitive, or promotional in nature without clear user opt-in. X's platform policy prohibits using the API for bulk unsolicited DMs. Best practices: only DM engaged users (followers who have interacted with your content), personalize each message, include an opt-out instruction, and keep volume under 100 DMs/day initially to build account trust.

Can RapidDev help me build a compliant DM campaign infrastructure?

Yes. RapidDev can guide you through building a DM campaign backend with proper rate limiting, recipient management, and opt-out tracking using Supabase and Next.js. We also have resources on X API tier selection to help you decide whether pay-per-use or Pro tier makes sense for your expected campaign volume.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.