Rate Limits & Errors
The VelaFlows API enforces rate limits to ensure fair usage and platform stability. This page explains how rate limiting works, how to handle errors, and best practices for building resilient integrations.
Rate Limits
Default Limits
Rate limits are applied per API token, per endpoint:
| Endpoint Category | Limit | Window |
|---|---|---|
| Read endpoints (GET) | 300 requests | 15 minutes |
| Write endpoints (POST, PATCH, PUT) | 100 requests | 15 minutes |
| Delete endpoints (DELETE) | 50 requests | 15 minutes |
| Authentication endpoints | 20 requests | 15 minutes |
| Bulk operations | 10 requests | 15 minutes |
Rate Limit Headers
Every API response includes rate limit headers so you can track your usage:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1711296000| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the current window resets |
429 Too Many Requests
When you exceed the rate limit, the API returns a 429 status code:
{
"success": false,
"error": {
"code": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded. Try again in 45 seconds."
}
}The response also includes a Retry-After header with the number of seconds
to wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 45Error Reference
Error Response Format
All errors follow a consistent structure:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {
"errors": [
{ "path": "email", "message": "Invalid email address" }
]
}
}
}HTTP Status Codes
| Status | Code | Description |
|---|---|---|
400 | BAD_REQUEST | The request body is malformed or missing required fields |
401 | UNAUTHORIZED | Authentication failed — token is missing, invalid, or expired |
403 | FORBIDDEN | The token doesn’t have permission for this action |
404 | NOT_FOUND | The requested resource doesn’t exist |
409 | CONFLICT | The action conflicts with the current state (e.g., duplicate resource) |
422 | VALIDATION_ERROR | The request body failed validation — check details.errors |
429 | TOO_MANY_REQUESTS | Rate limit exceeded — wait and retry |
500 | INTERNAL_ERROR | Something went wrong on our end — contact support if persistent |
Validation Errors (422)
When a request fails validation, the details.errors array tells you exactly
what’s wrong:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"errors": [
{ "path": "email", "message": "Invalid email address" },
{ "path": "firstName", "message": "First name is required" }
]
}
}
}Each entry in errors tells you which field (path) failed and why (message).
Best Practices
Implement Exponential Backoff
When you receive a 429 or 5xx error, don’t retry immediately. Use exponential
backoff with jitter:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.ok) {
return response.json()
}
if (response.status === 429 || response.status >= 500) {
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} retries`)
}
const retryAfter = response.headers.get('Retry-After')
const baseDelay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000
// Add jitter to avoid thundering herd
const jitter = Math.random() * 1000
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter))
continue
}
// Non-retryable error
const error = await response.json()
throw new Error(error.error?.message || 'Request failed')
}
}Cache Responses
For data that doesn’t change frequently, cache API responses to reduce the number of requests:
const cache = new Map()
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
async function getCachedConversation(id) {
const cacheKey = `conversation:${id}`
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data
}
const data = await velaflows.getConversation(id)
cache.set(cacheKey, { data, timestamp: Date.now() })
return data
}Use Webhooks Instead of Polling
Instead of polling the API every few seconds to check for new conversations or messages, subscribe to webhooks. Webhooks push events to your server in real-time, eliminating the need for repeated API calls.
Polling: 300 requests/15min to check for updates (wasteful)
Webhooks: 0 requests — VelaFlows notifies you when something happensPaginate Efficiently
When listing large datasets, use pagination parameters to fetch only what you need:
# Fetch page 1 with 20 items
GET /api/v1/inbox/conversations?page=1&limit=20
# Fetch next page
GET /api/v1/inbox/conversations?page=2&limit=20Don’t set limit higher than you need. Fetching 100 items when you only display
10 wastes bandwidth and counts against your rate limit.
Batch Operations
Where available, use batch endpoints instead of making individual requests:
# Instead of 50 individual tag operations:
POST /api/v1/tags/bulk
{
"entityType": "conversation",
"entityIds": ["id1", "id2", ...],
"tagIds": ["tag1", "tag2"]
}