Building a Reddit integration that handles private messages requires understanding Reddit's unique approach to messaging. The reddit api messages system uses a combination of inbox endpoints, fullname identifiers, and OAuth scopes that differ from typical messaging APIs. This guide walks you through everything you need to implement a complete Reddit DM integration.
Reddit's messaging system handles both private messages between users and comment replies in a unified inbox. Your application needs to distinguish between these types, manage conversation threads, and handle Reddit's rate limits appropriately.
Introduction to Reddit API
Reddit provides a comprehensive API for accessing user data, posting content, and managing private messages. The API uses OAuth 2.0 for authentication and requires specific scopes for different operations.
For messaging functionality, you'll work with these primary endpoints:
| Endpoint | Purpose | Method |
|---|---|---|
/message/inbox | All inbox items (messages + comment replies) | GET |
/message/messages | Private messages only | GET |
/message/sent | Sent messages | GET |
/api/compose | Send new message | POST |
/api/comment | Reply to message | POST |
/api/read_message | Mark as read | POST |
The reddit inbox api returns messages in a listing format with pagination support. Each message includes metadata about the sender, recipient, subject, and conversation thread.
OAuth 2.0 Authentication for Reddit
Before accessing the reddit private messages api, you need to authenticate users with the correct OAuth scopes. The privatemessages scope grants access to read and send direct messages.
interface RedditAuthConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
scopes: string[];
}
const config: RedditAuthConfig = {
clientId: process.env.REDDIT_CLIENT_ID || '',
clientSecret: process.env.REDDIT_CLIENT_SECRET || '',
redirectUri: process.env.REDDIT_REDIRECT_URI || '',
scopes: ['identity', 'privatemessages', 'read', 'history']
};
function getAuthUrl(state: string): string {
const params = new URLSearchParams({
client_id: config.clientId,
response_type: 'code',
state: state,
redirect_uri: config.redirectUri,
duration: 'permanent',
scope: config.scopes.join(' '),
});
return `https://www.reddit.com/api/v1/authorize?${params.toString()}`;
}
async function exchangeCodeForToken(code: string): Promise<TokenResponse> {
const credentials = Buffer.from(
`${config.clientId}:${config.clientSecret}`
).toString('base64');
const response = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'YourApp/1.0.0',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
}).toString(),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${response.status} ${error}`);
}
return response.json();
}
interface TokenResponse {
access_token: string;
refresh_token: string;
expires_in: number;
scope: string;
token_type: string;
}
Note: Reddit requires a descriptive User-Agent header for all API requests. Using a generic or missing User-Agent will result in rate limiting or blocked requests.
Understanding Fullnames (t1_, t3_, t4_)
Reddit uses a "fullname" system to identify all objects in the API. Understanding these prefixes is critical when working with the reddit dm api:
| Prefix | Object Type | Example |
|---|---|---|
t1_ | Comment | t1_abc123 |
t2_ | Account | t2_xyz789 |
t3_ | Link/Post | t3_def456 |
t4_ | Message | t4_ghi012 |
t5_ | Subreddit | t5_jkl345 |
When replying to messages or marking them as read, you must use the complete fullname including the prefix:
function normalizeFullname(id: string, type: 't1' | 't3' | 't4'): string {
if (id.startsWith(`${type}_`)) {
return id;
}
return `${type}_${id}`;
}
// Usage examples
const messageFullname = normalizeFullname('abc123', 't4'); // Returns: t4_abc123
const commentFullname = normalizeFullname('t1_xyz789', 't1'); // Returns: t1_xyz789
Fetching Inbox Messages
The inbox endpoint returns all items including private messages and comment replies. To fetch only private messages, use the /message/messages endpoint instead.
interface RedditMessage {
id: string;
name: string; // Fullname (t4_xxx)
author: string;
dest: string;
subject: string;
body: string;
created_utc: number;
new: boolean;
was_comment: boolean;
first_message_name: string | null;
}
interface InboxResponse {
messages: RedditMessage[];
after: string | null;
}
async function fetchInboxMessages(
accessToken: string,
options: { limit?: number; after?: string } = {}
): Promise<InboxResponse> {
const limit = Math.min(options.limit || 25, 100);
let url = `https://oauth.reddit.com/message/messages?limit=${limit}&raw_json=1`;
if (options.after) {
url += `&after=${options.after}`;
}
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
},
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to fetch inbox: ${response.status} ${error}`);
}
const data = await response.json();
const messages: RedditMessage[] = (data.data?.children || [])
.filter((item: any) => !item.data.was_comment) // Filter out comment replies
.map((item: any) => ({
id: item.data.id,
name: item.data.name,
author: item.data.author,
dest: item.data.dest,
subject: item.data.subject,
body: item.data.body,
created_utc: item.data.created_utc,
new: item.data.new,
was_comment: item.data.was_comment,
first_message_name: item.data.first_message_name,
}));
return {
messages,
after: data.data?.after || null,
};
}
The first_message_name field is important for threading. Messages in the same conversation share this value, allowing you to group them together.
Sending Private Messages with the Reddit API
To send a new private message, use the /api/compose endpoint. This endpoint requires the recipient's username, a subject line, and the message body.
interface SendMessageParams {
to: string;
subject: string;
text: string;
}
interface SendMessageResult {
success: boolean;
errors?: string[];
}
async function sendPrivateMessage(
accessToken: string,
params: SendMessageParams
): Promise<SendMessageResult> {
const { to, subject, text } = params;
// Validate inputs
if (!to || !subject || !text) {
return {
success: false,
errors: ['Recipient, subject, and message text are required'],
};
}
if (subject.length > 100) {
return {
success: false,
errors: ['Subject must be 100 characters or less'],
};
}
const response = await fetch('https://oauth.reddit.com/api/compose', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
api_type: 'json',
to,
subject,
text,
}).toString(),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to send message: ${response.status} ${error}`);
}
const data = await response.json();
const errors = data.json?.errors || [];
if (errors.length > 0) {
return {
success: false,
errors: errors.map((e: any[]) => e.join(': ')),
};
}
return { success: true };
}
Note: Reddit limits new accounts from sending private messages. Users may need to verify their email and have some account age before the messaging feature becomes available.
Replying to Messages
Replying to an existing message uses the /api/comment endpoint (the same endpoint used for comment replies). You need the fullname of the message you're replying to.
interface ReplyResult {
success: boolean;
replyId?: string;
errors?: string[];
}
async function replyToMessage(
accessToken: string,
messageFullname: string,
text: string
): Promise<ReplyResult> {
// Ensure proper fullname format
const fullname = messageFullname.startsWith('t4_')
? messageFullname
: `t4_${messageFullname}`;
const response = await fetch('https://oauth.reddit.com/api/comment', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
api_type: 'json',
thing_id: fullname,
text,
}).toString(),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to reply: ${response.status} ${error}`);
}
const data = await response.json();
const errors = data.json?.errors || [];
if (errors.length > 0) {
return {
success: false,
errors: errors.map((e: any[]) => e.join(': ')),
};
}
const replyData = data.json?.data?.things?.[0]?.data;
return {
success: true,
replyId: replyData?.id,
};
}
Message Threading and Grouping
Reddit groups messages into conversations using the first_message_name field. Building a conversation view requires fetching messages from both inbox and sent folders, then grouping them by thread.
interface Conversation {
id: string;
participantUsername: string;
subject: string;
messages: ThreadMessage[];
lastUpdated: Date;
hasUnread: boolean;
}
interface ThreadMessage {
id: string;
fullname: string;
text: string;
sentAt: Date;
isFromMe: boolean;
senderUsername: string;
}
async function getConversations(
accessToken: string,
myUsername: string
): Promise<Conversation[]> {
// Fetch both inbox and sent messages
const [inboxResponse, sentResponse] = await Promise.all([
fetch('https://oauth.reddit.com/message/messages?limit=100&raw_json=1', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
},
}),
fetch('https://oauth.reddit.com/message/sent?limit=100&raw_json=1', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
},
}),
]);
const inboxData = await inboxResponse.json();
const sentData = await sentResponse.json();
const allMessages: any[] = [
...(inboxData.data?.children || []),
...(sentData.data?.children || []),
];
// Group by conversation
const conversationsMap = new Map<string, Conversation>();
const seenMessageIds = new Set<string>();
for (const item of allMessages) {
const msg = item.data;
// Skip duplicates and comment replies
if (seenMessageIds.has(msg.name) || msg.was_comment) {
continue;
}
seenMessageIds.add(msg.name);
const conversationId = msg.first_message_name || msg.name;
const isFromMe = msg.author?.toLowerCase() === myUsername.toLowerCase();
const otherParticipant = isFromMe ? msg.dest : msg.author;
const threadMessage: ThreadMessage = {
id: msg.id,
fullname: msg.name,
text: msg.body,
sentAt: new Date(msg.created_utc * 1000),
isFromMe,
senderUsername: msg.author,
};
if (!conversationsMap.has(conversationId)) {
conversationsMap.set(conversationId, {
id: conversationId,
participantUsername: otherParticipant,
subject: msg.subject,
messages: [threadMessage],
lastUpdated: threadMessage.sentAt,
hasUnread: msg.new && !isFromMe,
});
} else {
const conversation = conversationsMap.get(conversationId)!;
conversation.messages.push(threadMessage);
if (threadMessage.sentAt > conversation.lastUpdated) {
conversation.lastUpdated = threadMessage.sentAt;
}
if (msg.new && !isFromMe) {
conversation.hasUnread = true;
}
}
}
// Sort messages within each conversation and sort conversations by last update
const conversations = Array.from(conversationsMap.values());
for (const conv of conversations) {
conv.messages.sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime());
}
conversations.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime());
return conversations;
}
Marking Messages as Read
The /api/read_message endpoint accepts a comma-separated list of fullnames, allowing you to mark multiple messages as read in a single request.
async function markMessagesAsRead(
accessToken: string,
messageFullnames: string[]
): Promise<{ success: boolean }> {
if (messageFullnames.length === 0) {
return { success: true };
}
// Ensure all fullnames have the t4_ prefix
const normalizedFullnames = messageFullnames.map(fn =>
fn.startsWith('t4_') ? fn : `t4_${fn}`
);
const response = await fetch('https://oauth.reddit.com/api/read_message', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
id: normalizedFullnames.join(','),
}).toString(),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to mark messages as read: ${response.status} ${error}`);
}
return { success: true };
}
// Mark all messages in a conversation as read
async function markConversationAsRead(
accessToken: string,
conversation: Conversation
): Promise<void> {
const unreadFullnames = conversation.messages
.filter(msg => !msg.isFromMe)
.map(msg => msg.fullname);
if (unreadFullnames.length > 0) {
await markMessagesAsRead(accessToken, unreadFullnames);
}
}
Rate Limits and API Rules
Reddit enforces strict rate limits that your application must respect. The API allows approximately 60 requests per minute for OAuth-authenticated requests.
| Limit Type | Value | Scope |
|---|---|---|
| Requests per minute | 60 | Per OAuth token |
| Burst limit | 10 requests | Per second |
| Message sending | Varies | Account age dependent |
Here's a rate limiter implementation for Reddit API calls:
class RedditRateLimiter {
private requestTimestamps: number[] = [];
private readonly maxRequests = 60;
private readonly windowMs = 60000; // 1 minute
async throttle(): Promise<void> {
const now = Date.now();
// Remove timestamps outside the window
this.requestTimestamps = this.requestTimestamps.filter(
ts => now - ts < this.windowMs
);
if (this.requestTimestamps.length >= this.maxRequests) {
const oldestTimestamp = this.requestTimestamps[0];
const waitTime = this.windowMs - (now - oldestTimestamp);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
this.requestTimestamps.push(Date.now());
}
}
const rateLimiter = new RedditRateLimiter();
async function makeRedditRequest(
url: string,
options: RequestInit,
accessToken: string
): Promise<any> {
await rateLimiter.throttle();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`,
'User-Agent': 'YourApp/1.0.0',
},
});
// Check for rate limit headers
const remaining = response.headers.get('x-ratelimit-remaining');
const reset = response.headers.get('x-ratelimit-reset');
if (remaining && parseInt(remaining) < 5) {
console.warn(`Reddit rate limit low: ${remaining} requests remaining`);
}
if (!response.ok) {
const error = await response.text();
if (response.status === 429) {
// Rate limited, wait and retry
const retryAfter = parseInt(reset || '60');
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return makeRedditRequest(url, options, accessToken);
}
throw new Error(`Reddit API error: ${response.status} ${error}`);
}
return response.json();
}
Note: Reddit may impose additional limits on new accounts or accounts with low karma. If your users experience messaging restrictions, advise them to verify their email and participate in the community.
Using Late for Reddit Integration
Building and maintaining Reddit API integrations involves handling OAuth flows, managing rate limits, normalizing data formats, and keeping up with API changes. Late provides a unified API that simplifies all of this.
With Late, you can integrate Reddit messaging alongside other platforms using a consistent interface:
import { Late } from '@late/sdk';
const late = new Late({ apiKey: process.env.LATE_API_KEY });
// Fetch conversations across all connected platforms
const conversations = await late.messages.listConversations({
platform: 'reddit',
accountId: 'your-reddit-account-id',
});
// Send a message using Late's unified API
await late.messages.send({
platform: 'reddit',
accountId: 'your-reddit-account-id',
to: 'recipient_username',
subject: 'Hello from Late',
text: 'This message was sent through Late\'s unified API',
});
// Mark messages as read
await late.messages.markAsRead({
platform: 'reddit',
accountId: 'your-reddit-account-id',
messageIds: ['t4_abc123', 't4_def456'],
});
Late handles the complexity of Reddit's fullname system, rate limiting, and token refresh automatically. You get a clean, predictable API that works the same way across Reddit, Twitter, LinkedIn, and other platforms.
Why Choose Late for Reddit Integration
Unified Data Format: Late normalizes Reddit's message format to match other platforms, so you write one integration that works everywhere.
Automatic Token Management: Late handles OAuth token refresh and expiration, eliminating authentication failures in production.
Built-in Rate Limiting: Late's infrastructure manages rate limits across all your users, preventing API blocks.
Error Handling: Late provides consistent error types and messages, making debugging straightforward.
Ready to simplify your Reddit integration? Check out Late's documentation to get started, or explore the API reference for detailed endpoint information.
[INTERNAL_LINK:social-media-api-comparison] [INTERNAL_LINK:oauth-best-practices]

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic