Ruby SDK

Ruby SDK

The official Ruby SDK for Sent provides an elegant, Ruby-idiomatic interface for sending messages. Designed for Rails applications, with Yard docstrings and RBS and RBI type signatures.

Requirements

Ruby 3.2.0 or later.

Installation

To use this gem, install via Bundler by adding the following to your app's Gemfile:

gem "sentdm"

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"
    }
  },
  channel: ["sms", "whatsapp", "rcs"]
)

puts(result.data.recipients[0].message_id)
puts(result.data.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", "rcs"]
)

puts(result.data.recipients[0].message_id)
puts(result.data.status)

Sandbox mode

Use sandbox: true to validate requests without sending real messages:

result = sent_dm.messages.send_(
  to: ["+1234567890"],
  template: {
    id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    name: "welcome"
  },
  sandbox: true # Validates but doesn't send
)

# Response will have test data
puts(result.data.recipients[0].message_id)
puts(result.data.status)

Check message status

Retrieve the current status of a sent message. The direction field indicates whether the message is "OUTBOUND" (sent by you) or "INBOUND" (a reply or opt-out keyword received from an end user):

status = sent_dm.messages.retrieve_status("msg-uuid")

puts status.data.status      # e.g. "DELIVERED"
puts status.data.channel     # e.g. "sms"
puts status.data.direction   # "OUTBOUND" | "INBOUND"

Message activities

Retrieve the full activity log for a message, useful for auditing delivery attempts across carriers:

activities = sent_dm.messages.retrieve_activities("msg-uuid")

activities.data.activities.each do |activity|
  puts "#{activity.timestamp}: #{activity.status} via #{activity.from}"
  puts "  Price: #{activity.price}"
  puts "  Active contact price: #{activity.active_contact_price}"
end

Numbers

Look up carrier and line-type information for any phone number before sending:

result = sent_dm.numbers.lookup("+12025551234")

puts result.data.is_valid      # true/false
puts result.data.carrier_name  # e.g. "T-Mobile"
puts result.data.line_type     # "mobile", "landline", "voip"
puts result.data.is_voip       # true/false

Handling errors

When the library is unable to connect to the API, or if the API returns a non-success status code (that is, 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

Requests time out after 60 seconds by default. Set timeout (in seconds) on the client or per request; timeout: nil disables the timeout entirely.

# Configure the default for all requests:
sent_dm = Sentdm::Client.new(
  timeout: 20 # seconds (default is 60; nil disables the timeout)
)

# 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(page: 1, page_size: 100)

# Access fields
template = result.data.templates.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(page: 1, page_size: 100)

result.data.contacts.each do |contact|
  puts "#{contact.phone_number} - #{contact.available_channels}"
end

# Get a contact
result = sent_dm.contacts.retrieve("contact-uuid")

# Update a contact
result = sent_dm.contacts.update(
  "contact-uuid",
  default_channel: "whatsapp"
)

# Delete a contact
sent_dm.contacts.delete("contact-uuid")

Templates

List and retrieve templates:

# List templates
result = sent_dm.templates.list(page: 1, page_size: 100)

result.data.templates.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.retrieve("template-uuid")

puts "Name: #{result.data.name}"
puts "Status: #{result.data.status}"

Webhooks

Recommended pattern: Webhooks are the primary way to track message delivery, so 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:

  • message: Message status changes (QUEUED, ROUTED, SCHEDULED, SENT, DELIVERED, READ, FAILED, FILTERED, BLOCKED, RECEIVED); each fires as a sub-type (for example, message.delivered, message.filtered). Use message.received to receive inbound messages from contacts.
  • 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)}. Complete verified webhook handlers are available in the Rails integration guide and the Sinatra integration guide; the Webhooks reference documents the full payload schema and all status values.

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 (or the number of CPU cores, if greater). As such, Sent recommends instantiating the client once per app 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.

Framework Integration

Dedicated guides cover client setup, message sending, verified webhook handling, and testing for each framework:

Sorbet

This library provides 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