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 Mode | With 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:
- Authentication runs — Invalid API keys are still rejected
- Validation runs — Malformed payloads return real errors
- Permissions checked — Authorization rules still apply
- Response returned — A realistic fake response with sample data
- 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:integrationDebugging 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:
| Endpoint | Test Behavior |
|---|---|
POST /v3/messages | Returns fake message object, no SMS/WhatsApp sent |
POST /v3/contacts | Returns fake contact, no database write |
PATCH /v3/contacts/{id} | Returns fake updated contact |
DELETE /v3/contacts/{id} | Returns 204, contact not deleted |
POST /v3/templates | Returns fake template with "PENDING" status |
PUT /v3/templates/{id} | Returns fake updated template |
DELETE /v3/templates/{id} | Returns 204, template not deleted |
POST /v3/webhooks | Returns fake webhook with random secret |
POST /v3/profiles | Returns fake profile |
POST /v3/brands | Returns 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.shDebug 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:
| Feature | Use Case | Side Effects | Response |
|---|---|---|---|
| Test Mode | Development, testing, debugging | None (validation only) | Fake/sample data |
| Idempotency | Safe retries, duplicate prevention | Only on first request | Real/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 retryPro Tip: Use test mode during development, idempotency in production. Together, they give you bulletproof reliability.
Troubleshooting
Test Mode Not Working?
Check:
- Is
test_modea booleantrue(not string"true")? - Is it in the request body (not headers)?
- 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, ...}'