SDK Best Practices
Build production-ready messaging integrations with Sent 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')
raiseRetry 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
| Language | Method Style | Example |
|---|---|---|
| TypeScript | camelCase | client.webhooks.verifySignature() |
| Python | snake_case | client.webhooks.verify_signature() |
| Go | PascalCase (exported) | client.Webhooks.VerifySignature() |
| Java | camelCase | client.webhooks().verifySignature() |
| C# | PascalCase | client.Webhooks.VerifySignature() |
| PHP | camelCase | $client->webhooks->verifySignature() |
| Ruby | snake_case | client.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 != nilbefore 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()
)
raiseEnvironment 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.