Rate Limits
The Sent API v3 implements rate limiting to ensure platform stability and fair access for all users. Rate limits are applied per customer account based on the API key used.
Rate Limit Tiers
Standard Endpoints
Most API endpoints have the following limits:
| Tier | Requests per Minute | Burst Capacity |
|---|---|---|
| Standard | 200 | 50 |
Standard endpoints include:
- Contacts (GET, POST, PATCH, DELETE)
- Messages (GET, POST)
- Templates (GET, POST, PUT, DELETE)
- Profiles (GET, POST, PATCH, DELETE)
- Brands and Campaigns (GET, POST, PUT, DELETE)
Sensitive Endpoints
Certain endpoints that perform sensitive operations have stricter limits:
| Tier | Requests per Minute | Burst Capacity |
|---|---|---|
| Sensitive | 10 | 5 |
Sensitive endpoints include:
- Webhook secret rotation (
POST /v3/webhooks/{id}/rotate-secret) - User invitation (
POST /v3/users) - Profile completion (
POST /v3/profiles/{profileId}/complete)
Message Sending
Message sending endpoints have specific limits based on your account tier:
| Tier | Messages per Minute | Notes |
|---|---|---|
| Starter | 60 | For new accounts and testing |
| Growth | 300 | Standard production limits |
| Enterprise | Custom | Contact support for higher limits |
Rate Limit Headers
All API responses include rate limit headers. When you exceed a rate limit, the 429 Too Many Requests response includes additional headers:
Standard Response Headers
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 150
X-RateLimit-Reset: 1705312800Rate Limit Exceeded Response (429)
Retry-After: 60
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800Header Reference
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window | 200 |
X-RateLimit-Remaining | Requests remaining in current window | 150 |
X-RateLimit-Reset | Unix timestamp when the window resets | 1705312800 |
Retry-After | Seconds until you can retry (only on 429) | 60 |
Handling Rate Limits
429 Response Body
When rate limited, the API returns:
{
"success": false,
"error": {
"code": "BUSINESS_002",
"message": "Rate limit exceeded",
"doc_url": "https://docs.sent.dm/errors/BUSINESS_002"
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"version": "v3"
}
}Best Practices
Implement Exponential Backoff
async function makeRequestWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
if (attempt === maxRetries) {
throw new Error('Max retries exceeded');
}
// Get retry delay from header or use exponential backoff
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Retrying after ${delay}ms...`);
await sleep(delay);
}
throw new Error('Unreachable');
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}import time
import requests
from typing import Optional
def make_request_with_retry(
method: str,
url: str,
headers: dict,
json_data: Optional[dict] = None,
max_retries: int = 3
) -> requests.Response:
"""Make request with exponential backoff on 429."""
for attempt in range(max_retries + 1):
response = requests.request(
method, url, headers=headers, json=json_data
)
if response.status_code != 429:
return response
if attempt == max_retries:
raise Exception('Max retries exceeded')
# Get retry delay from header or use exponential backoff
retry_after = response.headers.get('Retry-After')
delay = int(retry_after) if retry_after else (2 ** attempt)
print(f'Rate limited. Retrying after {delay}s...')
time.sleep(delay)
raise Exception('Unreachable')package main
import (
"fmt"
"math"
"net/http"
"strconv"
"time"
)
func makeRequestWithRetry(
req *http.Request,
maxRetries int,
) (*http.Response, error) {
client := &http.Client{}
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 429 {
return resp, nil
}
if attempt == maxRetries {
return nil, fmt.Errorf("max retries exceeded")
}
// Get retry delay from header or use exponential backoff
retryAfter := resp.Header.Get("Retry-After")
delay := math.Pow(2, float64(attempt))
if retryAfter != "" {
if seconds, err := strconv.Atoi(retryAfter); err == nil {
delay = float64(seconds)
}
}
fmt.Printf("Rate limited. Retrying after %.0fs...\n", delay)
time.Sleep(time.Duration(delay) * time.Second)
}
return nil, fmt.Errorf("unreachable")
}Monitor Rate Limit Headers
interface RateLimitStatus {
limit: number;
remaining: number;
reset: Date;
}
async function makeRequest(url: string, options: RequestInit): Promise<Response> {
const response = await fetch(url, options);
// Log rate limit status for monitoring
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (limit && remaining && reset) {
console.log(`Rate limit: ${remaining}/${limit}, resets at ${new Date(parseInt(reset) * 1000)}`);
// Alert when running low
if (parseInt(remaining) < 20) {
console.warn('Rate limit running low!');
}
// Store for metrics
trackRateLimit({
limit: parseInt(limit),
remaining: parseInt(remaining),
reset: new Date(parseInt(reset) * 1000)
});
}
return response;
}
function trackRateLimit(status: RateLimitStatus): void {
// Send to your monitoring system
// metrics.gauge('api.rate_limit.remaining', status.remaining);
}import time
from dataclasses import dataclass
from typing import Optional
import requests
@dataclass
class RateLimitStatus:
limit: int
remaining: int
reset: int # Unix timestamp
def make_request(url: str, headers: dict) -> requests.Response:
"""Make request and track rate limit headers."""
response = requests.get(url, headers=headers)
# Log rate limit status for monitoring
limit = response.headers.get('X-RateLimit-Limit')
remaining = response.headers.get('X-RateLimit-Remaining')
reset = response.headers.get('X-RateLimit-Reset')
if limit and remaining and reset:
reset_time = time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(int(reset)))
print(f'Rate limit: {remaining}/{limit}, resets at {reset_time}')
# Alert when running low
if int(remaining) < 20:
print('WARNING: Rate limit running low!')
# Store for metrics
track_rate_limit(RateLimitStatus(
limit=int(limit),
remaining=int(remaining),
reset=int(reset)
))
return response
def track_rate_limit(status: RateLimitStatus):
"""Send to your monitoring system (Datadog, Prometheus, etc)."""
# Example: statsd.gauge('api.rate_limit.remaining', status.remaining)
passpackage main
import (
"fmt"
"net/http"
"strconv"
"time"
)
type RateLimitStatus struct {
Limit int
Remaining int
Reset time.Time
}
func makeRequest(req *http.Request) (*http.Response, error) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
// Log rate limit status for monitoring
limit := resp.Header.Get("X-RateLimit-Limit")
remaining := resp.Header.Get("X-RateLimit-Remaining")
reset := resp.Header.Get("X-RateLimit-Reset")
if limit != "" && remaining != "" && reset != "" {
limitInt, _ := strconv.Atoi(limit)
remainingInt, _ := strconv.Atoi(remaining)
resetUnix, _ := strconv.ParseInt(reset, 10, 64)
resetTime := time.Unix(resetUnix, 0)
fmt.Printf("Rate limit: %d/%d, resets at %s\n",
remainingInt, limitInt, resetTime)
// Alert when running low
if remainingInt < 20 {
fmt.Println("WARNING: Rate limit running low!")
}
// Store for metrics
trackRateLimit(RateLimitStatus{
Limit: limitInt,
Remaining: remainingInt,
Reset: resetTime,
})
}
return resp, nil
}
func trackRateLimit(status RateLimitStatus) {
// Send to your monitoring system
// Example: statsd.Gauge("api.rate_limit.remaining", status.Remaining)
}Implement Request Throttling
class RateLimiter {
private minInterval: number;
private lastRequestTime: number = 0;
constructor(requestsPerSecond: number) {
this.minInterval = 1000 / requestsPerSecond;
}
async throttle(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minInterval) {
const delay = this.minInterval - timeSinceLastRequest;
await sleep(delay);
}
this.lastRequestTime = Date.now();
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Use: limit to 3 requests per second (180/min, well under the 200 limit)
const limiter = new RateLimiter(3);
async function makeThrottledRequest(url: string, options: RequestInit): Promise<Response> {
await limiter.throttle();
return fetch(url, options);
}import time
import requests
from typing import Optional
class RateLimiter:
"""Simple rate limiter using token bucket algorithm."""
def __init__(self, requests_per_second: float):
self.min_interval = 1.0 / requests_per_second
self.last_request_time: Optional[float] = None
def throttle(self):
"""Wait if necessary to maintain rate limit."""
if self.last_request_time is None:
self.last_request_time = time.time()
return
now = time.time()
time_since_last = now - self.last_request_time
if time_since_last < self.min_interval:
delay = self.min_interval - time_since_last
time.sleep(delay)
self.last_request_time = time.time()
# Use: limit to 3 requests per second (180/min, well under the 200 limit)
limiter = RateLimiter(3)
def make_throttled_request(url: str, headers: dict) -> requests.Response:
limiter.throttle()
return requests.get(url, headers=headers)package main
import (
"net/http"
"sync"
"time"
)
type RateLimiter struct {
minInterval time.Duration
lastRequestTime time.Time
mu sync.Mutex
}
func NewRateLimiter(requestsPerSecond float64) *RateLimiter {
return &RateLimiter{
minInterval: time.Duration(float64(time.Second) / requestsPerSecond),
}
}
func (r *RateLimiter) Throttle() {
r.mu.Lock()
defer r.mu.Unlock()
if r.lastRequestTime.IsZero() {
r.lastRequestTime = time.Now()
return
}
timeSinceLast := time.Since(r.lastRequestTime)
if timeSinceLast < r.minInterval {
time.Sleep(r.minInterval - timeSinceLast)
}
r.lastRequestTime = time.Now()
}
// Use: limit to 3 requests per second (180/min, well under the 200 limit)
var limiter = NewRateLimiter(3)
func makeThrottledRequest(req *http.Request) (*http.Response, error) {
limiter.Throttle()
client := &http.Client{}
return client.Do(req)
}Use Request Batching
When possible, batch operations to reduce API calls:
// ❌ Inefficient: Individual requests (100 API calls)
for (const contact of contacts) {
await createContact(contact);
}
// ✅ Efficient: Client-side caching to reduce duplicates
class CachedContactClient {
private cache = new Map<string, Contact>();
async getContact(id: string): Promise<Contact> {
// Return cached result if available
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
const contact = await fetchContact(id);
this.cache.set(id, contact);
return contact;
}
async batchProcessContacts(
contacts: Contact[],
batchSize: number = 10
): Promise<void> {
// Process in batches with rate limiting
for (let i = 0; i < contacts.length; i += batchSize) {
const batch = contacts.slice(i, i + batchSize);
// Process batch concurrently
await Promise.all(
batch.map(contact => this.processContact(contact))
);
// Wait between batches
if (i + batchSize < contacts.length) {
await sleep(1000);
}
}
}
private async processContact(contact: Contact): Promise<void> {
// Implementation
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}from typing import Dict, List, Optional
import time
import requests
# ❌ Inefficient: Individual requests (100 API calls)
# for contact in contacts:
# create_contact(contact)
# ✅ Efficient: Client-side caching to reduce duplicates
class CachedContactClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.cache: Dict[str, dict] = {}
def get_contact(self, contact_id: str) -> dict:
"""Get contact with caching."""
# Return cached result if available
if contact_id in self.cache:
return self.cache[contact_id]
response = requests.get(
f'https://api.sent.dm/v3/contacts/{contact_id}',
headers={'x-api-key': self.api_key}
)
contact = response.json()['data']
self.cache[contact_id] = contact
return contact
def batch_process_contacts(
self,
contacts: List[dict],
batch_size: int = 10
):
"""Process contacts in batches with rate limiting."""
for i in range(0, len(contacts), batch_size):
batch = contacts[i:i + batch_size]
# Process batch
for contact in batch:
self.process_contact(contact)
# Wait between batches (except after last batch)
if i + batch_size < len(contacts):
time.sleep(1)
def process_contact(self, contact: dict):
"""Process a single contact."""
# Implementation
passpackage main
import (
"sync"
"time"
)
// ❌ Inefficient: Individual requests (100 API calls)
// for _, contact := range contacts {
// createContact(contact)
// }
// ✅ Efficient: Client-side caching to reduce duplicates
type CachedContactClient struct {
apiKey string
cache map[string]Contact
mu sync.RWMutex
}
func NewCachedContactClient(apiKey string) *CachedContactClient {
return &CachedContactClient{
apiKey: apiKey,
cache: make(map[string]Contact),
}
}
func (c *CachedContactClient) GetContact(id string) (Contact, error) {
// Return cached result if available
c.mu.RLock()
if contact, ok := c.cache[id]; ok {
c.mu.RUnlock()
return contact, nil
}
c.mu.RUnlock()
// Fetch and cache
contact, err := fetchContact(id, c.apiKey)
if err != nil {
return Contact{}, err
}
c.mu.Lock()
c.cache[id] = contact
c.mu.Unlock()
return contact, nil
}
func (c *CachedContactClient) BatchProcessContacts(
contacts []Contact,
batchSize int,
) error {
if batchSize == 0 {
batchSize = 10
}
for i := 0; i < len(contacts); i += batchSize {
end := i + batchSize
if end > len(contacts) {
end = len(contacts)
}
batch := contacts[i:end]
// Process batch concurrently
var wg sync.WaitGroup
for _, contact := range batch {
wg.Add(1)
go func(c Contact) {
defer wg.Done()
processContact(c)
}(contact)
}
wg.Wait()
// Wait between batches
if end < len(contacts) {
time.Sleep(time.Second)
}
}
return nil
}
func processContact(contact Contact) error {
// Implementation
return nil
}
type Contact struct {
ID string
PhoneNumber string
}
func fetchContact(id, apiKey string) (Contact, error) {
// Implementation
return Contact{}, nil
}Rate Limiting by Endpoint
Contacts
| Endpoint | Method | Rate Limit |
|---|---|---|
/v3/contacts | GET | 200/min |
/v3/contacts | POST | 200/min |
/v3/contacts/{id} | GET | 200/min |
/v3/contacts/{id} | PATCH | 200/min |
/v3/contacts/{id} | DELETE | 200/min |
Messages
| Endpoint | Method | Rate Limit |
|---|---|---|
/v3/messages | POST | 60-300/min* |
/v3/messages/{id} | GET | 200/min |
/v3/messages/{id}/activities | GET | 200/min |
*Based on your account tier
Templates
| Endpoint | Method | Rate Limit |
|---|---|---|
/v3/templates | GET | 200/min |
/v3/templates | POST | 200/min |
/v3/templates/{id} | GET | 200/min |
/v3/templates/{id} | PUT | 200/min |
/v3/templates/{id} | DELETE | 200/min |
Webhooks
| Endpoint | Method | Rate Limit |
|---|---|---|
/v3/webhooks | GET | 200/min |
/v3/webhooks | POST | 200/min |
/v3/webhooks/{id}/rotate-secret | POST | 10/min |
/v3/webhooks/{id}/test | POST | 60/min |
Users
| Endpoint | Method | Rate Limit |
|---|---|---|
/v3/users | GET | 200/min |
/v3/users | POST | 10/min |
/v3/users/{id} | PATCH | 200/min |
/v3/users/{id} | DELETE | 200/min |
Increasing Rate Limits
If you're consistently hitting rate limits, consider:
Optimize Your Integration
- Implement caching to reduce duplicate requests
- Use webhooks instead of polling for status updates
- Batch operations where possible
Contact Support
For legitimate high-volume use cases, contact support@sent.dm to discuss:
- Increased rate limits
- Enterprise pricing
- Dedicated infrastructure
When contacting support, provide:
- Your use case and expected volume
- Current rate limit issues you're experiencing
- Timeframe for increased needs
Rate Limiting FAQ
Do rate limits apply per API key or per account?
Rate limits apply per customer account. All API keys for the same account share the same rate limit pool.
Do failed requests count against rate limits?
Authentication failures (401, 403) do not count against rate limits. All other requests, including validation errors (400, 422), count toward your limit.
What happens when I exceed a rate limit?
You receive a 429 Too Many Requests response with a Retry-After header indicating when you can retry.
Can I check my current rate limit status?
Yes, check the X-RateLimit-* headers on any API response.
Do rate limits reset at specific times?
Rate limits use a sliding window. The X-RateLimit-Reset header indicates when your current window expires.