Testing & Debugging
Validate your integration, debug issues, and ensure reliable message delivery before going to production.
Sandbox Mode
Use sandbox mode to validate requests without sending real messages or incurring charges.
Enabling Sandbox Mode
Add "sandbox": true to the request body to validate without side effects:
curl -X POST "https://api.sent.dm/v3/messages" \
-H "x-api-key: $SENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["+1234567890"],
"template": {
"id": "tmpl_123"
},
"sandbox": true
}'const response = await client.messages.send({
to: ['+1234567890'],
template: { id: 'tmpl_123' },
sandbox: true
});
// Sandbox mode returns realistic response without side effects
console.log('Status:', response.data.status);
console.log('Recipients:', response.data.recipients);
console.log('Sandbox mode:', response.headers['X-Sandbox']);response = client.messages.send(
to=["+1234567890"],
template={"id": "tmpl_123"},
sandbox=True
)
# Sandbox mode returns realistic response without side effects
print(f"Status: {response.data.status}")
print(f"Recipients: {response.data.recipients}")response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("tmpl_123"),
},
Sandbox: sentdm.Bool(true),
})Sandbox Mode Response
The API returns a realistic response (202 Accepted) without executing side effects. Check the X-Sandbox: true header:
{
"success": true,
"data": {
"status": "QUEUED",
"template_id": "tmpl_123",
"template_name": "welcome_message",
"recipients": [
{
"message_id": "msg_test_1234567890",
"to": "+1234567890",
"channel": "sms"
}
]
},
"error": null,
"meta": {
"request_id": "req_test_001",
"timestamp": "2026-03-04T11:28:25.2096416+00:00",
"version": "v3"
}
}What Sandbox Mode Validates
| Check | Description |
|---|---|
| Authentication | API key is valid |
| Template | Template exists and is accessible |
| Variables | All required template parameters provided |
| Recipients | Phone numbers are valid format |
| Rate Limits | Request is within rate limits |
| Idempotency | Key uniqueness (if provided) |
Sandbox mode returns 202 Accepted with realistic fake data. No database writes, no messages sent, no external API calls.
Testing Scenarios
Unit Testing
Test your message sending logic in isolation:
// messageService.test.ts
import { describe, it, expect, vi } from 'vitest';
import { sendWelcomeMessage } from './messageService';
describe('sendWelcomeMessage', () => {
it('should send welcome message successfully', async () => {
const result = await sendWelcomeMessage('+1234567890', 'John');
expect(result.success).toBe(true);
expect(result.messageId).toBeDefined();
});
it('should handle invalid phone number', async () => {
const result = await sendWelcomeMessage('invalid', 'John');
expect(result.success).toBe(false);
expect(result.error).toContain('phone number');
});
it('should use sandbox mode in development', async () => {
process.env.NODE_ENV = 'development';
const result = await sendWelcomeMessage('+1234567890', 'John');
expect(result.sandbox).toBe(true);
});
});Integration Testing
Test the full flow with the actual API:
// integration.test.ts
describe('Sent API Integration', () => {
it('should send and track message delivery', async () => {
// Send message
const sendResult = await client.messages.send({
to: [TEST_PHONE_NUMBER],
template: { id: TEST_TEMPLATE_ID },
sandbox: true
});
expect(sendResult.data.recipients[0].message_id).toBeDefined();
// Query status
const messageId = sendResult.data.recipients[0].message_id;
const statusResult = await client.messages.get(messageId);
expect(statusResult.data.status).toBe('QUEUED');
});
});Load Testing
Test your integration under load:
// loadTest.ts
async function loadTest() {
const concurrency = 10;
const totalMessages = 100;
const startTime = Date.now();
// Send messages in batches
for (let i = 0; i < totalMessages; i += concurrency) {
const batch = Array(concurrency).fill(null).map((_, j) =>
client.messages.send({
to: [TEST_PHONE_NUMBER],
template: { id: TEST_TEMPLATE_ID },
sandbox: true
})
);
await Promise.all(batch);
console.log(`Sent batch ${i / concurrency + 1}`);
}
const duration = Date.now() - startTime;
const rate = totalMessages / (duration / 1000);
console.log(`Rate: ${rate.toFixed(2)} messages/second`);
}Debugging Failed Messages
1. Check Response Details
try {
const response = await client.messages.send({...});
} catch (error) {
console.log('Status:', error.status);
console.log('Code:', error.code);
console.log('Message:', error.message);
console.log('Request ID:', error.meta?.request_id); // For support tickets
}2. Enable Debug Logging
const client = new SentDm({
logLevel: 'debug'
});
// Or via environment variable
process.env.SENT_DM_LOG = 'debug';3. Use the Dashboard
- Go to Activities
- Find the failed message
- Click for detailed error information
- View request/response payloads
4. Common Issues and Solutions
| Symptom | Possible Cause | Solution |
|---|---|---|
401 AUTH_001 | Invalid API key | Verify key in dashboard |
400 VALIDATION_001 | Invalid request format | Check request body |
404 RESOURCE_002 | Template not found | Verify template ID |
422 VALIDATION_002 | Invalid phone number | Use E.164 format |
402 BUSINESS_003 | Insufficient balance | Add funds to account |
429 BUSINESS_002 | Rate limit exceeded | Implement backoff (200 req/min std) |
| Message stuck in "QUEUED" | KYC pending | Complete verification |
| Webhook not received | Invalid URL | Verify endpoint returns 2xx |
Webhook Testing
Local Development with ngrok
# Start your local server
npm run dev # Running on http://localhost:3000
# Create tunnel
npx ngrok http 3000
# Use the HTTPS URL in webhook configuration
# https://abc123.ngrok.io/webhooks/sentWebhook Testing Tools
Test webhook handlers without sending real messages:
// webhook.test.ts
describe('Webhook Handler', () => {
it('should handle message delivered event', async () => {
const mockEvent = {
field: 'messages',
timestamp: new Date().toISOString(),
payload: {
account_id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
message_id: 'msg_123',
message_status: 'DELIVERED',
channel: 'sms',
inbound_number: '+1234567890',
outbound_number: '+1987654321',
template_id: '9ba7b840-9dad-11d1-80b4-00c04fd430c8'
}
};
const response = await request(app)
.post('/webhooks/sent')
.send(mockEvent)
.expect(200);
// Verify database was updated
const message = await db.messages.findById(mockEvent.payload.message_id);
expect(message.status).toBe('DELIVERED');
});
it('should verify webhook signature', async () => {
const mockEvent = {
field: 'messages',
timestamp: new Date().toISOString(),
payload: {
account_id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
message_id: 'msg_123',
message_status: 'DELIVERED',
channel: 'sms',
inbound_number: '+1234567890',
outbound_number: '+1987654321',
template_id: '9ba7b840-9dad-11d1-80b4-00c04fd430c8'
}
};
const signature = generateSignature(mockEvent, WEBHOOK_SECRET);
const response = await request(app)
.post('/webhooks/sent')
.set('X-Signature', signature)
.send(mockEvent)
.expect(200);
});
});
});Replay Webhook Events
# Using curl to replay a webhook
curl -X POST http://localhost:3000/webhooks/sent \
-H "Content-Type: application/json" \
-H "X-Webhook-ID: test-event-id" \
-H "X-Webhook-Timestamp: $(date +%s)" \
-H "X-Webhook-Signature: v1,<signature>" \
-d '{
"field": "messages",
"timestamp": "2025-01-15T08:30:15Z",
"payload": {
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"message_status": "DELIVERED",
"channel": "sms",
"inbound_number": "+1234567890",
"outbound_number": "+1987654321"
}
}'Test Environment
Isolated Testing
Create a separate account for testing:
- Sign up with a test email
- Complete KYC with test business details
- Use test phone numbers (see below)
- Set low balance alerts
Test Phone Numbers
| Number | Behavior |
|---|---|
+15005550000 | Always succeeds |
+15005550001 | Always fails |
+15005550006 | Invalid number format |
These test numbers only work in sandbox mode. Do not use in production.
Monitoring in Production
Health Checks
// healthcheck.ts
async function healthCheck() {
try {
// Test API connectivity
const response = await client.templates.list({ limit: 1 });
return {
status: 'healthy',
api: 'connected',
timestamp: new Date().toISOString()
};
} catch (error) {
return {
status: 'unhealthy',
api: 'disconnected',
error: error.message
};
}
}Metrics to Track
// Track key metrics
metrics.histogram('message_send_duration');
metrics.counter('message_send_total', { status: 'success' });
metrics.counter('message_send_total', { status: 'failed' });
metrics.gauge('messages_queued');Troubleshooting Checklist
When something isn't working:
- Check API key is correct and active
- Verify KYC is approved
- Confirm account has sufficient balance
- Check template exists and is approved
- Validate phone number format (E.164)
- Review rate limit status
- Check webhook endpoint is responding 200
- Enable debug logging
- Test in sandbox mode first
- Review dashboard activity logs
Support Resources
If you're stuck:
- Check the Error Catalog - Detailed error codes
- Review Troubleshooting Guide - Common issues
- Contact Support - Email support@sent.dm with:
- Request ID (from error response)
- Timestamp of issue
- Code snippet (with API keys removed)
- Expected vs actual behavior