Skip to main content

Overview

The Inbox API enforces rate limits to ensure fair usage and platform stability. This guide covers the limits, how to detect them, and strategies for handling them gracefully.

Current limits

Rate limits are global per team — all endpoints share the same limits, and all API tokens for a team share the same quota.
WindowLimit
Per minute300 requests
Per hour10,000 requests
Per day100,000 requests
All three windows are enforced simultaneously using a sliding window. If any window is exceeded, requests are rejected until that window resets.
Limits are per team, not per token. Multiple API tokens for the same team share the same rate limit quota.

Rate limit headers

Every response includes rate limit information:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 295
X-RateLimit-Reset: 1705312800
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets

Rate limit responses

When you exceed the limit: Status: 429 Too Many Requests
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Try again in 60 seconds.",
    "status": 429
  }
}
Headers:
Retry-After: 60

Exponential backoff

The recommended pattern for handling rate limits:
import axios, { AxiosError } from 'axios';

async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxRetries?: number;
    baseDelay?: number;
    maxDelay?: number;
  } = {}
): Promise<T> {
  const {
    maxRetries = 5,
    baseDelay = 1000,
    maxDelay = 60000
  } = options;

  let attempt = 0;

  while (true) {
    try {
      return await fn();
    } catch (error) {
      if (!axios.isAxiosError(error)) throw error;

      const status = error.response?.status;

      // Only retry on rate limits and server errors
      if (status !== 429 && (status < 500 || status >= 600)) {
        throw error;
      }

      attempt++;
      if (attempt >= maxRetries) {
        throw new Error(`Max retries (${maxRetries}) exceeded`);
      }

      // Calculate delay
      let delay: number;
      if (status === 429) {
        // Use Retry-After header if available
        const retryAfter = error.response?.headers['retry-after'];
        delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : baseDelay * Math.pow(2, attempt);
      } else {
        // Exponential backoff for server errors
        delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
      }

      console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const thread = await withRetry(() => client.get(`/threads/${threadId}`));

Bulk operations

For operations that process many items, implement rate-aware batching:
async function bulkSendMessages(
  threads: Array<{ id: string; message: string }>
) {
  const results = [];

  for (const thread of threads) {
    try {
      const { data } = await withRetry(() =>
        client.post(`/threads/${thread.id}/messages`, {
          content: thread.message
        })
      );

      results.push({
        threadId: thread.id,
        success: true,
        messageId: data.id
      });

    } catch (error) {
      results.push({
        threadId: thread.id,
        success: false,
        error: error.message
      });
    }

    // Small delay between sends to stay well within 300/min
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  return results;
}

Proactive rate limit tracking

Track your usage to avoid hitting limits:
class RateLimitTracker {
  private remaining: number;
  private resetTime: number;

  constructor() {
    this.remaining = 300;
    this.resetTime = Date.now() + 60000;
  }

  update(headers: Record<string, string>) {
    this.remaining = parseInt(headers['x-ratelimit-remaining'] || '300', 10);
    this.resetTime = parseInt(headers['x-ratelimit-reset'] || '0', 10) * 1000;
  }

  async waitIfNeeded() {
    if (this.remaining <= 5) {
      const waitTime = Math.max(0, this.resetTime - Date.now());
      if (waitTime > 0) {
        console.log(`Approaching rate limit. Waiting ${waitTime}ms`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }
  }

  canMakeRequest(): boolean {
    return this.remaining > 0 || Date.now() > this.resetTime;
  }
}

// Usage with axios interceptor
const tracker = new RateLimitTracker();

client.interceptors.response.use(
  response => {
    tracker.update(response.headers);
    return response;
  },
  error => {
    if (error.response?.headers) {
      tracker.update(error.response.headers);
    }
    return Promise.reject(error);
  }
);

// Before each request
async function makeRequest(fn: () => Promise<any>) {
  await tracker.waitIfNeeded();
  return fn();
}

Pagination with rate limits

When paginating through large datasets:
async function getAllThreadsWithRateLimits() {
  const allThreads = [];
  let cursorId: string | undefined;
  let cursorTimestamp: string | undefined;
  let requestCount = 0;

  do {
    const { data, headers } = await client.get('/threads', {
      params: {
        limit: 100,
        ...(cursorId && { cursorId, cursorTimestamp })
      }
    });

    allThreads.push(...data.threads);
    cursorId = data.nextCursor?.id;
    cursorTimestamp = data.nextCursor?.timestamp;
    requestCount++;

    // Check remaining requests
    const remaining = parseInt(headers['x-ratelimit-remaining'] || '300', 10);

    if (remaining < 10 && cursorId) {
      const resetTime = parseInt(headers['x-ratelimit-reset'] || '0', 10) * 1000;
      const waitTime = Math.max(0, resetTime - Date.now());
      console.log(`Rate limit approaching. Waiting ${waitTime}ms`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }

  } while (cursorId);

  return allThreads;
}

Best practices

Cache read results to avoid repeated requests:
const cache = new Map<string, { data: any; expiry: number }>();

async function getCachedThread(threadId: string) {
  const cached = cache.get(threadId);
  if (cached && cached.expiry > Date.now()) {
    return cached.data;
  }

  const { data: thread } = await client.get(`/threads/${threadId}`);
  cache.set(threadId, {
    data: thread,
    expiry: Date.now() + 60000  // 1 minute cache
  });

  return thread;
}
Track X-RateLimit-Remaining and pause before hitting zero.
Stop making requests temporarily when consistently hitting limits.

Platform considerations

Sending too many messages too quickly may trigger platform-level restrictions on your X account, separate from API rate limits. Space out messages appropriately, especially for outreach.