A comprehensive Ruby client library for the WhatsApp Business Cloud API. This SDK provides a complete interface for sending messages, managing media, templates, and more, with built-in error handling, retry logic, and debug capabilities.
- π Complete API Coverage: All Kapso Cloud API endpoints supported
- π± Rich Message Types: Text, media, templates, interactive messages, and more
- π Dual Authentication: Meta Graph API and Kapso Proxy support
- π‘οΈ Smart Error Handling: Comprehensive error categorization and retry logic
- π Advanced Features: Message history, analytics, and contact management (via Kapso)
- π Debug Support: Detailed logging and request/response tracing
- π Type Safety: Structured response objects and validation
- β‘ Performance: HTTP connection pooling and efficient request handling
- π€οΈ Rails Integration: First-class Rails support with generators, service classes, and background jobs
Add this line to your application's Gemfile:
gem 'kapso-client-ruby'And then execute:
$ bundle installOr install it yourself as:
$ gem install kapso-client-rubyFor Rails applications, use the built-in generator to set up everything automatically:
rails generate kapso_client_ruby:installThis creates:
- Configuration initializer
- Webhook controller
- Service class for messaging
- Background job examples
- Routes for webhooks
See the Rails Integration Guide for detailed Rails-specific documentation.
require 'kapso_client_api'
# Initialize client with Meta Graph API access token
client = KapsoClientRuby::Client.new(
access_token: 'your_access_token'
)
# Send a text message
response = client.messages.send_text(
phone_number_id: 'your_phone_number_id',
to: '+1234567890',
body: 'Hello from Ruby!'
)
puts "Message sent: #{response.messages.first.id}"# Initialize client with Kapso API key for enhanced features
kapso_client = KapsoClientRuby::Client.new(
kapso_api_key: 'your_kapso_api_key',
base_url: 'https://app.kapso.ai/api/meta'
)
# Access message history and analytics
messages = kapso_client.messages.query(
phone_number_id: 'your_phone_number_id',
direction: 'inbound',
limit: 10
)Create and manage interactive WhatsApp Flows for rich data collection:
# Idempotent deployment (recommended)
deployment = client.flows.deploy(
business_account_id: 'your_business_id',
name: 'appointment_booking',
categories: ['APPOINTMENT_BOOKING'],
flow_json: {
version: '3.0',
screens: [
{
id: 'APPOINTMENT',
title: 'Book Appointment',
layout: {
type: 'SingleColumnLayout',
children: [
{
type: 'Form',
name: 'appointment_form',
children: [
{
type: 'TextInput',
name: 'customer_name',
label: 'Full Name',
required: true
},
{
type: 'DatePicker',
name: 'appointment_date',
label: 'Preferred Date',
required: true
},
{
type: 'Footer',
label: 'Submit',
on_click_action: {
name: 'complete',
payload: {
customer_name: '${form.customer_name}',
appointment_date: '${form.appointment_date}'
}
}
}
]
}
]
}
}
]
}
)
puts "Flow ID: #{deployment[:id]}"require 'securerandom'
client.messages.send_flow(
phone_number_id: 'phone_id',
to: '+1234567890',
flow_id: 'your_flow_id',
flow_cta: 'Book Appointment',
flow_token: SecureRandom.uuid,
header: {
type: 'text',
text: 'Appointment Booking'
},
body_text: 'Book your appointment in just a few taps!',
footer_text: 'Available slots fill up fast'
)# In your webhook endpoint
def handle_flow_webhook(encrypted_request)
private_key = OpenSSL::PKey::RSA.new(File.read('private_key.pem'))
# Decrypt incoming Flow event
flow_event = client.flows.receive_flow_event(
encrypted_request: encrypted_request,
private_key: private_key
)
# Process form data
case flow_event.action
when 'INIT'
response_data = {
version: flow_event.version,
screen: 'APPOINTMENT',
data: { available_dates: ['2024-01-15', '2024-01-16'] }
}
when 'data_exchange'
# Save appointment data
customer_name = flow_event.data['customer_name']
appointment_date = flow_event.data['appointment_date']
response_data = {
version: flow_event.version,
screen: 'SUCCESS',
data: { success: true }
}
end
# Encrypt and return response
client.flows.respond_to_flow(
response_data: response_data,
private_key: private_key
)
end# List all Flows
flows = client.flows.list(business_account_id: 'business_id')
# Get Flow details
flow = client.flows.get(flow_id: 'flow_id')
# Update Flow
client.flows.update(
flow_id: 'flow_id',
categories: ['APPOINTMENT_BOOKING', 'CUSTOMER_SUPPORT']
)
# Update Flow JSON
asset_response = client.flows.update_asset(
flow_id: 'flow_id',
asset: flow_json_hash
)
# Publish Flow
client.flows.publish(flow_id: 'flow_id')
# Get preview URL
preview = client.flows.preview(flow_id: 'flow_id')
puts preview.preview_url
# Deprecate Flow
client.flows.deprecate(flow_id: 'flow_id')
# Delete Flow
client.flows.delete(flow_id: 'flow_id')Send various types of messages with the Messages resource:
# Simple text message
client.messages.send_text(
phone_number_id: 'phone_id',
to: '+1234567890',
body: 'Hello World!'
)
# Text with URL preview
client.messages.send_text(
phone_number_id: 'phone_id',
to: '+1234567890',
body: 'Check this out: https://example.com',
preview_url: true
)# Send image
client.messages.send_image(
phone_number_id: 'phone_id',
to: '+1234567890',
image: {
link: 'https://example.com/image.jpg',
caption: 'Beautiful sunset'
}
)
# Send document
client.messages.send_document(
phone_number_id: 'phone_id',
to: '+1234567890',
document: {
id: 'media_id', # or link: 'https://...'
filename: 'report.pdf',
caption: 'Monthly report'
}
)
# Send audio
client.messages.send_audio(
phone_number_id: 'phone_id',
to: '+1234567890',
audio: { id: 'audio_media_id' }
)
# Send voice note (OGG/OPUS format recommended)
client.messages.send_audio(
phone_number_id: 'phone_id',
to: '+1234567890',
audio: { link: 'https://example.com/voice-note.ogg' },
voice: true # Marks as voice note
)
# Send video
client.messages.send_video(
phone_number_id: 'phone_id',
to: '+1234567890',
video: {
link: 'https://example.com/video.mp4',
caption: 'Tutorial video'
}
)Send messages to WhatsApp groups by setting recipient_type: 'group':
# Send text to group
client.messages.send_text(
phone_number_id: 'phone_id',
to: '120363XXXXXXXXX@g.us', # Group ID format
body: 'Hello everyone!',
recipient_type: 'group'
)
# Send image to group
client.messages.send_image(
phone_number_id: 'phone_id',
to: '120363XXXXXXXXX@g.us',
image: { link: 'https://example.com/team-photo.jpg' },
caption: 'Team photo from our event',
recipient_type: 'group'
)Note: Group messaging works with all message types (text, images, videos, documents, interactive messages, etc.)
Group ID Format: XXXXXXXXX@g.us (WhatsApp group identifier)
# Send location
client.messages.send_location(
phone_number_id: 'phone_id',
to: '+1234567890',
latitude: 37.7749,
longitude: -122.4194,
name: 'Our Office',
address: '123 Main St, San Francisco, CA'
)
# Request user's location
client.messages.send_interactive_location_request(
phone_number_id: 'phone_id',
to: '+1234567890',
body_text: 'Please share your location for delivery',
footer_text: 'Your privacy is important to us'
)
# Request location with header
client.messages.send_interactive_location_request(
phone_number_id: 'phone_id',
to: '+1234567890',
header: {
type: 'image',
image: { link: 'https://example.com/map-icon.png' }
},
body_text: 'Share your location to find the nearest store',
footer_text: 'Tap to share'
)# Button interactive message
client.messages.send_interactive_buttons(
phone_number_id: 'phone_id',
to: '+1234567890',
body_text: 'Choose an option:',
buttons: [
{
type: 'reply',
reply: {
id: 'option_1',
title: 'Option 1'
}
},
{
type: 'reply',
reply: {
id: 'option_2',
title: 'Option 2'
}
}
],
header: {
type: 'text',
text: 'Menu'
}
)
# List interactive message
client.messages.send_interactive_list(
phone_number_id: 'phone_id',
to: '+1234567890',
body_text: 'Please select from the list:',
button_text: 'View Options',
sections: [
{
title: 'Section 1',
rows: [
{
id: 'item_1',
title: 'Item 1',
description: 'Description 1'
}
]
}
]
)Send messages with Call-to-Action buttons that open URLs:
# With image header
client.messages.send_interactive_cta_url(
phone_number_id: 'phone_id',
to: '+1234567890',
header: {
type: 'image',
image: { link: 'https://example.com/banner.jpg' }
},
body_text: 'Get 25% off your first purchase!',
display_text: 'Shop Now', # Max 20 characters
url: 'https://shop.example.com?utm_source=whatsapp',
footer_text: 'Limited time offer'
)
# With text header
client.messages.send_interactive_cta_url(
phone_number_id: 'phone_id',
to: '+1234567890',
header: {
type: 'text',
text: 'Special Offer'
},
body_text: 'Join our exclusive members club!',
display_text: 'Join Now',
url: 'https://members.example.com/signup'
)
# With video or document header
client.messages.send_interactive_cta_url(
phone_number_id: 'phone_id',
to: '+1234567890',
header: {
type: 'video',
video: { link: 'https://example.com/demo.mp4' }
},
body_text: 'Watch our product in action!',
display_text: 'Learn More',
url: 'https://example.com/product'
)Validations:
body_text: Max 1024 charactersdisplay_text: Max 20 charactersurl: Must be valid HTTP/HTTPS URLfooter_text: Max 60 characters (optional)header: Supports text, image, video, or document
Send your product catalog directly in WhatsApp:
client.messages.send_interactive_catalog_message(
phone_number_id: 'phone_id',
to: '+1234567890',
body_text: 'Browse our entire product catalog!',
thumbnail_product_retailer_id: 'SKU-001', # Product SKU for thumbnail
footer_text: 'Tap to explore'
)Parameters:
body_text: Max 1024 charactersthumbnail_product_retailer_id: Product SKU to display as thumbnail (required)footer_text: Max 60 characters (optional)
# Simple template
client.messages.send_template(
phone_number_id: 'phone_id',
to: '+1234567890',
name: 'hello_world',
language: 'en_US'
)
# Template with parameters
client.messages.send_template(
phone_number_id: 'phone_id',
to: '+1234567890',
name: 'appointment_reminder',
language: 'en_US',
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: 'John Doe' },
{ type: 'text', text: 'Tomorrow at 2 PM' }
]
}
]
)# Add reaction
client.messages.send_reaction(
phone_number_id: 'phone_id',
to: '+1234567890',
message_id: 'message_to_react_to',
emoji: 'π'
)
# Remove reaction
client.messages.send_reaction(
phone_number_id: 'phone_id',
to: '+1234567890',
message_id: 'message_to_react_to',
emoji: nil
)# Mark message as read
client.messages.mark_read(
phone_number_id: 'phone_id',
message_id: 'message_id'
)
# Send typing indicator
client.messages.send_typing_indicator(
phone_number_id: 'phone_id',
to: '+1234567890'
)Handle media uploads, downloads, and management:
# Upload media
upload_response = client.media.upload(
phone_number_id: 'phone_id',
type: 'image',
file: '/path/to/image.jpg'
)
media_id = upload_response.id
# Get media metadata
metadata = client.media.get(media_id: media_id)
puts "File size: #{metadata.file_size} bytes"
puts "MIME type: #{metadata.mime_type}"
# Download media
content = client.media.download(
media_id: media_id,
as: :binary
)
# Save media to file
client.media.save_to_file(
media_id: media_id,
filepath: '/path/to/save/file.jpg'
)
# Delete media
client.media.delete(media_id: media_id)Create, manage, and use message templates:
# List templates
templates = client.templates.list(
business_account_id: 'your_business_id',
status: 'APPROVED'
)
# Create marketing template
template_data = client.templates.build_marketing_template(
name: 'summer_sale',
language: 'en_US',
body: 'Hi {{1}}! Our summer sale is here with {{2}} off!',
header: {
type: 'HEADER',
format: 'TEXT',
text: 'Summer Sale π'
},
footer: 'Limited time offer',
buttons: [
{
type: 'URL',
text: 'Shop Now',
url: 'https://shop.example.com'
}
],
body_example: {
body_text: [['John', '25%']]
}
)
response = client.templates.create(
business_account_id: 'your_business_id',
**template_data
)
# Create authentication template
auth_template = client.templates.build_authentication_template(
name: 'verify_code',
language: 'en_US',
ttl_seconds: 300
)
client.templates.create(
business_account_id: 'your_business_id',
**auth_template
)
# Delete template
client.templates.delete(
business_account_id: 'your_business_id',
name: 'old_template',
language: 'en_US'
)Access enhanced features with Kapso proxy:
# Initialize Kapso client
kapso_client = KapsoClientRuby::Client.new(
kapso_api_key: 'your_kapso_key',
base_url: 'https://app.kapso.ai/api/meta'
)
# Message history
messages = kapso_client.messages.query(
phone_number_id: 'phone_id',
direction: 'inbound',
since: '2024-01-01T00:00:00Z',
limit: 50
)
# Conversation management
conversations = kapso_client.conversations.list(
phone_number_id: 'phone_id',
status: 'active'
)
conversation = kapso_client.conversations.get(
conversation_id: conversations.data.first.id
)
# Update conversation status
kapso_client.conversations.update_status(
conversation_id: conversation.id,
status: 'archived'
)
# Contact management
contacts = kapso_client.contacts.list(
phone_number_id: 'phone_id',
limit: 100
)
# Update contact metadata
kapso_client.contacts.update(
phone_number_id: 'phone_id',
wa_id: 'contact_wa_id',
metadata: {
tags: ['premium', 'customer'],
source: 'website'
}
)
# Search contacts
results = kapso_client.contacts.search(
phone_number_id: 'phone_id',
query: 'john',
search_in: ['profile_name', 'phone_number']
)KapsoClientRuby.configure do |config|
config.debug = true
config.timeout = 60
config.open_timeout = 10
config.max_retries = 3
config.retry_delay = 1.0
endclient = KapsoClientRuby::Client.new(
access_token: 'token',
debug: true,
timeout: 30,
logger: Logger.new('whatsapp.log')
)Enable debug logging to see detailed HTTP requests and responses:
# Enable debug mode
client = KapsoClientRuby::Client.new(
access_token: 'token',
debug: true
)
# Custom logger
logger = Logger.new(STDOUT)
logger.level = Logger::DEBUG
client = KapsoClientRuby::Client.new(
access_token: 'token',
logger: logger
)The SDK provides comprehensive error handling with detailed categorization:
begin
client.messages.send_text(
phone_number_id: 'phone_id',
to: 'invalid_number',
body: 'Test'
)
rescue KapsoClientRuby::Errors::GraphApiError => e
puts "Error: #{e.message}"
puts "Category: #{e.category}"
puts "HTTP Status: #{e.http_status}"
puts "Code: #{e.code}"
# Check error type
case e.category
when :authorization
puts "Authentication failed - check your access token"
when :parameter
puts "Invalid parameter - check phone number format"
when :throttling
puts "Rate limited - wait before retrying"
if e.retry_hint[:retry_after_ms]
sleep(e.retry_hint[:retry_after_ms] / 1000.0)
end
when :template
puts "Template error - check template name and parameters"
when :media
puts "Media error - check file format and size"
end
# Check retry recommendations
case e.retry_hint[:action]
when :retry
puts "Safe to retry this request"
when :retry_after
puts "Retry after specified delay: #{e.retry_hint[:retry_after_ms]}ms"
when :do_not_retry
puts "Do not retry - permanent error"
when :fix_and_retry
puts "Fix the request and retry"
when :refresh_token
puts "Access token needs to be refreshed"
end
end:authorization- Authentication and token errors:permission- Permission and access errors:parameter- Invalid parameters or format errors:throttling- Rate limiting errors:template- Template-related errors:media- Media upload/download errors:phone_registration- Phone number registration errors:integrity- Message integrity errors:business_eligibility- Business account eligibility errors:reengagement_window- 24-hour messaging window errors:waba_config- WhatsApp Business Account configuration errors:flow- WhatsApp Flow errors:synchronization- Data synchronization errors:server- Server-side errors:unknown- Unclassified errors
def send_with_retry(client, max_retries = 3)
retries = 0
begin
client.messages.send_text(
phone_number_id: 'phone_id',
to: '+1234567890',
body: 'Test message'
)
rescue KapsoClientRuby::Errors::GraphApiError => e
retries += 1
case e.retry_hint[:action]
when :retry
if retries <= max_retries
sleep(retries * 2) # Exponential backoff
retry
end
when :retry_after
if e.retry_hint[:retry_after_ms] && retries <= max_retries
sleep(e.retry_hint[:retry_after_ms] / 1000.0)
retry
end
end
raise # Re-raise if no retry
end
endHandle incoming webhooks from WhatsApp:
# Verify webhook signature
def verify_webhook_signature(payload, signature, app_secret)
require 'openssl'
sig_hash = signature.sub('sha256=', '')
expected_sig = OpenSSL::HMAC.hexdigest('sha256', app_secret, payload)
sig_hash == expected_sig
end
# In your webhook endpoint
def handle_webhook(request)
payload = request.body.read
signature = request.headers['X-Hub-Signature-256']
unless verify_webhook_signature(payload, signature, ENV['WHATSAPP_APP_SECRET'])
return [401, {}, ['Unauthorized']]
end
webhook_data = JSON.parse(payload)
# Process webhook data
webhook_data['entry'].each do |entry|
entry['changes'].each do |change|
if change['field'] == 'messages'
messages = change['value']['messages'] || []
messages.each do |message|
handle_incoming_message(message)
end
end
end
end
[200, {}, ['OK']]
end
def handle_incoming_message(message)
case message['type']
when 'text'
puts "Received text: #{message['text']['body']}"
when 'image'
puts "Received image: #{message['image']['id']}"
when 'interactive'
puts "Received interactive response: #{message['interactive']}"
end
endRun the test suite:
# Install development dependencies
bundle install
# Run tests
bundle exec rspec
# Run tests with coverage
bundle exec rspec --format documentation
# Run rubocop for style checking
bundle exec rubocopThe SDK includes VCR cassettes for testing without making real API calls:
# spec/spec_helper.rb
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = 'spec/vcr_cassettes'
config.hook_into :webmock
config.configure_rspec_metadata!
# Filter sensitive data
config.filter_sensitive_data('<ACCESS_TOKEN>') { ENV['WHATSAPP_ACCESS_TOKEN'] }
config.filter_sensitive_data('<PHONE_NUMBER_ID>') { ENV['PHONE_NUMBER_ID'] }
end
# In your tests
RSpec.describe 'Messages' do
it 'sends text message', :vcr do
client = KapsoClientRuby::Client.new(access_token: 'test_token')
response = client.messages.send_text(
phone_number_id: 'test_phone_id',
to: '+1234567890',
body: 'Test message'
)
expect(response.messages.first.id).to be_present
end
endSee the examples directory for comprehensive usage examples:
- Basic Messaging - Text, media, and template messages
- Media Management - Upload, download, and manage media
- Template Management - Create and manage templates
- Advanced Features - Kapso proxy features and analytics
- Ruby >= 2.7.0
- Faraday >= 2.0
- A WhatsApp Business Account with Cloud API access
- Valid access token from Meta or Kapso API key
Bug reports and pull requests are welcome on GitHub at https://github.com/gokapso/whatsapp-cloud-api-ruby.
git clone https://github.com/gokapso/whatsapp-cloud-api-ruby.git
cd whatsapp-cloud-api-ruby
bundle install# Run all tests
bundle exec rspec
# Run specific test file
bundle exec rspec spec/client_spec.rb
# Run with coverage
COVERAGE=true bundle exec rspec# Check style
bundle exec rubocop
# Auto-fix issues
bundle exec rubocop -AThis gem is available as open source under the terms of the MIT License.
- π WhatsApp Cloud API Documentation
- π Kapso Platform for enhanced features
- π Issue Tracker
- π§ Email: support@kapso.ai
See CHANGELOG.md for version history and updates.
Built with β€οΈ for the Kapso team
