Test Mode

Build fearlessly. Test recklessly. Deploy confidently.

Test mode is your safety net — a way to validate every aspect of your integration without sending real messages, consuming credits, or touching production data. It's like having a perfectly realistic simulation of the Sent API that never leaves your sandbox.

The Golden Rule: Add test_mode: true to any mutation request. The API validates everything and returns a realistic response — but nothing actually happens.


Why Test Mode Changes Everything

Without Test ModeWith Test Mode
😰 Burn credits on every test😎 Zero credit consumption
📱 Accidentally message real users🧪 Fake responses, zero side effects
🔧 Guess if your payload is valid✅ Real validation, instant feedback
🚀 Hope it works in production🎯 Know it works before you ship

How It Works

When you include test_mode: true in your request:

  1. Authentication runs — Invalid API keys are still rejected
  2. Validation runs — Malformed payloads return real errors
  3. Permissions checked — Authorization rules still apply
  4. Response returned — A realistic fake response with sample data
  5. Nothing happens — No messages sent, no database writes, no external calls
POST /v3/messages
Content-Type: application/json
x-api-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

{
  "test_mode": true,
  "phone_number": "+1234567890",
  "template_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "variables": {
    "customer_name": "Test User"
  }
}

Response:

HTTP/1.1 201 Created
X-Test-Mode: true
X-Request-Id: req_test_abc123
Content-Type: application/json

{
  "success": true,
  "data": {
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "phone": "+1234567890",
    "template_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "channel": "sms",
    "status": "pending",
    "price": 0.01,
    "created_at": "2024-01-15T10:30:00Z"
  },
  "error": null,
  "meta": {
    "request_id": "req_test_abc123",
    "timestamp": "2024-01-15T10:30:00Z",
    "version": "v3"
  }
}

Notice the X-Test-Mode: true header — your confirmation that this was a test.


Perfect for These Scenarios

Unit Testing

Test your integration without mocking the entire API:

// Your test suite
async function testMessageSending() {
  const response = await sentApi.sendMessage({
    test_mode: true,  // Zero side effects
    phone_number: "+1234567890",
    template_id: "welcome-template",
    variables: { name: "Test" }
  });

  // Assert against real response format
  expect(response.success).toBe(true);
  expect(response.data.status).toBe("pending");
  expect(response.meta.request_id).toBeDefined();
}
# Your test suite
def test_message_sending():
    response = sent_api.send_message(
        test_mode=True,  # Zero side effects
        phone_number="+1234567890",
        template_id="welcome-template",
        variables={"name": "Test"}
    )

    # Assert against real response format
    assert response['success'] is True
    assert response['data']['status'] == "pending"
    assert 'request_id' in response['meta']
// Your test suite
func TestMessageSending(t *testing.T) {
    response, err := sentAPI.SendMessage(SendMessageRequest{
        TestMode:    true,  // Zero side effects
        PhoneNumber: "+1234567890",
        TemplateID:  "welcome-template",
        Variables:   map[string]string{"name": "Test"},
    })

    // Assert against real response format
    assert.True(t, response.Success)
    assert.Equal(t, "pending", response.Data.Status)
    assert.NotEmpty(t, response.Meta.RequestID)
}

CI/CD Pipelines

Run integration tests in your pipeline without burning credits:

# .github/workflows/test.yml
- name: Integration Tests
  env:
    SENT_API_KEY: ${{ secrets.SENT_API_KEY }}
  run: |
    # All tests run in test mode — zero cost
    npm run test:integration

Debugging Production Issues

Reproduce a production error without risking more issues:

# Reproduce the exact request that failed
response = client.request('POST', '/v3/messages', {
    'test_mode': True,  # Safe reproduction
    'phone_number': problem_number,
    'template_id': problem_template,
    'variables': problem_variables
})

# Inspect the full response without side effects
print(f"Validation result: {response}")

Interactive Development

Experiment freely while building your integration:

# Try different payloads without consequences
curl -X POST https://api.sent.dm/v3/contacts \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "test_mode": true,
    "phone_number": "+15555555555"
  }'

Supported Endpoints

Test mode works on all mutation endpoints:

EndpointTest Behavior
POST /v3/messagesReturns fake message object, no SMS/WhatsApp sent
POST /v3/contactsReturns fake contact, no database write
PATCH /v3/contacts/{id}Returns fake updated contact
DELETE /v3/contacts/{id}Returns 204, contact not deleted
POST /v3/templatesReturns fake template with "PENDING" status
PUT /v3/templates/{id}Returns fake updated template
DELETE /v3/templates/{id}Returns 204, template not deleted
POST /v3/webhooksReturns fake webhook with random secret
POST /v3/profilesReturns fake profile
POST /v3/brandsReturns fake brand with TCR simulation

Test Mode vs Production

class SentClient {
  private apiKey: string;
  private baseUrl = 'https://api.sent.dm';

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async sendMessage(
    payload: MessagePayload,
    options: { test?: boolean } = {}
  ): Promise<APIResponse> {
    const response = await fetch(`${this.baseUrl}/v3/messages`, {
      method: 'POST',
      headers: {
        'x-api-key': this.apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        ...payload,
        test_mode: options.test ?? false
      })
    });

    return response.json();
  }
}

// Usage
const client = new SentClient(process.env.SENT_API_KEY!);

// Development — safe, no side effects
await client.sendMessage({
  phone_number: '+1234567890',
  template_id: 'welcome-template',
  variables: { name: 'Test' }
}, { test: true });

// Production — the real deal
await client.sendMessage({
  phone_number: '+1234567890',
  template_id: 'welcome-template',
  variables: { name: 'Real User' }
}, { test: false });
import os
import requests
from typing import Optional, Dict, Any

class SentClient:
    def __init__(self, api_key: Optional[str] = None):
        self.api_key = api_key or os.environ['SENT_API_KEY']
        self.base_url = 'https://api.sent.dm'

    def send_message(
        self,
        phone_number: str,
        template_id: str,
        variables: Optional[Dict[str, Any]] = None,
        test_mode: bool = False
    ) -> Dict[str, Any]:
        """Send a message with optional test mode."""

        response = requests.post(
            f'{self.base_url}/v3/messages',
            headers={
                'x-api-key': self.api_key,
                'Content-Type': 'application/json'
            },
            json={
                'phone_number': phone_number,
                'template_id': template_id,
                'variables': variables or {},
                'test_mode': test_mode  # Safe testing when True
            }
        )

        return response.json()


# Usage
client = SentClient()

# Development — safe, no side effects
client.send_message(
    '+1234567890',
    'welcome-template',
    {'name': 'Test'},
    test_mode=True
)

# Production — the real deal
client.send_message(
    '+1234567890',
    'welcome-template',
    {'name': 'Real User'},
    test_mode=False
)
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

type SentClient struct {
    APIKey  string
    BaseURL string
}

func NewClient() *SentClient {
    return &SentClient{
        APIKey:  os.Getenv("SENT_API_KEY"),
        BaseURL: "https://api.sent.dm",
    }
}

func (c *SentClient) SendMessage(
    phone string,
    templateID string,
    variables map[string]string,
    testMode bool,
) (map[string]interface{}, error) {
    payload := map[string]interface{}{
        "phone_number": phone,
        "template_id":  templateID,
        "variables":    variables,
        "test_mode":    testMode, // Safe testing when true
    }

    body, _ := json.Marshal(payload)
    req, _ := http.NewRequest(
        "POST",
        c.BaseURL+"/v3/messages",
        bytes.NewBuffer(body),
    )

    req.Header.Set("x-api-key", c.APIKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    return result, nil
}

// Usage
func main() {
    client := NewClient()

    // Development — safe, no side effects
    client.SendMessage(
        "+1234567890",
        "welcome-template",
        map[string]string{"name": "Test"},
        true, // testMode
    )

    // Production — the real deal
    client.SendMessage(
        "+1234567890",
        "welcome-template",
        map[string]string{"name": "Real User"},
        false, // testMode
    )
}

Best Practices

Always Test First

// Good: Test in development, then deploy
async function deployToProduction() {
  // Step 1: Validate in test mode
  const testResult = await sendMessage(payload, { test: true });
  if (!testResult.success) {
    throw new Error('Validation failed');
  }

  // Step 2: Deploy with confidence
  return await sendMessage(payload, { test: false });
}
# Good: Test in development, then deploy
def deploy_to_production(payload):
    # Step 1: Validate in test mode
    test_result = send_message(payload, test_mode=True)
    if not test_result.get('success'):
        raise Exception('Validation failed')

    # Step 2: Deploy with confidence
    return send_message(payload, test_mode=False)
// Good: Test in development, then deploy
func deployToProduction(payload MessagePayload) (*APIResponse, error) {
    // Step 1: Validate in test mode
    testResult, err := sendMessage(payload, true)
    if err != nil || !testResult.Success {
        return nil, fmt.Errorf("validation failed")
    }

    // Step 2: Deploy with confidence
    return sendMessage(payload, false)
}

Use in CI/CD

# Test job — runs in test mode
- name: Run Integration Tests
  run: |
    export SENT_API_KEY=${{ secrets.SENT_API_KEY }}
    pytest tests/integration --test-mode

# Deploy job — only after tests pass
- name: Deploy to Production
  if: github.ref == 'refs/heads/main'
  run: ./deploy.sh

Debug Without Fear

// Reproduce production issues safely
async function debugFailedMessage(originalPayload: MessagePayload) {
  // Add test mode to debug without side effects
  const debugPayload = { ...originalPayload, test_mode: true };

  const response = await api.post('/v3/messages', debugPayload);

  // Inspect full response
  logger.debug('Debug response:', response);

  return response;
}
# Reproduce production issues safely
def debug_failed_message(original_payload):
    # Add test mode to debug without side effects
    debug_payload = {**original_payload, 'test_mode': True}

    response = api.post('/v3/messages', json=debug_payload)

    # Inspect full response
    logger.debug(f"Debug response: {response}")

    return response
// Reproduce production issues safely
func debugFailedMessage(originalPayload map[string]interface{}) (map[string]interface{}, error) {
    // Add test mode to debug without side effects
    debugPayload := make(map[string]interface{})
    for k, v := range originalPayload {
        debugPayload[k] = v
    }
    debugPayload["test_mode"] = true

    response, err := api.Post("/v3/messages", debugPayload)
    if err != nil {
        return nil, err
    }

    // Inspect full response
    log.Printf("Debug response: %+v", response)

    return response, nil
}

Environment-Based Toggle

// Automatically use test mode in development
const isProduction = process.env.NODE_ENV === 'production';

const client = new SentClient({
  apiKey: process.env.SENT_API_KEY!,
  testMode: !isProduction // Auto-enable in dev/staging
});
import os

# Automatically use test mode in development
is_production = os.environ.get('ENVIRONMENT') == 'production'

client = SentClient(
    api_key=os.environ.get('SENT_API_KEY'),
    test_mode=not is_production  # Auto-enable in dev/staging
)
import "os"

// Automatically use test mode in development
isProduction := os.Getenv("ENVIRONMENT") == "production"

client := NewSentClient(SentClientOptions{
    APIKey:   os.Getenv("SENT_API_KEY"),
    TestMode: !isProduction, // Auto-enable in dev/staging
})

Common Patterns

Feature Flags

// Gradual rollout with test mode
async function sendWithFeatureFlag(payload: MessagePayload) {
const featureEnabled = await checkFeatureFlag('new-messaging');

if (!featureEnabled) {
// Test mode fallback — validate without sending
return await api.sendMessage({ ...payload, test_mode: true });
}

// Full production send
return await api.sendMessage(payload);
}
# Gradual rollout with test mode
def send_with_feature_flag(payload):
feature_enabled = check_feature_flag('new-messaging')

if not feature_enabled:
# Test mode fallback — validate without sending
return api.send_message({**payload, 'test_mode': True})

# Full production send
return api.send_message(payload)
// Gradual rollout with test mode
func sendWithFeatureFlag(payload map[string]interface{}) (*APIResponse, error) {
featureEnabled := checkFeatureFlag("new-messaging")

if !featureEnabled {
// Test mode fallback — validate without sending
payload["test_mode"] = true
return api.SendMessage(payload)
}

// Full production send
return api.SendMessage(payload)
}

Load Testing

// Load test without burning credits
async function loadTest(): Promise<APIResponse[]> {
const tasks: Promise<APIResponse>[] = [];

for (let i = 0; i < 1000; i++) {
const task = api.sendMessage({
phone_number: '+1234567890',
template_id: 'test-template',
variables: { index: i },
test_mode: true  // Zero cost load test
});
tasks.push(task);
}

const results = await Promise.all(tasks);
return analyzeResults(results);
}
# Load test without burning credits
import asyncio

async def load_test():
tasks = []
for i in range(1000):
task = api.send_message(
'+1234567890',
'test-template',
{'index': i},
test_mode=True  # Zero cost load test
)
tasks.append(task)

results = await asyncio.gather(*tasks)
return analyze_results(results)
// Load test without burning credits
func loadTest() ([]APIResponse, error) {
var wg sync.WaitGroup
results := make(chan APIResponse, 1000)

for i := 0; i < 1000; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
resp, _ := api.SendMessage(map[string]interface{}{
"phone_number": "+1234567890",
"template_id":  "test-template",
"variables":    map[string]int{"index": index},
"test_mode":    true, // Zero cost load test
})
results <- resp
}(i)
}

wg.Wait()
close(results)

return analyzeResults(results), nil
}

Canary Deployments

// Validate in test mode before canary
async function deployCanary(): Promise<boolean> {
// Validate configuration
const testResult = await validateConfig({ test_mode: true });
if (!testResult.valid) return false;

// Deploy to 1% of traffic
await deployToCanary(0.01);

// Monitor, then scale
await monitorAndScale();
return true;
}
# Validate in test mode before canary
async def deploy_canary():
# Validate configuration
test_result = await validate_config({'test_mode': True})
if not test_result.get('valid'):
return False

# Deploy to 1% of traffic
await deploy_to_canary(0.01)

# Monitor, then scale
await monitor_and_scale()
return True
// Validate in test mode before canary
func deployCanary() error {
// Validate configuration
testResult, err := validateConfig(map[string]interface{}{
"test_mode": true,
})
if err != nil || !testResult.Valid {
return fmt.Errorf("validation failed")
}

// Deploy to 1% of traffic
if err := deployToCanary(0.01); err != nil {
return err
}

// Monitor, then scale
return monitorAndScale()
}

Test Mode vs Idempotency

Both features help you build reliable integrations, but serve different purposes:

FeatureUse CaseSide EffectsResponse
Test ModeDevelopment, testing, debuggingNone (validation only)Fake/sample data
IdempotencySafe retries, duplicate preventionOnly on first requestReal/cached data

Using Together

// The ultimate safety combo
await client.request('POST', '/v3/messages', {
test_mode: true,           // No side effects
phone_number: '+1234567890',
template_id: 'template-id'
}, {
idempotencyKey: 'test-001' // Safe to retry
});
# The ultimate safety combo
client.request(
'POST',
'/v3/messages',
{
    'test_mode': True,           # No side effects
    'phone_number': '+1234567890',
    'template_id': 'template-id'
},
idempotency_key='test-001'       # Safe to retry
)
// The ultimate safety combo
client.Request("POST", "/v3/messages", map[string]interface{}{
"test_mode":    true,           // No side effects
"phone_number": "+1234567890",
"template_id":  "template-id",
}, "test-001")                      // Safe to retry

Pro Tip: Use test mode during development, idempotency in production. Together, they give you bulletproof reliability.


Troubleshooting

Test Mode Not Working?

Check:

  1. Is test_mode a boolean true (not string "true")?
  2. Is it in the request body (not headers)?
  3. Is the endpoint a mutation (POST/PUT/PATCH/DELETE)?

Getting 401 Errors?

Test mode still requires valid authentication:

# ❌ Won't work — invalid API key
curl -X POST https://api.sent.dm/v3/messages \
  -H "x-api-key: invalid-key" \
  -d '{"test_mode": true, ...}'

# ✅ Works — valid key + test mode
curl -X POST https://api.sent.dm/v3/messages \
  -H "x-api-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  -d '{"test_mode": true, ...}'

On this page