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

CheckDescription
AuthenticationAPI key is valid
TemplateTemplate exists and is accessible
VariablesAll required template parameters provided
RecipientsPhone numbers are valid format
Rate LimitsRequest is within rate limits
IdempotencyKey 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

  1. Go to Activities
  2. Find the failed message
  3. Click for detailed error information
  4. View request/response payloads

4. Common Issues and Solutions

SymptomPossible CauseSolution
401 AUTH_001Invalid API keyVerify key in dashboard
400 VALIDATION_001Invalid request formatCheck request body
404 RESOURCE_002Template not foundVerify template ID
422 VALIDATION_002Invalid phone numberUse E.164 format
402 BUSINESS_003Insufficient balanceAdd funds to account
429 BUSINESS_002Rate limit exceededImplement backoff (200 req/min std)
Message stuck in "QUEUED"KYC pendingComplete verification
Webhook not receivedInvalid URLVerify 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/sent

Webhook 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:

  1. Sign up with a test email
  2. Complete KYC with test business details
  3. Use test phone numbers (see below)
  4. Set low balance alerts

Test Phone Numbers

NumberBehavior
+15005550000Always succeeds
+15005550001Always fails
+15005550006Invalid 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:

  1. Check the Error Catalog - Detailed error codes
  2. Review Troubleshooting Guide - Common issues
  3. 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

On this page