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": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
},
"sandbox": true
}'const response = await client.messages.send({
to: ['+1234567890'],
template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' },
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": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"},
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("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
},
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": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"template_name": "welcome_message",
"recipients": [
{
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"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.retrieveStatus(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 |
Message finalized as BLOCKED (message.blocked webhook) | Insufficient balance or another account-level precondition | 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. For the production handler implementations under test, refer to Message Status Tracking (status events) and Two-Way Conversations (inbound messages):
// webhook.test.ts
const mockEvent = {
field: 'message',
event: 'message.delivered',
timestamp: new Date().toISOString(),
payload: {
updated_at: new Date().toISOString(),
account_id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
message_id: '8ba7b830-9dad-11d1-80b4-00c04fd430c8',
template_id: '9ba7b840-9dad-11d1-80b4-00c04fd430c8',
template_name: 'order_confirmation',
outbound_number: '+1987654321',
message_status: 'DELIVERED',
channel: 'sms'
}
};
describe('Webhook Handler', () => {
it('should handle message delivered event', async () => {
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 rawBody = JSON.stringify(mockEvent);
const webhookId = '550e8400-e29b-41d4-a716-446655440000';
const timestamp = Math.floor(Date.now() / 1000).toString();
// HMAC-SHA256 over `${webhookId}.${timestamp}.${rawBody}`, formatted as `v1,{base64}`
const signature = generateSignature(webhookId, timestamp, rawBody, WEBHOOK_SECRET);
await request(app)
.post('/webhooks/sent')
.set('X-Webhook-ID', webhookId)
.set('X-Webhook-Timestamp', timestamp)
.set('X-Webhook-Signature', signature)
.set('Content-Type', 'application/json')
.send(rawBody)
.expect(200);
});
});Refer to Webhook Security for the full signature scheme and a verification implementation.
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": "message",
"event": "message.delivered",
"timestamp": "2025-01-15T08:30:15Z",
"payload": {
"updated_at": "2025-01-15T08:30:15Z",
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"template_id": "9ba7b840-9dad-11d1-80b4-00c04fd430c8",
"template_name": "order_confirmation",
"outbound_number": "+1987654321",
"message_status": "DELIVERED",
"channel": "sms"
}
}'Test Environment
Isolated Testing
Sent does not provide a separate test environment, and KYC verification requires real business details: placeholder details lead to a rejected verification. Follow the supported process in Account Setup, then keep test traffic isolated on your verified account:
- Create a dedicated API key for development so you can rotate or revoke it without touching production. See API keys.
- Send with
"sandbox": trueto validate requests without dispatching messages or incurring charges. - For end-to-end delivery tests, send real (billed) messages to phone numbers your team controls.
Sandbox requests never dispatch a message, so any validly formatted E.164 recipient works. Sent has no reserved test phone numbers with simulated delivery outcomes.
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
How to handle Sent API rate limits
Keep your integration under Sent API v3 rate limits: back off on 429 responses, honor Retry-After, throttle requests client-side, and pace batch workloads.
Testing with Sandbox Mode
How to test a Sent integration with sandbox mode: assert against real API responses, run CI suites without sending messages, and reproduce failures safely.