Error Handling
Error codes, response format, and rate limiting.
Error Response Format
All errors follow a consistent format:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Human-readable error message",
"details": {
"field": "additional context"
},
"requestId": "req_abc123xyz"
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable description |
details | object | Additional context (optional) |
requestId | string | Unique request identifier for support |
HTTP Status Codes
| Status | Description | When Used |
|---|---|---|
200 | OK | Successful GET, PATCH |
201 | Created | Successful POST creating a resource |
204 | No Content | Successful DELETE |
400 | Bad Request | Invalid input, validation errors |
401 | Unauthorized | Missing or invalid token |
403 | Forbidden | Insufficient permissions |
404 | Not Found | Resource doesn't exist |
409 | Conflict | Resource conflict (e.g., duplicate) |
422 | Unprocessable Entity | Business logic validation failed |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server-side error |
Error Codes
Authentication Errors (401)
| Code | Message | Description |
|---|---|---|
UNAUTHORIZED | Invalid or missing authentication token | Token is missing, expired, or invalid |
TOKEN_EXPIRED | Authentication token has expired | JWT has passed its expiration time |
TOKEN_INVALID | Authentication token is malformed | JWT cannot be decoded or verified |
Authorization Errors (403)
| Code | Message | Description |
|---|---|---|
FORBIDDEN | Insufficient permissions | User lacks required scope |
TENANT_MISMATCH | Resource belongs to different tenant | Cross-tenant access attempted |
SCOPE_REQUIRED | Missing required scope: {scope} | Specific scope needed |
Not Found Errors (404)
| Code | Message | Description |
|---|---|---|
CUSTOMER_NOT_FOUND | Customer not found | Customer ID doesn't exist |
SEGMENT_NOT_FOUND | Segment not found | Segment ID doesn't exist |
CAMPAIGN_NOT_FOUND | Campaign not found | Campaign ID doesn't exist |
JOURNEY_NOT_FOUND | Journey not found | Journey ID doesn't exist |
JOURNEY_STEP_NOT_FOUND | Journey step not found | Step ID doesn't exist |
ENROLLMENT_NOT_FOUND | Enrollment not found | Enrollment doesn't exist |
CONNECTION_NOT_FOUND | Social connection not found | Connection ID doesn't exist |
POST_NOT_FOUND | Social post not found | Post ID doesn't exist |
PROMOTION_NOT_FOUND | Event promotion not found | Promotion ID doesn't exist |
NOTE_NOT_FOUND | Note not found | Note ID doesn't exist |
Validation Errors (400)
| Code | Message | Description |
|---|---|---|
VALIDATION_ERROR | Validation failed | General validation failure |
INVALID_EMAIL | Invalid email format | Email doesn't match pattern |
INVALID_PHONE | Invalid phone format | Phone not in E.164 format |
INVALID_DATE | Invalid date format | Date not in ISO 8601 format |
INVALID_CRITERIA | Invalid segment criteria | Criteria JSON is malformed |
MISSING_REQUIRED_FIELD | Missing required field: {field} | Required field not provided |
INVALID_ENUM_VALUE | Invalid value for {field} | Value not in allowed enum |
Business Logic Errors (422)
| Code | Message | Description |
|---|---|---|
INVALID_CAMPAIGN_STATUS | Invalid status transition | Can't transition campaign state |
INVALID_JOURNEY_STATUS | Invalid status transition | Can't transition journey state |
JOURNEY_NO_STEPS | Journey has no steps | Can't activate journey without steps |
JOURNEY_ACTIVE | Cannot modify active journey | Journey must be paused first |
CAMPAIGN_ALREADY_SENT | Campaign has already been sent | Can't modify completed campaign |
SEGMENT_IN_USE | Segment is used by active campaigns | Can't delete segment |
CUSTOMER_ALREADY_ENROLLED | Customer already enrolled in journey | Re-entry not allowed |
MERGE_SAME_CUSTOMER | Cannot merge customer with itself | Winner and loser are same |
DUPLICATE_EMAIL | Email already exists | Email uniqueness violation |
OAUTH_FAILED | OAuth authorization failed | OAuth flow error |
Social Media Errors (500/422)
| Code | Message | Description |
|---|---|---|
SOCIAL_PUBLISH_FAILED | Failed to publish to platform | Platform API error |
SOCIAL_TOKEN_EXPIRED | Social media token expired | Need to reconnect |
SOCIAL_PERMISSION_DENIED | Missing platform permission | Reconnect with permissions |
SOCIAL_RATE_LIMITED | Platform rate limit exceeded | Try again later |
SOCIAL_CONTENT_REJECTED | Content rejected by platform | Policy violation |
Rate Limit Errors (429)
| Code | Message | Description |
|---|---|---|
RATE_LIMITED | Too many requests | Rate limit exceeded |
QUOTA_EXCEEDED | Monthly quota exceeded | Usage limit reached |
Rate Limits
Default Limits
| Endpoint Type | Limit | Window |
|---|---|---|
| Read operations | 1000 | per minute |
| Write operations | 100 | per minute |
| Campaign execution | 10 | per minute |
| Social publishing | 30 | per minute |
| Bulk operations | 10 | per minute |
| GraphQL queries | 500 | per minute |
Rate Limit Headers
All responses include rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1734184800
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when limit resets |
Rate Limit Response
When rate limited, you'll receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1734184800
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please retry after 30 seconds.",
"details": {
"retryAfter": 30
},
"requestId": "req_abc123"
}
}
Error Handling Best Practices
Retry Strategy
For transient errors (5xx, 429), implement exponential backoff:
async function requestWithRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
const retryAfter = error.headers['retry-after'] || Math.pow(2, attempt);
await sleep(retryAfter * 1000);
continue;
}
if (error.status >= 500 && attempt < maxRetries - 1) {
await sleep(Math.pow(2, attempt) * 1000);
continue;
}
throw error;
}
}
}
Handling Validation Errors
Validation errors include details about which fields failed:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"errors": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "phoneNumber", "message": "Must be E.164 format" }
]
},
"requestId": "req_abc123"
}
}
Request ID for Support
Always log the requestId from error responses. This ID can be used when contacting support to trace the specific request.
GraphQL Errors
GraphQL errors follow the GraphQL specification:
{
"data": null,
"errors": [
{
"message": "Customer not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["customer"],
"extensions": {
"code": "CUSTOMER_NOT_FOUND",
"requestId": "req_abc123"
}
}
]
}
Partial success is possible - some fields may resolve while others error:
{
"data": {
"customer": {
"id": "123",
"name": "John",
"segment": null
}
},
"errors": [
{
"message": "Segment not found",
"path": ["customer", "segment"],
"extensions": {
"code": "SEGMENT_NOT_FOUND"
}
}
]
}