Ruby SDK
The official Ruby SDK for Sent provides an elegant, Ruby-idiomatic interface for sending messages. Built with love for Rails applications, featuring comprehensive types & docstrings in Yard, RBS, and RBI.
Requirements
Ruby 3.2.0 or later.
Installation
To use this gem, install via Bundler by adding the following to your application's Gemfile:
gem "sentdm", "~> 0.13.0"Then run:
bundle installOr install directly:
gem install sentdmQuick Start
Initialize the client
require "sentdm"
sent_dm = Sentdm::Client.new(
api_key: ENV["SENT_DM_API_KEY"] # This is the default and can be omitted
)Send your first message
require "sentdm"
sent_dm = Sentdm::Client.new
result = sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome",
parameters: {
name: "John Doe",
order_id: "12345"
}
},
channel: ["sms", "whatsapp"]
)
puts(result.data.messages[0].id)
puts(result.data.messages[0].status)Authentication
The client reads SENT_DM_API_KEY from the environment by default, or you can pass it explicitly:
require "sentdm"
# Using environment variables
sent_dm = Sentdm::Client.new
# Or explicit configuration
sent_dm = Sentdm::Client.new(
api_key: "your_api_key"
)Send Messages
The Ruby SDK uses send_ (with trailing underscore) instead of send because send is a reserved method in Ruby's Object class.
Send a message
result = sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome",
parameters: {
name: "John Doe",
order_id: "12345"
}
},
channel: ["sms", "whatsapp"]
)
puts(result.data.messages[0].id)
puts(result.data.messages[0].status)Test mode
Use test_mode: true to validate requests without sending real messages:
result = sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome"
},
test_mode: true # Validates but doesn't send
)
# Response will have test data
puts(result.data.messages[0].id)
puts(result.data.messages[0].status)Handling errors
When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of Sentdm::Errors::APIError will be thrown:
begin
result = sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome",
parameters: {name: "John Doe", order_id: "12345"}
}
)
rescue Sentdm::Errors::APIConnectionError => e
puts("The server could not be reached")
puts(e.cause) # an underlying Exception, likely raised within `net/http`
rescue Sentdm::Errors::RateLimitError => e
puts("A 429 status code was received; we should back off a bit.")
rescue Sentdm::Errors::APIStatusError => e
puts("Another non-200-range status code was received")
puts(e.status)
endError codes are as follows:
| Cause | Error Type |
|---|---|
| HTTP 400 | BadRequestError |
| HTTP 401 | AuthenticationError |
| HTTP 403 | PermissionDeniedError |
| HTTP 404 | NotFoundError |
| HTTP 409 | ConflictError |
| HTTP 422 | UnprocessableEntityError |
| HTTP 429 | RateLimitError |
| HTTP >= 500 | InternalServerError |
| Other HTTP error | APIStatusError |
| Timeout | APITimeoutError |
| Network error | APIConnectionError |
Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
# Configure the default for all requests:
sent_dm = Sentdm::Client.new(
max_retries: 0 # default is 2
)
# Or, configure per-request:
sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome"
},
request_options: {max_retries: 5}
)Timeouts
By default, requests will time out after 60 seconds.
# Configure the default for all requests:
sent_dm = Sentdm::Client.new(
timeout: nil # default is 60
)
# Or, configure per-request:
sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome"
},
request_options: {timeout: 5}
)BaseModel
All parameter and response objects inherit from Sentdm::Internal::Type::BaseModel, which provides several conveniences:
- All fields, including unknown ones, are accessible with
obj[:prop]syntax - Structural equivalence for equality
- Both instances and classes can be pretty-printed
- Helpers such as
#to_h,#deep_to_h,#to_json, and#to_yaml
result = sent_dm.templates.list
# Access fields
template = result.data.data.first
template.name
template[:name] # Same thing
# Convert to hash
template.to_h
# Serialize to JSON
template.to_json
# Pretty print
puts template.inspectContacts
Create and manage contacts:
# Create a contact
result = sent_dm.contacts.create(
phone_number: "+1234567890"
)
puts "Contact ID: #{result.data.id}"
puts "Channels: #{result.data.available_channels}"
# List contacts
result = sent_dm.contacts.list(limit: 100)
result.data.data.each do |contact|
puts "#{contact.phone_number} - #{contact.available_channels}"
end
# Get a contact
result = sent_dm.contacts.get("contact-uuid")
# Update a contact
result = sent_dm.contacts.update(
"contact-uuid",
phone_number: "+1987654321"
)
# Delete a contact
sent_dm.contacts.delete("contact-uuid")Templates
List and retrieve templates:
# List templates
result = sent_dm.templates.list
result.data.data.each do |template|
puts "#{template.name} (#{template.status}): #{template.id}"
puts " Category: #{template.category}"
puts " Channels: #{template.channels.join(', ')}"
end
# Get a specific template
result = sent_dm.templates.get("template-uuid")
puts "Name: #{result.data.name}"
puts "Status: #{result.data.status}"Webhooks
Recommended pattern: Webhooks are the primary way to track message delivery — don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.
Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:
messages— Message status changes (SENT,DELIVERED,READ,FAILED, …)templates— WhatsApp template approval/rejection
The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key. The signed content is {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody} and the signature format is v1,{base64(hmac)}. See the Rails and Sinatra integration sections below for full examples, and the Webhooks reference for the full payload schema.
Making custom or undocumented requests
Undocumented properties
You can send undocumented parameters to any endpoint using extra_* options:
result = sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome"
},
request_options: {
extra_query: {my_query_parameter: value},
extra_body: {my_body_parameter: value},
extra_headers: {"my-header" => value}
}
)
puts(result[:my_undocumented_property])Undocumented endpoints
To make requests to undocumented endpoints:
response = sent_dm.request(
method: :post,
path: '/undocumented/endpoint',
query: {"dog" => "woof"},
headers: {"useful-header" => "interesting-value"},
body: {"hello" => "world"}
)Concurrency & connection pooling
The Sentdm::Client instances are thread-safe, but are only fork-safe when there are no in-flight HTTP requests.
Each instance of Sentdm::Client has its own HTTP connection pool with a default size of 99. As such, we recommend instantiating the client once per application in most settings.
When all available connections from the pool are checked out, requests wait for a new connection to become available, with queue time counting towards the request timeout.
Rails Integration
Configuration
# config/initializers/sentdm.rb
$SENT_DM = Sentdm::Client.new(
api_key: ENV['SENT_DM_API_KEY']
)
# Usage anywhere
$SENT_DM.messages.send_(
to: ['+1234567890'],
template: {
id: 'welcome-template',
name: 'welcome'
}
)Controllers
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:webhook]
def send_welcome
result = $SENT_DM.messages.send_(
to: [params[:phone]],
template: {
id: 'welcome-template',
name: 'welcome',
parameters: {name: params[:name]}
}
)
if result.success
render json: {
success: true,
message_id: result.data.messages[0].id,
status: result.data.messages[0].status
}
else
render json: {error: result.error.message}, status: :bad_request
end
end
def webhook
require 'openssl'
require 'base64'
# 1. Read raw body
payload = request.body.read
webhook_id = request.headers['X-Webhook-ID'] || ''
timestamp = request.headers['X-Webhook-Timestamp'] || ''
signature = request.headers['X-Webhook-Signature'] || ''
# 2. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
secret = ENV['SENT_DM_WEBHOOK_SECRET'] # "whsec_abc123..."
key_bytes = Base64.strict_decode64(secret.delete_prefix('whsec_'))
signed = "#{webhook_id}.#{timestamp}.#{payload}"
digest = OpenSSL::HMAC.digest('SHA256', key_bytes, signed)
expected = "v1,#{Base64.strict_encode64(digest)}"
unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)
return render json: {error: 'Invalid signature'}, status: :unauthorized
end
# 3. Optional: reject replayed events older than 5 minutes
if (Time.now.to_i - timestamp.to_i).abs > 300
return render json: {error: 'Timestamp too old'}, status: :unauthorized
end
event = JSON.parse(payload, symbolize_names: true)
# 4. Update message status in your own database
if event[:field] == 'messages'
Message.where(sent_id: event[:payload][:message_id])
.update_all(status: event[:payload][:message_status])
end
# 5. Always return 200 quickly
render json: {received: true}
end
endActiveJob Integration
# app/jobs/send_welcome_message_job.rb
class SendWelcomeMessageJob < ApplicationJob
queue_as :default
retry_on Sentdm::Errors::RateLimitError, wait: :polynomially_longer, attempts: 5
discard_on Sentdm::Errors::ValidationError
def perform(user)
result = $SENT_DM.messages.send_(
to: [user.phone_number],
template: {
id: 'welcome-template',
name: 'welcome',
parameters: {name: user.first_name}
}
)
raise result.error.message unless result.success
end
endRake Tasks
# lib/tasks/sentdm.rake
namespace :sentdm do
desc "Send test message"
task :test, [:phone, :template] => :environment do |t, args|
args.with_defaults(template: 'welcome-template')
puts "Sending test message to #{args.phone}..."
result = $SENT_DM.messages.send_(
to: [args.phone],
template: {
id: args.template,
name: 'welcome'
}
)
if result.success
puts "Sent: #{result.data.messages[0].id}"
else
puts "Failed: #{result.error.message}"
exit 1
end
end
endSinatra Integration
# app.rb
require 'sinatra'
require 'sentdm'
require 'json'
configure do
set :sent_client, Sentdm::Client.new
end
post '/send-message' do
content_type :json
data = JSON.parse(request.body.read)
result = settings.sent_client.messages.send_(
to: [data['phone_number']],
template: {
id: data['template_id'],
name: data['template_name'] || 'welcome',
parameters: data['variables'] || {}
}
)
if result.success
{message_id: result.data.messages[0].id, status: result.data.messages[0].status}.to_json
else
status 400
{error: result.error.message}.to_json
end
end
post '/webhooks/sent' do
require 'openssl'
require 'base64'
content_type :json
# 1. Read raw body
payload = request.body.read
webhook_id = request.env['HTTP_X_WEBHOOK_ID'] || ''
timestamp = request.env['HTTP_X_WEBHOOK_TIMESTAMP'] || ''
signature = request.env['HTTP_X_WEBHOOK_SIGNATURE'] || ''
# 2. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
secret = ENV['SENT_DM_WEBHOOK_SECRET'] # "whsec_abc123..."
key_bytes = Base64.strict_decode64(secret.delete_prefix('whsec_'))
signed = "#{webhook_id}.#{timestamp}.#{payload}"
digest = OpenSSL::HMAC.digest('SHA256', key_bytes, signed)
expected = "v1,#{Base64.strict_encode64(digest)}"
unless Rack::Utils.secure_compare(expected, signature)
status 401
return {error: 'Invalid signature'}.to_json
end
# 3. Optional: reject replayed events older than 5 minutes
if (Time.now.to_i - timestamp.to_i).abs > 300
status 401
return {error: 'Timestamp too old'}.to_json
end
event = JSON.parse(payload, symbolize_names: true)
# 4. Update message status in your own database
if event[:field] == 'messages'
# DB[:messages].where(sent_id: event[:payload][:message_id])
# .update(status: event[:payload][:message_status])
puts "Message #{event[:payload][:message_id]} → #{event[:payload][:message_status]}"
end
# 5. Always return 200 quickly
{received: true}.to_json
endSorbet
This library provides comprehensive RBI definitions and has no dependency on sorbet-runtime.
You can provide typesafe request parameters:
sent_dm.messages.send_(
to: ["+1234567890"],
template: {
id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
name: "welcome",
parameters: {name: "John Doe"}
}
)Source & Issues
- Version: 0.13.0
- GitHub: sentdm/sent-dm-ruby
- RubyGems: sentdm
- RubyDoc: gemdocs.org/gems/sentdm
- Issues: Report a bug
Getting Help
- Documentation: API Reference
- Troubleshooting: Common Issues
- Support: Email support@sent.dm with your request ID