SDK Troubleshooting

Resolve common issues with Sent SDKs. This guide covers frequent errors, debugging strategies, and solutions organized by symptom.

Quick Diagnostics

Before diving into specific issues, check these common causes:

API Key

Verify your API key is correct and not expired

Environment

Check if you're using the right environment (test vs production)

Rate Limits

You may have hit the rate limit - check retry-after headers

Template Status

WhatsApp templates must be APPROVED to send

Common Error Codes

Authentication Errors

401 Unauthorized - Invalid API key

Symptoms:

AuthenticationError: 401 - Invalid API key

Solutions:

  1. Verify your API key from the Sent Dashboard
  2. Check for extra whitespace or copy-paste errors
  3. Ensure you're using the right environment variable (SENT_DM_API_KEY)
  4. Verify the key hasn't been revoked
// Debug: Log first 8 characters
console.log('API Key:', process.env.SENT_DM_API_KEY?.substring(0, 8) + '...');

Contact Errors

404 Not Found - Contact not found

Symptoms:

NotFoundError: 404 - Contact not found

Solutions:

Contacts opted out

Opt-out blocks are asynchronous. POST /v3/messages is accepted with 202 even when every recipient has opted out, so the send call itself never raises an error. Each blocked recipient's message is finalized as FILTERED inside the pipeline, before any provider call, and you are not charged for it.

Detect the block from the message status, not from the send response:

  • The message.filtered webhook event
  • GET /v3/messages/{id}: status is FILTERED
  • GET /v3/messages/{id}/activities: a FILTERED activity

Sent records the reason code ERR_CONSENT_BLOCKED internally and does not return it in API responses or webhook payloads. FILTERED also covers routing deny rules, so read the contact's opt_out field to confirm a consent block. Either way, respect the recipient's preference and do not retry.

// The send is accepted even when the recipient has opted out.
const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    name: 'welcome'
  }
});

const messageId = response.data.recipients[0].message_id;

// The block arrives on the message.filtered webhook. For a one-off check,
// read the status back once the message is finalized.
const message = await client.messages.retrieveStatus(messageId);

if (message.data.status === 'FILTERED') {
  // FILTERED also covers routing deny rules, so confirm the opt-out on the
  // contact record before mirroring it into your own database.
  const contacts = await client.contacts.list({ phone: '+1234567890' });

  if (contacts.data.contacts[0]?.opt_out) {
    await db.users.update({
      where: { phone: '+1234567890' },
      data: { messagingOptOut: true }
    });
    console.log('Recipient opted out, skipping future sends');
  }
}
# The send is accepted even when the recipient has opted out.
response = client.messages.send(
    to=['+1234567890'],
    template={
        'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
        'name': 'welcome'
    }
)

message_id = response.data.recipients[0].message_id

# The block arrives on the message.filtered webhook. For a one-off check,
# read the status back once the message is finalized.
message = client.messages.retrieve_status(message_id)

if message.data.status == 'FILTERED':
    # FILTERED also covers routing deny rules, so confirm the opt-out on the
    # contact record before mirroring it into your own database.
    contacts = client.contacts.list(phone='+1234567890')

    if contacts.data.contacts and contacts.data.contacts[0].opt_out:
        db.users.update(
            phone='+1234567890',
            messaging_opt_out=True
        )
        print('Recipient opted out, skipping future sends')

Template Errors

400 Bad Request - Template not found

Symptoms:

BadRequestError: 400 - Template not found

Solutions:

400 Bad Request - WhatsApp template pending

Symptoms:

BadRequestError: 400 - Template is not approved for WhatsApp

Solutions:

  1. Check template status in dashboard

    • WhatsApp templates need Meta approval (can take hours)
    • SMS templates work immediately
  2. Use SMS as fallback

// Try sending (may throw if template not approved)
try {
  const response = await client.messages.send({
    to: ['+1234567890'],
    template: {
      id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
      name: 'welcome'
    }
  });

  console.log('Sent:', response.data.recipients[0].message_id);
} catch (error) {
  if (error instanceof SentDm.BadRequestError &&
      error.message.includes('not approved')) {
    console.log('Template not approved yet');

    // Queue for later or use alternative channel
    await db.queuedMessages.create({
      phoneNumber: '+1234567890',
      templateId: 'welcome-template',
      retryAfter: new Date(Date.now() + 3600000), // 1 hour
      status: 'pending_approval'
    });
  }
}
from sent_dm import BadRequestError

# Try sending (may raise if template not approved)
try:
    response = client.messages.send(
        to=['+1234567890'],
        template={
            'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
            'name': 'welcome'
        }
    )

    print(f'Sent: {response.data.recipients[0].message_id}')
except BadRequestError as e:
    if 'not approved' in str(e):
        print('Template not approved yet')

        # Queue for later or use alternative channel
        db.queued_messages.create(
            phone_number='+1234567890',
            template_id='welcome-template',
            retry_after=datetime.now() + timedelta(hours=1),
            status='pending_approval'
        )

Rate Limiting

429 Rate Limit - Too many requests

Symptoms:

RateLimitError: 429 - Rate limit exceeded
Retry-After: 60

Solutions:


Billing Errors

Payment Required - Account balance too low

Symptoms:

BadRequestError: 400 - Insufficient credits to send message

Solutions:

  1. Add credits in the dashboard

  2. Graceful degradation

try {
  const response = await client.messages.send({
    to: ['+1234567890'],
    template: {
      id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
      name: 'welcome'
    }
  });
} catch (error) {
  if (error instanceof SentDm.BadRequestError &&
      error.message.includes('credits')) {
    // Queue for later
    await db.messageQueue.create({
      ...messageData,
      status: 'pending_credits'
    });

    await notifyOpsTeam('Account balance low');
  }
}
from sent_dm import BadRequestError

try:
    response = client.messages.send(
        to=['+1234567890'],
        template={
            'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
            'name': 'welcome'
        }
    )
except BadRequestError as e:
    if 'credits' in str(e):
        # Queue for later
        db.message_queue.create(
            status='pending_credits',
            **message_data
        )

        notify_ops_team('Account balance low')

Webhook Issues

Webhook not receiving events

Duplicate webhook events

Webhook events may be delivered multiple times. Handle them idempotently:

async function handleWebhook(event: WebhookEvent) {
  const eventId = event.meta?.request_id || event.id;

  // Check if already processed
  const existing = await db.processedEvents.findUnique({
    where: { eventId }
  });

  if (existing) {
    console.log(`Event ${eventId} already processed`);
    return { received: true };
  }

  // Process event...

  // Mark as processed
  await db.processedEvents.create({
    data: { eventId, processedAt: new Date() }
  });
}

Connection Issues

Timeout errors

Symptoms:

APIConnectionError: Request timeout after 30000ms

Solutions:

  1. Increase timeout
const client = new SentDm({
  timeout: 60000  // 60 seconds
});
from sent_dm import Sent

client = Sent(timeout=60.0)  # 60 seconds
client := sentdm.NewClient(
    option.WithTimeout(60 * time.Second),
)
  1. Check network connectivity
# Test API reachability
curl https://api.sent.dm/v3/health
  1. Implement circuit breaker
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime?: number;
  private readonly threshold = 5;
  private readonly timeout = 60000;

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.isOpen()) {
      throw new Error('Circuit breaker is open');
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private isOpen(): boolean {
    if (this.failures < this.threshold) return false;
    if (!this.lastFailureTime) return false;
    return Date.now() - this.lastFailureTime < this.timeout;
  }

  private onSuccess() {
    this.failures = 0;
  }

  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
  }
}

Debugging Tips

Enable Debug Logging

Most SDKs support debug logging via environment variables:

# Set environment variable
export SENT_LOG=debug

Or configure in code:

const client = new SentDm({
  logLevel: 'debug'  // 'debug', 'info', 'warn', 'error', 'off'
});
# Set environment variable
export SENT_LOG=debug

Or use Python logging:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('sent_dm')
logger.setLevel(logging.DEBUG)
// Use the debug option
client := sentdm.NewClient(
    option.WithDebugLog(nil),
)
# Set the SENT_LOG environment variable
SENT_LOG=debug java -jar myapp.jar

Log Request IDs

Always log information for support tickets:

try {
  const response = await client.messages.send({
    to: ['+1234567890'],
    template: {
      id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
      name: 'welcome'
    }
  });
  console.log('Message sent:', response.data.recipients[0].message_id);
} catch (error) {
  if (error instanceof SentDm.APIError) {
    console.log('Request ID:', error.headers.get('x-request-id'));
    console.log('Status:', error.status);
    console.log('Message:', error.message);
    // Include these in support tickets!
  }
}
from sent_dm import APIStatusError

try:
    response = client.messages.send(
        to=['+1234567890'],
        template={
            'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
            'name': 'welcome'
        }
    )
    print(f'Message sent: {response.data.recipients[0].message_id}')
except APIStatusError as e:
    print(f'Request ID: {e.response.headers.get("x-request-id")}')
    print(f'Status: {e.status_code}')
    print(f'Message: {str(e)}')
    # Include these in support tickets!
response, err := client.Messages.Send(ctx, params)
if err != nil {
    var apiErr *sentdm.Error
    if errors.As(err, &apiErr) {
        fmt.Printf("Request ID: %s\n", apiErr.Response.Header.Get("x-request-id"))
        fmt.Printf("Status: %d\n", apiErr.StatusCode)
        fmt.Printf("Message: %s\n", apiErr.Error())
        // Include these in support tickets!
    }
} else {
    fmt.Printf("Message sent: %s\n", response.Data.Recipients[0].MessageID)
}

Test with cURL

Compare SDK behavior with raw API calls:

# Test authentication
curl -X GET https://api.sent.dm/v3/templates \
  -H "x-api-key: YOUR_API_KEY"

# Test message sending
curl -X POST https://api.sent.dm/v3/messages \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["+1234567890"],
    "template": {"id": "your-template-id"}
  }'

Frequently Asked Questions

Q: Why is a message stuck in "QUEUED" status?

A: Outbound messages start as QUEUED and transition through:

  1. QUEUED → Message accepted and queued for processing
  2. PROCESSED → Message processed and queued for sending
  3. ROUTED → Message routed to a channel provider
  4. SENT → Dispatched to carrier/WhatsApp
  5. DELIVERED → Confirmed delivery
  6. READ → (WhatsApp & RCS) Opened by recipient

Messages that cannot be delivered end in FAILED.

Webhooks are recommended over polling for tracking status changes. If you need to check a single message, you can poll GET /v3/messages/{id} (client.messages.retrieveStatus() in the SDKs).

Q: Can you use the same API key for multiple environments?

A: Yes, but it's not recommended. Use separate keys for different environments and explicitly pass them to the SDK:

// Choose key based on environment
const apiKey = process.env.NODE_ENV === 'production'
  ? process.env.SENT_DM_API_KEY
  : process.env.SENT_DM_API_KEY_TEST;

const client = new SentDm({ apiKey });

This prevents accidental sends from test environments. Note: The SDK only automatically reads SENT_DM_API_KEY - you must implement the environment switching logic yourself.

Q: Why do 401 errors appear in production but not locally?

A: Common causes:

  1. Different API keys (check environment variables)
  2. Key not set in production environment
  3. Key was revoked/rotated
  4. Whitespace or encoding issues

Debug by logging the key prefix:

console.log('Key prefix:', process.env.SENT_DM_API_KEY?.substring(0, 8));

Q: How do you handle webhook failures?

A: Sent retries failed deliveries with exponential backoff, up to your webhook's configured retry count (1–5 attempts, default 3). Once the retry budget is exhausted, the event is marked FAILED and can be inspected in the Sent Dashboard. Ensure your endpoint is idempotent and responds quickly. Refer to handling retries for the full retry model.

Q: Can you send messages from the browser?

A: No. Never expose your API key in client-side code. API keys should only be used server-side. For browser-based messaging, route through your backend API.


Getting Help

If you're still stuck:

  1. Check the API Reference for detailed endpoint documentation
  2. Review SDK Guides for language-specific examples
  3. Contact Support with your request ID:
    • email: support@sent.dm
    • Include: Request ID from error response (in x-request-id header)
    • Include: Timestamp of the failed request
    • Include: Code snippet (remove API keys!)

When contacting support, always include the request ID from the API response headers (x-request-id). This lets Sent support trace the exact request in the delivery logs.


SDK-Specific Error Patterns

Different SDKs handle errors differently. Here's a quick reference:

SDKError PatternKey Exception Types
TypeScriptThrows exceptionsAPIError, BadRequestError, RateLimitError, AuthenticationError
PythonThrows exceptionsAPIError, BadRequestError, RateLimitError, AuthenticationError
GoReturns error value*sentdm.Error with StatusCode, Error(), RawJSON()
JavaThrows exceptionsSentException, BadRequestException, RateLimitException
C#Throws exceptionsSentApiException, SentBadRequestException, SentRateLimitException
PHPThrows exceptionsAPIException, BadRequestException, RateLimitException
RubyThrows exceptionsAPIError, BadRequestError, RateLimitError

On this page