All API errors follow a consistent format:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error description",
"details": {}
}
}
| Field | Type | Description |
|---|
success | boolean | Always false for errors |
error.code | string | Machine-readable error code |
error.message | string | Human-readable description |
error.details | object | Additional context (optional) |
| Status | Description |
|---|
200 OK | Request succeeded |
201 Created | Resource created successfully |
204 No Content | Request succeeded, no response body |
| Status | Description |
|---|
400 Bad Request | Invalid request body or parameters |
401 Unauthorized | Missing or invalid API key |
403 Forbidden | Valid API key but insufficient permissions |
404 Not Found | Resource doesn't exist |
409 Conflict | Resource already exists or conflict |
422 Unprocessable Entity | Validation failed |
429 Too Many Requests | Rate limit exceeded |
| Status | Description |
|---|
500 Internal Server Error | Unexpected server error |
502 Bad Gateway | Upstream service error |
503 Service Unavailable | Service temporarily unavailable |
| Code | HTTP Status | Description |
|---|
UNAUTHORIZED | 401 | API key missing or invalid |
INVALID_API_KEY | 401 | API key format is invalid |
EXPIRED_API_KEY | 401 | API key has expired |
REVOKED_API_KEY | 401 | API key has been revoked |
FORBIDDEN | 403 | API key lacks required permissions |
Example:
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key provided"
}
}
| Code | HTTP Status | Description |
|---|
INVALID_REQUEST | 400 | Request body is malformed |
VALIDATION_ERROR | 422 | One or more fields failed validation |
MISSING_FIELD | 422 | Required field is missing |
INVALID_FIELD | 422 | Field value is invalid |
Example:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"fields": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "to",
"message": "Phone number must be in E.164 format"
}
]
}
}
}
| Code | HTTP Status | Description |
|---|
NOT_FOUND | 404 | Resource doesn't exist |
ALREADY_EXISTS | 409 | Resource already exists |
CONFLICT | 409 | Operation conflicts with current state |
GONE | 410 | Resource has been deleted |
| Code | HTTP Status | Description |
|---|
RATE_LIMITED | 429 | Too many requests |
QUOTA_EXCEEDED | 429 | Monthly quota exceeded |
Example:
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 60 seconds",
"details": {
"retryAfter": 60
}
}
}
| Code | HTTP Status | Description |
|---|
INTERNAL_ERROR | 500 | Unexpected server error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
| Code | Description |
|---|
EMAIL_NOT_VERIFIED | Sender email not verified |
DOMAIN_NOT_VERIFIED | Sending domain not verified |
INVALID_TEMPLATE | Template not found or invalid |
RECIPIENT_SUPPRESSED | Recipient is on suppression list |
BOUNCE_LIMIT_EXCEEDED | Too many bounces for this server |
| Code | Description |
|---|
SMS_NOT_ENABLED | SMS not enabled for organization |
INVALID_PHONE_NUMBER | Phone number format invalid |
TEMPLATE_NOT_FOUND | SMS template not found |
TEMPLATE_NOT_APPROVED | Template pending approval |
NO_POOL_NUMBER | No pool number for destination country |
RECIPIENT_OPTED_OUT | Recipient has opted out |
| Code | Description |
|---|
USER_BLOCKED | User account is blocked |
EMAIL_NOT_VERIFIED | User email not verified |
PASSWORD_TOO_WEAK | Password doesn't meet policy |
PASSWORD_BREACHED | Password found in breach database |
MFA_REQUIRED | MFA verification required |
INVALID_MFA_CODE | MFA code is invalid |
ACCOUNT_LOCKED | Account temporarily locked |
| Code | Description |
|---|
FORM_NOT_LIVE | Form is not published |
FORM_CLOSED | Form is closed for submissions |
SUBMISSION_LIMIT_REACHED | Form submission limit reached |
INVALID_FIELD_TYPE | Unknown form field type |
import { Transactional, TransactionalError } from 'transactional-sdk';
const client = new Transactional({ apiKey: 'your-api-key' });
try {
const email = await client.emails.send({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Hello',
html: '<p>World</p>',
});
} catch (error) {
if (error instanceof TransactionalError) {
console.error(`Error code: ${error.code}`);
console.error(`Message: ${error.message}`);
console.error(`Status: ${error.statusCode}`);
// Handle specific errors
switch (error.code) {
case 'RATE_LIMITED':
const retryAfter = error.details?.retryAfter || 60;
console.log(`Retry after ${retryAfter} seconds`);
break;
case 'VALIDATION_ERROR':
console.log('Validation errors:', error.details?.fields);
break;
case 'UNAUTHORIZED':
console.log('Check your API key');
break;
}
}
}
from transactional import Transactional
from transactional.errors import (
TransactionalError,
AuthenticationError,
ValidationError,
RateLimitError,
NotFoundError,
)
client = Transactional(api_key="your-api-key")
try:
email = client.emails.send(
from_email="sender@example.com",
to="recipient@example.com",
subject="Hello",
html="<p>World</p>",
)
except AuthenticationError as e:
print(f"Authentication failed: {e.message}")
except ValidationError as e:
print(f"Validation failed: {e.message}")
for field in e.details.get("fields", []):
print(f" - {field['field']}: {field['message']}")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
except NotFoundError as e:
print(f"Resource not found: {e.message}")
except TransactionalError as e:
print(f"API error ({e.code}): {e.message}")
Use the error.code field for programmatic error handling, not the message:
// Good
if (error.code === 'RATE_LIMITED') {
await sleep(error.details.retryAfter * 1000);
return retry();
}
// Bad - messages may change
if (error.message.includes('rate limit')) {
// This is fragile
}
For rate limiting and transient errors:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (
error instanceof TransactionalError &&
error.code === 'RATE_LIMITED' &&
attempt < maxRetries - 1
) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
catch (error) {
if (error instanceof TransactionalError) {
console.error({
timestamp: new Date().toISOString(),
code: error.code,
message: error.message,
statusCode: error.statusCode,
details: error.details,
requestId: error.requestId,
});
}
}
Display validation errors to users in a helpful way:
catch (error) {
if (error instanceof TransactionalError && error.code === 'VALIDATION_ERROR') {
const fieldErrors = error.details?.fields || [];
// Map to form errors
const formErrors = fieldErrors.reduce((acc, err) => {
acc[err.field] = err.message;
return acc;
}, {});
setFormErrors(formErrors);
}
}
Every API response includes a request ID in the X-Request-Id header. Include this when contacting support:
curl -i https://api.usetransactional.com/v1/emails \
-H "Authorization: Bearer YOUR_API_KEY"
# Response headers include:
# X-Request-Id: req_abc123xyz
Use request IDs for:
- Support tickets
- Debugging specific failures
- Correlating logs