Note: This is an unofficial gem that wraps the official Cellcast API. I built this for my own use to make my life easier.
A Ruby gem for the Cellcast API focused on SMS sending and account management. Provides an enhanced developer experience with smart response objects while maintaining full access to the official Cellcast API.
- Sandbox Bulk SMS Bug: Fixed TypeError when processing array structures in bulk operations
- Enhanced Response Collection: Fixed
.to_hmethod for bulk response collections - Missing API Fields: Added support for
low_sms_alertfield from API responses - Error Code Detection: Complete coverage of official error codes (
FIELD_INVALID,OVER_LIMIT, etc.)
quick_send_bulk(): Simple bulk SMS sending for multiple recipients- Enhanced Error Handling: Structured error detection with specific error type methods
- Low Balance Detection: Automatic alerts when account credits are low
- Bulk Collection Methods: Better handling of multiple response aggregation
- Send SMS: Individual messages with enhanced response objects
- Bulk SMS: Send to multiple recipients with automatic chunking
- Account Management: Check balance with low-balance detection
- Message Tracking: Get message details with delivery tracking
- Inbound Messages: Retrieve and manage incoming SMS with pagination helpers
- New Zealand SMS: Dedicated endpoint for NZ numbers
- Template Messages: Send SMS using predefined templates
- Alpha ID Registration: Register business names for sender IDs
- Smart Response Objects: Convenient methods like
.success?,.message_id,.credits_used - Chainable Operations:
.on_successand.on_errorfor clean error handling - Automatic Retry: Built-in retry logic for rate limits and server errors
- Smart Defaults: Configure default sender IDs and other preferences
- Automatic Chunking: Large broadcasts split automatically for optimal delivery
- Message Tracking: Track messages until delivered with timeout support
- Pagination Helpers: Easy iteration through large inbound message lists
- Low Balance Alerts: Automatic detection of low account balances
- Enhanced Responses: Rich response objects with convenience methods (default)
- Raw Responses: Direct API responses for maximum compatibility
- Flexible Configuration: Custom retry settings, chunk sizes, and more
- Sandbox Mode: Safe testing without real API calls
Add to your Gemfile:
gem 'cellcast-sms'Then run:
bundle installrequire 'cellcast'
# Create client with enhanced responses (default behavior)
client = Cellcast.enhanced_sms(
api_key: 'your-api-key',
default_sender_id: 'YourBrand',
sandbox_mode: false # Set to true for testing
)
# Send a message with smart response handling
response = client.quick_send(
to: '+61400000000',
message: 'Hello from Cellcast!'
)
# Clean, readable response handling
if response.success?
puts "Message sent! ID: #{response.message_id}"
puts "Credits used: #{response.credits_used}"
puts "Sent to: #{response.to}"
else
puts "Failed: #{response.api_message}"
end
# Chainable operations for elegant error handling
client.quick_send(to: '+61400000000', message: 'Hello!')
.on_success { |r| puts "Sent! ID: #{r.message_id}" }
.on_error { |r| puts "Failed: #{r.api_message}" }# Fixed: Bulk SMS with array structures now works correctly
client.send_personalized(messages: [
{ to: '+61400000000', message: 'Hello Alice!' },
{ to: '+61400000001', message: 'Hello Bob!' }
])
# Fixed: Quick bulk sending method added
response = client.quick_send_bulk(
to: ['+61400000000', '+61400000001'],
message: 'Bulk message'
)
# Fixed: Response collections now support .to_h
puts response.to_h[:summary][:success_rate] # Works correctly now
# Fixed: Low balance alerts now properly exposed
response = client.quick_send(to: '+61400000000', message: 'Test')
if response.low_balance_alert?
puts "⚠️ #{response.low_sms_alert}"
end
# Fixed: Enhanced error code detection
begin
client.quick_send(to: 'invalid', message: 'Test')
rescue Cellcast::SMS::CellcastApiError => e
case
when e.field_invalid?
puts "📝 Invalid field provided"
when e.over_limit?
puts "⚠️ Message limit exceeded"
when e.invalid_message_length?
puts "📏 Message too long"
when e.insufficient_credit?
puts "💳 Insufficient account credit"
end
end# Send to multiple recipients with automatic chunking
recipients = ['+61400000000', '+61400000001', '+61400000002']
response = client.broadcast(
to: recipients,
message: 'Important announcement!'
)
# Rich information about the broadcast
puts "Success rate: #{response.success_rate}%"
puts "Total sent: #{response.success_number}/#{response.total_numbers}"
puts "Credits used: #{response.credits_used}"
# Handle mixed results
if response.has_failures?
puts "Some messages failed to send"
else
puts "All messages sent successfully!"
end
# Iterate through individual messages
response.each_message do |message|
puts "#{message['to']}: #{message['message_id']}"
end# Check account balance with enhanced features
balance = client.balance
puts "SMS Balance: $#{balance.sms_balance}"
puts "MMS Balance: $#{balance.mms_balance}"
puts "Account: #{balance.account_name}"
# Smart balance checking
if balance.low_balance?
puts "⚠️ Your balance is low!"
end
# Custom thresholds
if client.low_balance?(sms_threshold: 50)
puts "SMS balance below $50"
end
# Automatic low balance alerts in SMS responses
response = client.quick_send(to: '+61400000000', message: 'Hello!')
if response.low_balance_alert?
puts "⚠️ #{response.low_sms_alert}"
end# Track message delivery
message_id = response.message_id
final_status = client.track_message_delivery(
message_id: message_id,
timeout: 300, # 5 minutes
check_interval: 30 # Check every 30 seconds
)
puts "Final status: #{final_status.status}"
puts "Delivered at: #{final_status.delivered_at}" if final_status.delivered?
# Get inbound messages with pagination
# IMPORTANT: Messages persist across multiple calls until explicitly marked as read
inbound = client.get_inbound_messages(page: 1)
puts "Current page: #{inbound.current_page}/#{inbound.total_pages}"
puts "Messages on this page: #{inbound.message_count}"
# Iterate through messages (note: no 'to' field on inbound messages)
inbound.each_message do |message|
puts "From #{message.from}: #{message.body}"
puts "Received: #{message.received_at}"
puts "Message ID: #{message.message_id}"
puts "Read: #{message.read? ? 'Yes' : 'No'}" # Usually 'No' since get-responses returns unread only
end
# Safe polling pattern for production systems
def process_inbound_messages
messages = client.get_inbound_messages
processed_ids = []
messages.each_message do |msg|
begin
# Process your message here
handle_inbound_sms(msg.from, msg.body)
processed_ids << msg.message_id
rescue => e
logger.error "Failed to process message #{msg.message_id}: #{e}"
# Don't mark as read if processing failed
end
end
# Only mark successfully processed messages as read
if processed_ids.any?
client.mark_messages_read(message_ids: processed_ids)
end
end
# Alternative: Mark all current messages as read at once
client.mark_all_read
# Or mark messages older than a specific time
client.mark_messages_read(before: 1.hour.ago)# Send with automatic retry on failure
response = client.quick_send_with_retry(
to: '+61400000000',
message: 'Important message',
max_retries: 5
)
# Large broadcasts with custom chunking
large_list = (1..500).map { |i| "+6140000#{i.to_s.rjust(4, '0')}" }
response = client.broadcast_with_retry(
to: large_list,
message: 'Mass notification',
max_retries: 3
)
# BulkResponseCollection for large broadcasts
puts "Processed #{response.response_count} chunks"
puts "Overall success rate: #{response.success_rate}%"
# Personalized messages
messages = [
{ to: '+61400000000', message: 'Hello John!', sender_id: 'Store' },
{ to: '+61400000001', message: 'Hello Jane!', sender_id: 'Store' },
]
response = client.send_personalized(messages: messages)
# Template-based sending
response = client.send_template(
template_id: 'welcome_template',
numbers: [
{ number: '+61400000000', personalization: { name: 'John' } },
{ number: '+61400000001', personalization: { name: 'Jane' } }
]
)# Structured error handling with specific error types
begin
response = client.quick_send(to: 'invalid', message: 'Test')
rescue Cellcast::SMS::CellcastApiError => e
case
when e.insufficient_credit?
puts "💳 Insufficient credit: #{e.api_message}"
when e.invalid_number?
puts "📱 Invalid phone number: #{e.api_message}"
when e.field_invalid?
puts "📝 Invalid field: #{e.api_message}"
when e.over_limit?
puts "⚠️ Over limit: #{e.api_message}"
when e.invalid_message_length?
puts "📏 Message too long: #{e.api_message}"
when e.rate_limited?
puts "⏰ Rate limited. Retry after #{e.suggested_retry_delay}s"
when e.server_error?
puts "🔧 Server error - will auto-retry if retryable"
else
puts "❌ Error: #{e.api_message}"
end
end
# Automatic retry for retryable errors
response = client.quick_send_with_retry(
to: '+61400000000',
message: 'Important message',
max_retries: 5 # Will retry rate limits and server errors
)# Comprehensive configuration
client = Cellcast.sms(
api_key: 'your-api-key',
response_format: :enhanced, # :enhanced, :raw, or :both
default_sender_id: 'YourBrand', # Used when no sender_id specified
auto_retry_failed: true, # Automatically retry failed requests
max_retries: 3, # Maximum retry attempts
chunk_size: 100, # Bulk operation chunk size
low_balance_threshold: 20, # SMS balance warning threshold
sandbox_mode: false # Enable for testing
)
# Quick setups for common scenarios
enhanced_client = Cellcast.enhanced_sms(api_key: 'key', default_sender_id: 'Brand')
raw_client = Cellcast.raw_sms(api_key: 'key') # For legacy compatibilityFor maximum compatibility with existing code, you can use raw response mode:
# Create client with raw responses
client = Cellcast.raw_sms(api_key: 'your-api-key')
# Returns raw API responses (Hash objects)
response = client.quick_send(
to: '+61400000000',
message: 'Hello!',
from: 'YourBrand'
)
# Manual response parsing (legacy approach)
if response['meta'] && response['meta']['status'] == 'SUCCESS'
messages = response.dig('data', 'messages')
message_id = messages.first['message_id'] if messages&.first
puts "Message sent! ID: #{message_id}"
puts "Credits used: #{response.dig('data', 'credits_used')}"
else
puts "Failed: #{response['msg']}"
endbegin
response = client.quick_send(to: '+61400000000', message: 'Hello!')
rescue Cellcast::SMS::CellcastApiError => e
case
when e.insufficient_credit?
puts "💳 Insufficient credit: #{e.api_message}"
when e.invalid_number?
puts "📱 Invalid number format: #{e.api_message}"
when e.rate_limited?
puts "⏰ Rate limited. Retry after #{e.suggested_retry_delay} seconds"
when e.authentication_error?
puts "🔑 Authentication failed: #{e.api_message}"
else
puts "❌ Error: #{e.api_message}"
end
end
# Automatic retry for retryable errors
begin
response = client.quick_send_with_retry(
to: '+61400000000',
message: 'Important message'
)
rescue Cellcast::SMS::CellcastApiError => e
puts "Failed after retries: #{e.api_message}"
endresponse = client.quick_send(to: '+61400000000', message: 'Hello!')
case response.dig('meta', 'status')
when 'SUCCESS'
puts "Message sent successfully"
when 'FAILED'
puts "Message failed: #{response['msg']}"
end# SMS Operations
client.quick_send(to:, message:, from: nil)
client.quick_send_with_retry(to:, message:, from: nil, max_retries: nil)
client.broadcast(to:, message:, from: nil, chunk_size: nil)
client.broadcast_with_retry(to:, message:, from: nil, max_retries: nil)
client.send_personalized(messages:, chunk_size: nil)
client.send_to_nz(to:, message:, from: nil)
client.send_template(template_id:, numbers:, from: nil)
# Message Management
client.get_message_status(message_id:)
client.track_message_delivery(message_id:, timeout: 300, check_interval: 30)
client.delivery_stats(message_ids)
# Inbound Messages
client.get_inbound_messages(page: 1)
client.get_all_inbound_messages(limit: nil, unread_only: false)
client.inbound_message_stats(pages: 1)
client.mark_read(message_id:)
client.mark_all_read(before: nil)
client.mark_all_unread_as_read(before: nil)
# Account Operations
client.balance
client.low_balance?(sms_threshold: nil, mms_threshold: 5)
client.get_templates
client.find_template(identifier)
client.get_optouts
# Registration
client.register_alpha_id(alpha_id:, purpose:, business_registration: nil, contact_info: nil)response.success? # Boolean: API call successful
response.error? # Boolean: API call failed
response.message_id # String: Message ID
response.credits_used # Integer: Credits consumed
response.to # String: Recipient number
response.from # String: Sender ID used
response.message_text # String: Message content
response.all_successful? # Boolean: All messages sent
response.api_message # String: API response message
response.raw_response # Hash: Full API responseresponse.success_rate # Float: Success percentage
response.total_numbers # Integer: Total recipients
response.success_number # Integer: Successful sends
response.failed_number # Integer: Failed sends
response.credits_used # Integer: Credits consumed
response.messages # Array: Message details
response.all_successful? # Boolean: All messages sent
response.has_failures? # Boolean: Any failures occurred
response.each_message { |msg| ... } # Iterate messagesbalance.sms_balance # String: SMS balance
balance.mms_balance # String: MMS balance
balance.account_name # String: Account name
balance.low_sms_balance?(threshold) # Boolean: SMS balance low
balance.low_mms_balance?(threshold) # Boolean: MMS balance low
balance.low_balance?(sms_thresh, mms_thresh) # Boolean: Any balance low
balance.total_balance # Float: Combined balanceinbound.messages # Array<InboundMessage>: Message objects
inbound.current_page # Integer: Current page number
inbound.total_pages # Integer: Total pages available
inbound.has_more_pages? # Boolean: More pages available
inbound.message_count # Integer: Messages on current page
inbound.unread_messages # Array: Unread messages only
inbound.each_message { |msg| ... } # Iterate messagesmessage.from # String: Sender number
message.body # String: Message content
message.received_at # Time: When received
message.message_id # String: Message ID
message.read? # Boolean: Has been read
message.unread? # Boolean: Not yet readAll enhanced response objects support chainable operations:
client.quick_send(to: number, message: text)
.on_success { |response| log_success(response.message_id) }
.on_error { |response| log_error(response.api_message) }
client.broadcast(to: numbers, message: text)
.on_success { |response| puts "Sent to #{response.success_number} recipients" }
.on_error { |response| puts "Broadcast failed: #{response.api_message}" }You can set defaults using environment variables:
export CELLCAST_API_KEY="your-api-key"
export CELLCAST_DEFAULT_SENDER_ID="YourBrand"
export CELLCAST_SANDBOX_MODE="true" # For testing# Use environment variables
client = Cellcast.enhanced_sms(
api_key: ENV['CELLCAST_API_KEY'],
default_sender_id: ENV['CELLCAST_DEFAULT_SENDER_ID'],
sandbox_mode: ENV['CELLCAST_SANDBOX_MODE'] == 'true'
)config = Cellcast.configure do |c|
c.response_format = :enhanced
c.open_timeout = 30
c.read_timeout = 60
c.auto_retry_failed = true
c.max_retries = 3
c.retry_delay = 2 # Base delay for exponential backoff
c.chunk_size = 100
c.low_balance_threshold = 10
c.sandbox_mode = false
end
client = Cellcast.sms(api_key: 'your-key', config: config)The gem includes a comprehensive sandbox mode for testing without making real API calls:
# Enable sandbox mode
client = Cellcast.enhanced_sms(
api_key: 'test-key',
sandbox_mode: true
)
# Special test numbers trigger different behaviors
client.quick_send(to: '+15550000000', message: 'Test') # Always succeeds
client.quick_send(to: '+15550000001', message: 'Test') # Always fails
client.quick_send(to: '+15550000002', message: 'Test') # Rate limited
client.quick_send(to: '+15550000003', message: 'Test') # Invalid number
client.quick_send(to: '+15550000004', message: 'Test') # Insufficient credits
client.quick_send(to: '+15550000005', message: 'Test') # Low balance (success with alert)
# Test low balance alerts
response = client.quick_send(to: '+15550000005', message: 'Test')
if response.low_balance_alert?
puts "⚠️ #{response.low_sms_alert}"
end# Run all tests
bundle exec rake test
# Run specific test files
ruby -I lib test/test_enhanced_convenience.rb
ruby -I lib test/test_raw_response_format.rb
ruby -I lib test/test_enhanced_error_handling.rb
ruby -I lib test/test_helper_classes.rbIf you're upgrading from raw response format to enhanced format:
# Before (raw responses)
if response['meta']['status'] == 'SUCCESS'
message_id = response.dig('data', 'messages', 0, 'message_id')
credits = response.dig('data', 'credits_used')
puts "Sent! ID: #{message_id}, Credits: #{credits}"
end
# After (enhanced responses)
if response.success?
puts "Sent! ID: #{response.message_id}, Credits: #{response.credits_used}"
end
# Enhanced responses still support hash access for compatibility
message_id = response['data']['messages'][0]['message_id'] # Still works
message_id = response.dig('data', 'messages', 0, 'message_id') # Still worksYou can use :both response format to migrate gradually:
client = Cellcast.sms(
api_key: 'your-key',
response_format: :both # Enhanced objects with full raw access
)
response = client.quick_send(to: number, message: text)
# Use enhanced methods
puts response.success?
puts response.message_id
# Still access raw data when needed
puts response.raw_response['data']['messages']
puts response['meta']['status'] # Hash access still worksProblem: "Messages disappear after first fetch"
Solution: Update to the latest gem version. Older versions had parsing bugs that made messages appear to disappear. The API actually returns the same unread messages across multiple calls.
Problem: "Empty messages array but total_messages shows count"
Solution: This was a parsing bug in older versions. Update the gem - messages now appear correctly.
Problem: "Need defensive programming for message fields"
Solution: Update the gem. Message objects now have consistent, reliable accessors.
Use the diagnostic script to verify correct behavior:
CELLCASTKEY=your_api_key ruby examples/enhanced_diagnostics.rbThis will test:
- Message persistence across multiple reads
- Explicit marking behavior
- Object structure consistency
- Raw API vs gem parity
❌ Incorrect assumption:
# Messages are NOT auto-marked as read
messages1 = client.get_inbound_messages
messages2 = client.get_inbound_messages
# messages1 and messages2 will be identical until explicitly marked✅ Correct polling pattern:
def poll_messages
messages = client.get_inbound_messages
process_messages(messages)
# Mark as read only after successful processing
message_ids = messages.map(&:message_id)
client.mark_messages_read(message_ids: message_ids)
end❌ Incorrect field access:
msg.to # Inbound messages don't have 'to' field✅ Correct field access:
msg.from # Sender's number
msg.body # Message content
msg.message_id # Unique identifier
msg.received_at # TimestampThis gem strictly aligns with the official Cellcast API documentation. All endpoints, request formats, and response structures match the official specification.
send-sms- Send single SMSbulk-send-sms- Send bulk SMSget-sms- Get message detailsget-responses- Get inbound messagessend-sms-nz- Send SMS to New Zealandsend-sms-template- Send template SMSinbound-read- Mark message as readinbound-read-bulk- Mark multiple messages as readregister-alpha-id- Register business nameaccount- Get account balanceget-template- Get SMS templatesget-optout- Get opt-out list
- Ruby 3.2 or higher
- No external dependencies (uses only Ruby standard library)
Bug reports and pull requests are welcome on GitHub at https://github.com/timburgan/cellcast-sms.
The gem is available as open source under the terms of the MIT License.
This is an unofficial gem. I am not affiliated with Cellcast. Use at your own risk and always test thoroughly before production use.