Message Best Practices
Production-ready patterns for reliable, cost-effective messaging.
Test Mode
Validate requests without sending real messages by adding test_mode: true to your request:
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_1234567890"
},
"test_mode": true
}'const response = await client.messages.send({
to: ['+1234567890'],
template: {
id: 'tmpl_1234567890'
},
testMode: true
});
// Check for test mode header
console.log(response.headers['x-test-mode']); // 'true'response = client.messages.send(
to=["+1234567890"],
template={
"id": "tmpl_1234567890"
},
test_mode=True
)
# Check for test mode header
print(response.headers.get('x-test-mode')) # 'true'response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("tmpl_1234567890"),
},
TestMode: sentdm.Bool(true),
})
// Check for test mode header
fmt.Println(response.Headers["X-Test-Mode"]) // "true"MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("tmpl_1234567890")
.build())
.testMode(true)
.build();
var response = client.messages().send(params);
// Check for test mode header
System.out.println(response.headers().get("X-Test-Mode")); // "true"MessageSendParams parameters = new()
{
To = new List<string> { "+1234567890" },
Template = new MessageSendParamsTemplate
{
Id = "tmpl_1234567890"
},
TestMode = true
};
var response = await client.Messages.SendAsync(parameters);
// Check for test mode header
Console.WriteLine(response.Headers["X-Test-Mode"]); // "true"$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => 'tmpl_1234567890'
],
testMode: true
);
// Check for test mode header
echo $result->headers['X-Test-Mode']; // 'true'result = sent_dm.messages.send(
to: ["+1234567890"],
template: {
id: "tmpl_1234567890"
},
test_mode: true
)
# Check for test mode header
puts result.headers["X-Test-Mode"] # "true"The API validates your request and returns a realistic fake response without executing any side effects:
- No database writes
- No messages sent
- No external API calls
Look for the X-Test-Mode: true header in the response.
When to Use Test Mode
- Validate template variables - Ensure placeholders resolve correctly
- Check recipient formatting - Verify phone numbers are valid
- Verify account permissions - Confirm API key has required access
- Test in CI/CD pipelines - Run integration tests without sending real messages
- Estimate costs - Calculate potential spend before bulk sends
Cost Estimation Example
// Test a bulk send to estimate costs
const testResponse = await client.messages.send({
to: ['+1234567890', '+1987654321', '+1555555555'], // 3 recipients
template: { id: 'tmpl_promo' },
channel: ['whatsapp', 'sms'], // Will create 6 messages
testMode: true
});
console.log(`Would send ${testResponse.data.recipients.length} messages`);
// Calculate cost based on channel/country rates# Test a bulk send to estimate costs
test_response = client.messages.send(
to=["+1234567890", "+1987654321", "+1555555555"], # 3 recipients
template={"id": "tmpl_promo"},
channel=["whatsapp", "sms"], # Will create 6 messages
test_mode=True
)
print(f"Would send {len(test_response.data.recipients)} messages")
# Calculate cost based on channel/country rates// Test a bulk send to estimate costs
testResponse, _ := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
To: []string{"+1234567890", "+1987654321", "+1555555555"}, // 3 recipients
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("tmpl_promo"),
},
Channel: []string{"whatsapp", "sms"}, // Will create 6 messages
TestMode: sentdm.Bool(true),
})
fmt.Printf("Would send %d messages\n", len(testResponse.Data.Recipients))
// Calculate cost based on channel/country ratesBest Practices
1. Always Store Message IDs
Store message IDs for tracking delivery status:
// Store in your database - note: one entry per recipient
const recipients = response.data.recipients;
for (const r of recipients) {
await db.messages.create({
sentMessageId: r.message_id,
recipient: r.to,
channel: r.channel,
templateId: response.data.template_id,
templateName: response.data.template_name,
status: response.data.status
});
}# Store in your database - note: one entry per recipient
for r in response.data.recipients:
db.messages.create({
"sent_message_id": r.message_id,
"recipient": r.to,
"channel": r.channel,
"template_id": response.data.template_id,
"template_name": response.data.template_name,
"status": response.data.status
})// Store in your database - note: one entry per recipient
for _, r := range response.Data.Recipients {
db.Messages.Create(MessageRecord{
SentMessageID: r.MessageID,
Recipient: r.To,
Channel: r.Channel,
TemplateID: response.Data.TemplateID,
TemplateName: response.Data.TemplateName,
Status: response.Data.Status,
})
}// Store in your database - note: one entry per recipient
for (var r : response.data().recipients()) {
db.messages().create(MessageRecord.builder()
.sentMessageId(r.messageId())
.recipient(r.to())
.channel(r.channel())
.templateId(response.data().templateId())
.templateName(response.data().templateName())
.status(response.data().status())
.build());
}// Store in your database - note: one entry per recipient
foreach (var r in response.Data.Recipients)
{
await db.Messages.CreateAsync(new MessageRecord
{
SentMessageId = r.MessageId,
Recipient = r.To,
Channel = r.Channel,
TemplateId = response.Data.TemplateId,
TemplateName = response.Data.TemplateName,
Status = response.Data.Status
});
}// Store in your database - note: one entry per recipient
foreach ($result->data->recipients as $r) {
$db->messages->create([
'sent_message_id' => $r->message_id,
'recipient' => $r->to,
'channel' => $r->channel,
'template_id' => $result->data->template_id,
'template_name' => $result->data->template_name,
'status' => $result->data->status
]);
}# Store in your database - note: one entry per recipient
result.data.recipients.each do |r|
db.messages.create(
sent_message_id: r.message_id,
recipient: r.to,
channel: r.channel,
template_id: result.data.template_id,
template_name: result.data.template_name,
status: result.data.status
)
end2. Implement Idempotency
For critical messages (OTP, payments), use idempotency keys via the Idempotency-Key header:
curl -X POST "https://api.sent.dm/v3/messages" \
-H "x-api-key: $SENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp_user_123_20250115" \
-d '{
"to": ["+1234567890"],
"template": {
"id": "tmpl_otp_template"
}
}'const response = await client.messages.send({
to: ['+1234567890'],
template: {
id: 'tmpl_otp_template'
}
}, {
idempotencyKey: 'otp_user_123_20250115'
});response = client.messages.send(
to=["+1234567890"],
template={
"id": "tmpl_otp_template"
},
idempotency_key="otp_user_123_20250115"
)response, err := client.Messages.Send(
context.Background(),
sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("tmpl_otp_template"),
},
},
option.WithIdempotencyKey("otp_user_123_20250115"),
)MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("tmpl_otp_template")
.build())
.build();
var response = client.messages().send(params, RequestOptions.builder()
.idempotencyKey("otp_user_123_20250115")
.build());MessageSendParams parameters = new()
{
To = new List<string> { "+1234567890" },
Template = new MessageSendParamsTemplate
{
Id = "tmpl_otp_template"
}
};
var response = await client.Messages.SendAsync(
parameters,
new RequestOptions { IdempotencyKey = "otp_user_123_20250115" }
);$result = $client->messages->send(
to: ['+1234567890'],
template: [
'id' => 'tmpl_otp_template'
],
idempotencyKey: 'otp_user_123_20250115'
);result = sent_dm.messages.send(
to: ["+1234567890"],
template: {
id: "tmpl_otp_template"
},
idempotency_key: "otp_user_123_20250115"
)Keys are cached for 24 hours. Reusing a key returns the original response without sending a duplicate message.
3. Handle Failures Gracefully
try {
const response = await client.messages.send({...});
} catch (error) {
if (error.status === 429) {
// Rate limited - implement backoff
await delay(1000);
return retrySend();
}
if (error.code === 'BUSINESS_003') {
// Insufficient balance - alert billing team
await alertBillingTeam();
}
// Log for investigation
logger.error('Message send failed', { error, recipient });
}from sent_dm.errors import RateLimitError, BusinessError
try:
response = client.messages.send(...)
except RateLimitError as e:
# Rate limited - implement backoff
time.sleep(1)
return retry_send()
except BusinessError as e:
if e.code == 'BUSINESS_003':
# Insufficient balance - alert billing team
alert_billing_team()
# Log for investigation
logger.error(f"Message send failed: {e}")response, err := client.Messages.Send(context.Background(), params)
if err != nil {
if sentdm.IsRateLimitError(err) {
// Rate limited - implement backoff
time.Sleep(time.Second)
return retrySend()
}
if sentdm.IsBusinessError(err, "BUSINESS_003") {
// Insufficient balance - alert billing team
alertBillingTeam()
}
// Log for investigation
log.Printf("Message send failed: %v", err)
}try {
var response = client.messages().send(params);
} catch (RateLimitException e) {
// Rate limited - implement backoff
Thread.sleep(1000);
return retrySend();
} catch (BusinessException e) {
if (e.getCode().equals("BUSINESS_003")) {
// Insufficient balance - alert billing team
alertBillingTeam();
}
// Log for investigation
logger.error("Message send failed", e);
}try
{
var response = await client.Messages.SendAsync(parameters);
}
catch (RateLimitException ex)
{
// Rate limited - implement backoff
await Task.Delay(1000);
return await RetrySendAsync();
}
catch (BusinessException ex) when (ex.Code == "BUSINESS_003")
{
// Insufficient balance - alert billing team
await AlertBillingTeamAsync();
}
catch (Exception ex)
{
// Log for investigation
logger.LogError(ex, "Message send failed");
}use SentDM\Exceptions\RateLimitException;
use SentDM\Exceptions\BusinessException;
try {
$result = $client->messages->send(...);
} catch (RateLimitException $e) {
// Rate limited - implement backoff
sleep(1);
return retrySend();
} catch (BusinessException $e) {
if ($e->getCode() === 'BUSINESS_003') {
// Insufficient balance - alert billing team
alertBillingTeam();
}
// Log for investigation
error_log("Message send failed: " . $e->getMessage());
}begin
result = sent_dm.messages.send(...)
rescue Sentdm::RateLimitError => e
# Rate limited - implement backoff
sleep(1)
return retry_send()
rescue Sentdm::BusinessError => e
if e.code == 'BUSINESS_003'
# Insufficient balance - alert billing team
alert_billing_team()
end
# Log for investigation
logger.error("Message send failed: #{e.message}")
end4. Use Webhooks for Status Updates
Don't poll for status. Set up webhooks instead:
// Webhook handler
app.post('/webhooks/sent', async (req, res) => {
const { id, status, to, channel } = req.body;
await db.messages.update({ sentMessageId: id }, {
status: status,
channel: channel,
deliveredAt: status === 'DELIVERED' ? new Date() : null
});
res.sendStatus(200);
});from flask import Flask, request, jsonify
@app.route('/webhooks/sent', methods=['POST'])
def handle_webhook():
data = request.json
db.messages.update(
{'sent_message_id': data['id']},
{
'status': data['status'],
'channel': data['channel'],
'delivered_at': datetime.now() if data['status'] == 'DELIVERED' else None
}
)
return jsonify({'status': 'ok'}), 200import "github.com/gin-gonic/gin"
func webhookHandler(c *gin.Context) {
var payload struct {
ID string `json:"id"`
Status string `json:"status"`
To string `json:"to"`
Channel string `json:"channel"`
}
if err := c.BindJSON(&payload); err != nil {
c.Status(400)
return
}
db.Messages.Update(MessageUpdate{
SentMessageID: payload.ID,
Status: payload.Status,
Channel: payload.Channel,
})
c.Status(200)
}class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def sent
payload = params.permit(:id, :status, :to, :channel)
Message.find_by(sent_message_id: payload[:id])&.update(
status: payload[:status],
channel: payload[:channel],
delivered_at: payload[:status] == 'DELIVERED' ? Time.current : nil
)
head :ok
end
endCost Considerations
Pricing factors:
- Channel: WhatsApp is often cheaper than SMS
- Destination country: International rates vary
- Message length: SMS over 160 chars = multiple segments
- Template type: Utility vs Marketing pricing
Use test mode to validate costs before sending:
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"
},
"test_mode": true
}'const response = await client.messages.send({
to: ['+1234567890'],
template: { id: 'tmpl_123' },
testMode: true
});
// Validated without sending
console.log('Would send to:', response.data.recipients);response = client.messages.send(
to=["+1234567890"],
template={"id": "tmpl_123"},
test_mode=True
)
# Validated without sending
print(f"Would send to: {response.data.recipients}")response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
To: []string{"+1234567890"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("tmpl_123"),
},
TestMode: sentdm.Bool(true),
})
// Validated without sending
fmt.Printf("Would send to: %v\n", response.Data.Recipients)MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("tmpl_123")
.build())
.testMode(true)
.build();
var response = client.messages().send(params);
// Validated without sending
System.out.println("Would send to: " + response.data().recipients());MessageSendParams parameters = new()
{
To = new List<string> { "+1234567890" },
Template = new MessageSendParamsTemplate { Id = "tmpl_123" },
TestMode = true
};
var response = await client.Messages.SendAsync(parameters);
// Validated without sending
Console.WriteLine($"Would send to: {response.Data.Recipients.Count} recipients");$result = $client->messages->send(
to: ['+1234567890'],
template: ['id' => 'tmpl_123'],
testMode: true
);
// Validated without sending
echo "Would send to: " . count($result->data->recipients) . " recipients\n";result = sent_dm.messages.send(
to: ["+1234567890"],
template: { id: "tmpl_123" },
test_mode: true
)
# Validated without sending
puts "Would send to: #{result.data.recipients.length} recipients"Related Guides
Sending Messages
Core message sending guide with all SDK examples
Response Handling
Detailed guide to handling responses and errors
Batch Operations
High-volume sending and contact imports
Webhooks
Real-time delivery notifications