How to handle Sent API errors

This guide shows you how to handle Sent API v3 errors in your client code: detect failures, branch on error codes, capture request IDs for support, prevent avoidable errors, and test your handling without side effects. It assumes you can already authenticate and send requests to the API; if not, start with the API reference overview.

Every error uses the same JSON envelope with a machine-readable code. The Error Handling reference documents the envelope and HTTP status codes; the Error Catalog enumerates every code.

Check the success flag on every response

The envelope's success boolean is the reliable failure signal. Check it before reading data:

const response = await fetch('/v3/messages', { ... });
const data: ApiResponse<Message> = await response.json();

if (!data.success) {
  // Handle error
  console.error(`Error ${data.error?.code}: ${data.error?.message}`);
  return;
}

// Process successful response
console.log(data.data);
import requests

response = requests.post('/v3/messages', ...)
data = response.json()

if not data['success']:
    # Handle error
    print(f"Error {data['error']['code']}: {data['error']['message']}")
    return

# Process successful response
print(data['data'])
resp, err := http.Post("/v3/messages", "application/json", body)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var data ApiResponse[Message]
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
    log.Fatal(err)
}

if !data.Success {
    // Handle error
    log.Printf("Error %s: %s\n", data.Error.Code, data.Error.Message)
    return
}

// Process successful response
log.Println(data.Data)

Branch on error codes, not messages

Error messages can change; codes are stable. Branch on error.code and give each class of failure its own recovery path:

switch (data.error?.code) {
  case 'AUTH_001':
  case 'AUTH_002':
    // Redirect to login or refresh API key
    break;
  case 'RESOURCE_001':
    // Prompt user to create the resource
    break;
  case 'BUSINESS_002':
    // Implement retry with backoff
    const retryAfter = response.headers.get('Retry-After');
    await sleep(parseInt(retryAfter || '60') * 1000);
    break;
  default:
    // Log unexpected error
    console.error('Unexpected error:', data.error);
}
error_code = data['error']['code']

if error_code in ('AUTH_001', 'AUTH_002'):
    # Redirect to login or refresh API key
    pass
elif error_code == 'RESOURCE_001':
    # Prompt user to create the resource
    pass
elif error_code == 'BUSINESS_002':
    # Implement retry with backoff
    retry_after = int(response.headers.get('Retry-After', 60))
    time.sleep(retry_after)
else:
    # Log unexpected error
    print('Unexpected error:', data['error'])
switch data.Error.Code {
case "AUTH_001", "AUTH_002":
    // Redirect to login or refresh API key
case "RESOURCE_001":
    // Prompt user to create the resource
case "BUSINESS_002":
    // Implement retry with backoff
    retryAfter := resp.Header.Get("Retry-After")
    seconds, _ := strconv.Atoi(retryAfter)
    if seconds == 0 {
        seconds = 60
    }
    time.Sleep(time.Duration(seconds) * time.Second)
default:
    // Log unexpected error
    log.Printf("Unexpected error: %+v\n", data.Error)
}

If you retry on BUSINESS_002 (rate limit) or 5xx errors, use exponential backoff. See How to handle Sent API rate limits for a complete implementation.

Log request IDs for support

Every response includes meta.request_id. Log it with each failure, and include it when contacting support@sent.dm. The ID lets support trace the exact request:

console.error(`Request ID: ${data.meta.request_id}`);
// Provide this to support@sent.dm when reporting issues
print(f"Request ID: {data['meta']['request_id']}")
# Provide this to support@sent.dm when reporting issues
log.Printf("Request ID: %s\n", data.Meta.RequestID)
// Provide this to support@sent.dm when reporting issues

Make retries safe with idempotency keys

If your error handling retries mutations (POST, PUT, PATCH), send an Idempotency-Key header so a retry after a timeout or 5xx error cannot perform the operation twice:

POST /v3/messages
x-api-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Idempotency-Key: send-msg-order-8412
Content-Type: application/json

Derive the key from a business identifier for the operation (an order ID, a client-generated request ID) and reuse the same key on every retry attempt. How to retry Sent API requests safely covers key derivation and retry loops in full.

Validate inputs before sending

Prevent the most common VALIDATION_* errors client-side. Phone numbers must be in E.164 format:

// Validate phone number format
function isValidE164(phone: string): boolean {
  return /^\+[1-9]\d{1,14}$/.test(phone);
}

if (!isValidE164(phoneNumber)) {
  throw new Error('Phone number must be in E.164 format');
}
import re

# Validate phone number format
def is_valid_e164(phone: str) -> bool:
    return bool(re.match(r'^\+[1-9]\d{1,14}$', phone))

if not is_valid_e164(phone_number):
    raise ValueError('Phone number must be in E.164 format')
import "regexp"

// Validate phone number format
func isValidE164(phone string) bool {
    matched, _ := regexp.MatchString(`^\+[1-9]\d{1,14}$`, phone)
    return matched
}

if !isValidE164(phoneNumber) {
    log.Fatal("Phone number must be in E.164 format")
}

When the server rejects input, read the error.details object. It carries field-level messages you can surface directly to users.

Test your error handling with sandbox mode

To exercise your error paths without sending real messages, add sandbox: true to any mutation request. Validation still runs, so invalid input returns real validation errors with no side effects:

curl -X POST https://api.sent.dm/v3/messages \
  -H "x-api-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "sandbox": true,
    "to": ["invalid"],
    "template": {"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}
  }'

This returns a VALIDATION_* error envelope without attempting to send a message. Refer to the Sandbox Mode reference for the full contract.

Verify your handling

You have working error handling when:

  • A request with an invalid phone number surfaces the field-level message from error.details instead of a generic failure.
  • A 429 response triggers a delayed retry rather than an immediate one.
  • Failed requests appear in your logs with error.code and meta.request_id.

On this page