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

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.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

  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
Message finalized as BLOCKED (message.blocked webhook)Insufficient balance or another account-level preconditionAdd 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. 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:

  1. Create a dedicated API key for development so you can rotate or revoke it without touching production. See API keys.
  2. Send with "sandbox": true to validate requests without dispatching messages or incurring charges.
  3. 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:

  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