Error Handling

Understanding API errors and how to handle them.

Error Response Format

All API errors follow a consistent format:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error description",
    "details": {}
  }
}
FieldTypeDescription
successbooleanAlways false for errors
error.codestringMachine-readable error code
error.messagestringHuman-readable description
error.detailsobjectAdditional context (optional)

HTTP Status Codes

Success Codes

StatusDescription
200 OKRequest succeeded
201 CreatedResource created successfully
204 No ContentRequest succeeded, no response body

Client Error Codes

StatusDescription
400 Bad RequestInvalid request body or parameters
401 UnauthorizedMissing or invalid API key
403 ForbiddenValid API key but insufficient permissions
404 Not FoundResource doesn't exist
409 ConflictResource already exists or conflict
422 Unprocessable EntityValidation failed
429 Too Many RequestsRate limit exceeded

Server Error Codes

StatusDescription
500 Internal Server ErrorUnexpected server error
502 Bad GatewayUpstream service error
503 Service UnavailableService temporarily unavailable

Common Error Codes

Authentication Errors

CodeHTTP StatusDescription
UNAUTHORIZED401API key missing or invalid
INVALID_API_KEY401API key format is invalid
EXPIRED_API_KEY401API key has expired
REVOKED_API_KEY401API key has been revoked
FORBIDDEN403API key lacks required permissions

Example:

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid API key provided"
  }
}

Validation Errors

CodeHTTP StatusDescription
INVALID_REQUEST400Request body is malformed
VALIDATION_ERROR422One or more fields failed validation
MISSING_FIELD422Required field is missing
INVALID_FIELD422Field 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"
        }
      ]
    }
  }
}

Resource Errors

CodeHTTP StatusDescription
NOT_FOUND404Resource doesn't exist
ALREADY_EXISTS409Resource already exists
CONFLICT409Operation conflicts with current state
GONE410Resource has been deleted

Rate Limiting Errors

CodeHTTP StatusDescription
RATE_LIMITED429Too many requests
QUOTA_EXCEEDED429Monthly quota exceeded

Example:

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after 60 seconds",
    "details": {
      "retryAfter": 60
    }
  }
}

Server Errors

CodeHTTP StatusDescription
INTERNAL_ERROR500Unexpected server error
SERVICE_UNAVAILABLE503Service temporarily unavailable

Module-Specific Error Codes

Email Errors

CodeDescription
EMAIL_NOT_VERIFIEDSender email not verified
DOMAIN_NOT_VERIFIEDSending domain not verified
INVALID_TEMPLATETemplate not found or invalid
RECIPIENT_SUPPRESSEDRecipient is on suppression list
BOUNCE_LIMIT_EXCEEDEDToo many bounces for this server

SMS Errors

CodeDescription
SMS_NOT_ENABLEDSMS not enabled for organization
INVALID_PHONE_NUMBERPhone number format invalid
TEMPLATE_NOT_FOUNDSMS template not found
TEMPLATE_NOT_APPROVEDTemplate pending approval
NO_POOL_NUMBERNo pool number for destination country
RECIPIENT_OPTED_OUTRecipient has opted out

Auth Errors

CodeDescription
USER_BLOCKEDUser account is blocked
EMAIL_NOT_VERIFIEDUser email not verified
PASSWORD_TOO_WEAKPassword doesn't meet policy
PASSWORD_BREACHEDPassword found in breach database
MFA_REQUIREDMFA verification required
INVALID_MFA_CODEMFA code is invalid
ACCOUNT_LOCKEDAccount temporarily locked

Forms Errors

CodeDescription
FORM_NOT_LIVEForm is not published
FORM_CLOSEDForm is closed for submissions
SUBMISSION_LIMIT_REACHEDForm submission limit reached
INVALID_FIELD_TYPEUnknown form field type

Handling Errors in SDKs

TypeScript SDK

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;
    }
  }
}

Python SDK

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}")

Error Handling Best Practices

1. Always Check Error Codes

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
}

2. Implement Exponential Backoff

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');
}

3. Log Errors for Debugging

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,
    });
  }
}

4. Handle Validation Errors Gracefully

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);
  }
}

Request IDs

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