SDK Best Practices

Build production-ready messaging integrations with Sent LogoSent SDKs. This guide covers patterns for error handling, retries, testing, and security that apply across all languages.

These practices are recommended for production deployments. For quick prototyping, the basic SDK usage shown in language-specific guides is sufficient.

Error Handling Strategy

Handle Errors by Exception Type (TypeScript, Python, Java, C#)

Most SDKs throw exceptions for errors. Catch specific exception types:

import SentDm from '@sentdm/sentdm';

const client = new SentDm();

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

  console.log(`Sent: ${response.data.messages[0].id}`);
} catch (error) {
  if (error instanceof SentDm.BadRequestError) {
    console.error('Invalid request:', error.message);
  } else if (error instanceof SentDm.RateLimitError) {
    console.error('Rate limited. Retry after:', error.headers['retry-after']);
  } else if (error instanceof SentDm.AuthenticationError) {
    console.error('Invalid API key');
  } else if (error instanceof SentDm.APIError) {
    console.error(`API Error ${error.status}:`, error.message);
  } else {
    console.error('Unexpected error:', error);
  }
}
import sent_dm
from sent_dm import SentDm

client = SentDm()

try:
    response = client.messages.send(
        to=["+1234567890"],
        template={
            "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
            "name": "welcome"
        }
    )
    print(f"Sent: {response.data.messages[0].id}")
except sent_dm.BadRequestError as e:
    print(f"Invalid request: {e.message}")
except sent_dm.RateLimitError as e:
    print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}")
except sent_dm.AuthenticationError as e:
    print("Invalid API key")
except sent_dm.APIStatusError as e:
    print(f"API Error {e.status_code}: {e.message}")
except sent_dm.APIError as e:
    print(f"Unexpected error: {e}")
import dm.sent.client.SentDmClient;
import dm.sent.client.okhttp.SentDmOkHttpClient;
import dm.sent.exceptions.BadRequestException;
import dm.sent.exceptions.RateLimitException;
import dm.sent.exceptions.AuthenticationException;

SentDmClient client = SentDmOkHttpClient.fromEnv();

try {
    MessageSendParams params = MessageSendParams.builder()
        .addTo("+1234567890")
        .template(MessageSendParams.Template.builder()
            .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
            .name("welcome")
            .build())
        .build();

    var response = client.messages().send(params);
    System.out.println("Sent: " + response.data().messages().get(0).id());
} catch (BadRequestException e) {
    System.err.println("Invalid request: " + e.getMessage());
} catch (RateLimitException e) {
    System.err.println("Rate limited. Retry after: " + e.retryAfter());
} catch (AuthenticationException e) {
    System.err.println("Invalid API key");
} catch (SentDmException e) {
    System.err.println("API Error: " + e.getMessage());
}
using Sentdm;

SentDmClient client = new();

try {
    MessageSendParams parameters = new()
    {
        To = new List<string> { "+1234567890" },
        Template = new MessageSendParamsTemplate
        {
            Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
            Name = "welcome"
        }
    };

    var response = await client.Messages.SendAsync(parameters);
    Console.WriteLine($"Sent: {response.Data.Messages[0].Id}");
} catch (SentDmBadRequestException e) {
    Console.WriteLine($"Invalid request: {e.Message}");
} catch (SentDmRateLimitException e) {
    Console.WriteLine("Rate limited");
} catch (SentDmUnauthorizedException e) {
    Console.WriteLine("Invalid API key");
} catch (SentDmApiException e) {
    Console.WriteLine($"API Error: {e.Message}");
}
import (
    "errors"
    "github.com/sentdm/sent-dm-go"
)

client := sentdm.NewClient()

response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
    To: []string{"+1234567890"},
    Template: sentdm.MessageSendParamsTemplate{
        ID:   sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
        Name: sentdm.String("welcome"),
    },
})
if err != nil {
    var apiErr *sentdm.Error
    if errors.As(err, &apiErr) {
        switch apiErr.StatusCode {
        case 400:
            fmt.Println("Invalid request:", apiErr.Message)
        case 429:
            fmt.Println("Rate limited")
        case 401:
            fmt.Println("Invalid API key")
        default:
            fmt.Printf("API Error %d: %s\n", apiErr.StatusCode, apiErr.Message)
        }
    } else {
        fmt.Println("Network error:", err)
    }
} else {
    fmt.Printf("Sent: %s\n", response.Data.Messages[0].ID)
}

Handle Specific Error Codes

Different errors require different handling strategies:

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

  console.log(`Sent: ${response.id}`);
} catch (error) {
  if (error instanceof SentDm.RateLimitError) {
    // Back off and retry
    const retryAfter = parseInt(error.headers['retry-after'] || '60', 10);
    await delay(retryAfter * 1000);
    return retry();
  }

  if (error instanceof SentDm.BadRequestError) {
    // Check specific error code if available in message
    if (error.message.includes('TEMPLATE_001')) {
      // Template not found - check template ID
      console.error('Template not found');
    } else if (error.message.includes('INSUFFICIENT_CREDITS')) {
      // Alert operations team
      await alertOpsTeam('Account balance low');
    }
  }

  throw error;
}
import time
import sent_dm

try:
    response = client.messages.send(
        to=["+1234567890"],
        template={
            "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
            "name": "welcome"
        }
    )
    print(f"Sent: {response.data.messages[0].id}")
except sent_dm.RateLimitError as e:
    # Back off and retry
    retry_after = int(e.response.headers.get('retry-after', 60))
    time.sleep(retry_after)
    return retry()
except sent_dm.BadRequestError as e:
    # Check specific error in message
    if 'TEMPLATE_001' in str(e):
        print("Template not found")
    elif 'INSUFFICIENT_CREDITS' in str(e):
        await alert_ops_team('Account balance low')
    raise

Retry Strategies

Use Built-in Retries

All SDKs have built-in retry logic with exponential backoff:

// Configure max retries (default is 2)
const client = new SentDm({
  maxRetries: 3  // Retry up to 3 times
});

// Or per-request
await client.messages.send(params, {
  maxRetries: 5
});
from sent_dm import SentDm

# Configure max retries (default is 2)
client = SentDm(max_retries=3)

# Or per-request
client.with_options(max_retries=5).messages.send(
    to=["+1234567890"],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        "name": "welcome"
    }
)
// Configure max retries (default is 2)
client := sentdm.NewClient(
    option.WithMaxRetries(3),
)
SentDmClient client = SentDmOkHttpClient.builder()
    .maxRetries(3)
    .build();

Custom Retry Logic

For application-specific retry logic:

async function sendWithRetry(
  client: SentDm,
  params: SentDm.MessageSendParams,
  maxRetries = 3
): Promise<SentDm.Message> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await client.messages.send(params);
    } catch (error) {
      // Don't retry client errors (4xx) except rate limit
      if (error instanceof SentDm.BadRequestError &&
          !(error instanceof SentDm.RateLimitError)) {
        throw error;
      }

      // Don't retry on last attempt
      if (attempt === maxRetries) {
        throw error;
      }

      // Exponential backoff: 1s, 2s, 4s
      const delayMs = Math.min(1000 * Math.pow(2, attempt), 10000);
      await sleep(delayMs);
    }
  }

  throw new Error('Unreachable');
}
import time
from sent_dm import SentDm

def send_with_retry(
    client: SentDm,
    to: list[str],
    template: dict,
    max_retries: int = 3
):
    for attempt in range(max_retries + 1):
        try:
            return client.messages.send(
                to=to,
                template=template
            )
        except Exception as e:
            # Don't retry client errors (4xx) except rate limit
            if hasattr(e, 'status_code') and e.status_code == 429:
                pass  # Will retry
            elif hasattr(e, 'status_code') and 400 <= e.status_code < 500:
                raise

            # Don't retry on last attempt
            if attempt == max_retries:
                raise

            # Exponential backoff: 1s, 2s, 4s
            delay_ms = min(1000 * (2 ** attempt), 10000)
            time.sleep(delay_ms / 1000)

Testing Strategies

Use Test Mode

Always use test_mode in development and CI/CD:

All SDKs support test_mode parameter to validate requests without sending real messages. Use this in development and CI/CD environments.

// Enable test mode to validate without sending
const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    name: 'welcome'
  },
  testMode: true  // Validates but doesn't send
});
# Enable test mode to validate without sending
response = client.messages.send(
    to=["+1234567890"],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        "name": "welcome"
    },
    test_mode=True  # Validates but doesn't send
)
// Enable test mode to validate without sending
response, err := client.Messages.Send(ctx, sentdm.MessageSendParams{
    To: []string{"+1234567890"},
    Template: sentdm.MessageSendParamsTemplate{
        ID:   sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
        Name: sentdm.String("welcome"),
    },
    TestMode: sentdm.Bool(true), // Validates but doesn't send
})
// Enable test mode to validate without sending
MessageSendParams params = MessageSendParams.builder()
    .addTo("+1234567890")
    .template(MessageSendParams.Template.builder()
        .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
        .name("welcome")
        .build())
    .testMode(true) // Validates but doesn't send
    .build();

var response = client.messages().send(params);
// Enable test mode to validate without sending
MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Name = "welcome"
    },
    TestMode = true // Validates but doesn't send
};

var response = await client.Messages.SendAsync(parameters);
// Enable test mode to validate without sending
$result = $client->messages->send(
    to: ['+1234567890'],
    template: [
        'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
        'name' => 'welcome'
    ],
    testMode: true // Validates but doesn't send
);

Mock the SDK in Unit Tests

Don't make real API calls in unit tests:

// jest.mock example
jest.mock('@sentdm/sentdm', () => ({
  default: jest.fn().mockImplementation(() => ({
    messages: {
      send: jest.fn().mockResolvedValue({
        data: {
          messages: [{ id: 'msg_123', status: 'pending' }]
        }
      })
    }
  }))
}));

// Test your business logic
it('should send welcome message on signup', async () => {
  await userService.signup({ phone: '+1234567890' });

  // Verify the SDK method was called
  expect(mockSend).toHaveBeenCalledWith({
    to: ['+1234567890'],
    template: {
      id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
      name: 'welcome'
    }
  });
});
# pytest example with unittest.mock
from unittest.mock import Mock, patch

def test_send_welcome_message():
    mock_client = Mock()
    mock_client.messages.send.return_value = Mock(
        data=Mock(
            messages=[Mock(id='msg_123', status='pending')]
        )
    )

    with patch('myapp.services.SentDm', return_value=mock_client):
        user_service.signup(phone='+1234567890')

        mock_client.messages.send.assert_called_with(
            to=['+1234567890'],
            template={
                'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
                'name': 'welcome'
            }
        )

Language-Specific API Patterns

Each SDK follows the idiomatic patterns of its language. Here are the key differences:

Method Naming Conventions

LanguageMethod StyleExample
TypeScriptcamelCaseclient.webhooks.verifySignature()
Pythonsnake_caseclient.webhooks.verify_signature()
GoPascalCase (exported)client.Webhooks.VerifySignature()
JavacamelCaseclient.webhooks().verifySignature()
C#PascalCaseclient.Webhooks.VerifySignature()
PHPcamelCase$client->webhooks->verifySignature()
Rubysnake_caseclient.webhooks.verify_signature

Error Handling Patterns

Exception-based (TypeScript, Python, Java, C#, PHP, Ruby):

  • SDKs throw exceptions for API errors
  • Catch specific exception types for different handling
  • Network errors throw connection exceptions

Error return (Go):

  • Go returns errors as second value
  • Check err != nil before using response
  • API errors are typed errors

Webhook Security

Always Verify Signatures

Never process webhooks without verifying the signature. Unsigned webhooks could be spoofed.

import SentDm from '@sentdm/sentdm';

const client = new SentDm();

app.post('/webhooks/sent', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'] as string;
  const payload = req.body.toString();

  // Verify using SDK
  const isValid = client.webhooks.verifySignature({
    payload,
    signature,
    secret: process.env.SENT_DM_WEBHOOK_SECRET!
  });

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook
  const event = JSON.parse(payload);
  processWebhook(event);

  res.json({ received: true });
});
@app.route('/webhooks/sent', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    payload = request.get_data(as_text=True)

    # Verify using SDK
    is_valid = client.webhooks.verify_signature(
        payload=payload,
        signature=signature,
        secret=os.environ['SENT_DM_WEBHOOK_SECRET']
    )

    if not is_valid:
        return jsonify({'error': 'Invalid signature'}), 401

    # Process webhook
    event = request.get_json()
    process_webhook(event)

    return jsonify({'received': True})
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")

    body, _ := io.ReadAll(r.Body)
    payload := string(body)

    // Verify using SDK
    isValid := client.Webhooks.VerifySignature(payload, signature, webhookSecret)

    if !isValid {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Process webhook
    processWebhook(payload)

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

Handle Webhooks Idempotently

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

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

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

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

  // Process event
  await handleEvent(event);

  // Record as processed
  await db.webhookEvents.create({
    data: {
      eventId,
      type: event.type,
      processedAt: new Date()
    }
  });

  return { received: true };
}

Respond Quickly

Webhook handlers should respond quickly to avoid timeouts:

app.post('/webhooks/sent', async (req, res) => {
  // Verify signature
  if (!isValidSignature(req)) {
    return res.status(401).end();
  }

  // Respond immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhook(req.body).catch(err => {
    logger.error('Webhook processing failed', err);
  });
});

Performance Optimization

Reuse Client Instances

Create one client instance and reuse it across your application. Don't create a new client for each request.

// config/sent.ts
import SentDm from '@sentdm/sentdm';

// Create once
export const sentClient = new SentDm();

// Use everywhere
import { sentClient } from './config/sent';

export async function sendMessage(params) {
  return sentClient.messages.send(params);
}
# config/sent.py
from sent_dm import SentDm

# Create once
sent_client = SentDm()

# Use everywhere
from config.sent import sent_client

def send_message(phone_number, template_id):
    return sent_client.messages.send(
        to=[phone_number],
        template={
            "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
            "name": template_id
        }
    )
// config/sent.go
var Client *sentdm.Client

func init() {
    Client = sentdm.NewClient()
}

// Use everywhere
import "myapp/config"

func sendMessage(params sentdm.MessageSendParams) error {
    _, err := config.Client.Messages.Send(ctx, params)
    return err
}
// config/SentConfig.java
@Configuration
public class SentConfig {

    @Bean
    public SentDmClient sentClient() {
        return SentDmOkHttpClient.fromEnv();
    }
}

// Use via injection
@Service
public class MessageService {
    private final SentDmClient client;

    public MessageService(SentDmClient client) {
        this.client = client;
    }
}

Store Message IDs

Always store message IDs for tracking:

async function sendOrderConfirmation(order: Order) {
  try {
    const response = await client.messages.send({
      to: [order.customer.phone],
      template: {
        id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
        name: 'order-confirmation',
        parameters: { order_id: order.id }
      }
    });

    // Store for webhook correlation
    await db.messages.create({
      sentMessageId: response.data.messages[0].id,
      orderId: order.id,
      status: response.data.messages[0].status,
      sentAt: new Date()
    });

    return response;
  } catch (error) {
    // Handle error
    await db.messages.create({
      orderId: order.id,
      status: 'failed',
      error: error.message,
      sentAt: new Date()
    });
    throw error;
  }
}
def send_order_confirmation(order):
    try:
        response = client.messages.send(
            to=[order.customer.phone],
            template={
                'id': '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
                'name': 'order-confirmation',
                'parameters': {'order_id': order.id}
            }
        )

        # Store for webhook correlation
        db.messages.create(
            sent_message_id=response.data.messages[0].id,
            order_id=order.id,
            status=response.data.messages[0].status,
            sent_at=datetime.now()
        )

        return response
    except Exception as e:
        # Handle error
        db.messages.create(
            order_id=order.id,
            status='failed',
            error=str(e),
            sent_at=datetime.now()
        )
        raise

Environment Management

Separate Credentials by Environment

// config/sent.ts
const configs = {
  development: {
    apiKey: process.env.SENT_DM_API_KEY_TEST
  },
  staging: {
    apiKey: process.env.SENT_DM_API_KEY_TEST
  },
  production: {
    apiKey: process.env.SENT_DM_API_KEY
  }
};

const env = (process.env.NODE_ENV as keyof typeof configs) || 'development';
export const sentClient = new SentDm(configs[env]);

Validate Configuration on Startup

function validateSentConfig() {
  if (!process.env.SENT_DM_API_KEY) {
    throw new Error('SENT_DM_API_KEY is required');
  }

  if (!process.env.SENT_DM_WEBHOOK_SECRET) {
    console.warn('SENT_DM_WEBHOOK_SECRET not set - webhooks will fail');
  }
}

// Run on application startup
validateSentConfig();

Summary

Error Handling

Catch specific exceptions, handle different error types appropriately

Testing

Use test_mode in development, mock SDK in unit tests

Security

Verify webhook signatures, use HTTPS, rotate secrets

Performance

Reuse client instances, store message IDs


Following these practices ensures your Sent integration is reliable, secure, and maintainable at scale.

On this page