Ruby SDK

Ruby SDK

The official Ruby SDK for Sent LogoSent 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.3.0"

Then run:

bundle install

Or install directly:

gem install sentdm

Quick 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"
    }
  },
  channels: ["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

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"
    }
  },
  channels: ["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)
end

Error codes are as follows:

CauseError Type
HTTP 400BadRequestError
HTTP 401AuthenticationError
HTTP 403PermissionDeniedError
HTTP 404NotFoundError
HTTP 409ConflictError
HTTP 422UnprocessableEntityError
HTTP 429RateLimitError
HTTP >= 500InternalServerError
Other HTTP errorAPIStatusError
TimeoutAPITimeoutError
Network errorAPIConnectionError

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:

  1. All fields, including unknown ones, are accessible with obj[:prop] syntax
  2. Structural equivalence for equality
  3. Both instances and classes can be pretty-printed
  4. 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.inspect

Contacts

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}"

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
    payload = request.body.read
    signature = request.headers['X-Webhook-Signature']

    # Verify signature
    unless $SENT_DM.webhooks.verify_signature(
      payload: payload,
      signature: signature,
      secret: ENV['SENT_DM_WEBHOOK_SECRET']
    )
      return render json: {error: 'Invalid signature'}, status: :unauthorized
    end

    # Parse and handle event
    event = JSON.parse(payload, object_class: OpenStruct)

    case event.type
    when 'message.status.updated'
      Rails.logger.info "Message #{event.data.id} status: #{event.data.status}"
    when 'message.delivered'
      # Update database
    end

    render json: {received: true}
  end
end

ActiveJob 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
end

Rake 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
end

Sinatra 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
  content_type :json

  payload = request.body.read
  signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

  # Verify signature
  unless settings.sent_client.webhooks.verify_signature(
    payload: payload,
    signature: signature,
    secret: ENV['SENT_DM_WEBHOOK_SECRET']
  )
    status 401
    return {error: 'Invalid signature'}.to_json
  end

  event = JSON.parse(payload, object_class: OpenStruct)

  case event.type
  when 'message.delivered'
    puts "Message #{event.data.id} delivered!"
  when 'message.failed'
    puts "Message failed: #{event.data.error.message}"
  end

  {received: true}.to_json
end

Sorbet

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

Getting Help

On this page