Skip to content

PabloB07/kapso-client-ruby

Repository files navigation

Kapso Ruby Client

Kapso API Ruby SDK

Gem Version Ruby License: MIT

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.

Features

  • πŸš€ 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

Installation

Add this line to your application's Gemfile:

gem 'kapso-client-ruby'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install kapso-client-ruby

Rails Integration

For Rails applications, use the built-in generator to set up everything automatically:

rails generate kapso_client_ruby:install

This 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.

Quick Start

Basic Setup

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

Using Kapso Proxy (for enhanced features)

# 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
)

API Reference

Flows

Create and manage interactive WhatsApp Flows for rich data collection:

Create and Deploy a Flow

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

Send a Flow Message

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'
)

Handle Flow Webhooks

# 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

Manage Flows

# 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')

Messages

Send various types of messages with the Messages resource:

Text Messages

# 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
)

Media Messages

# 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'
  }
)

Group Messaging

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)

Location Messages

# 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'
)

Interactive Messages

# 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'
        }
      ]
    }
  ]
)

Interactive CTA URL Messages

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 characters
  • display_text: Max 20 characters
  • url: Must be valid HTTP/HTTPS URL
  • footer_text: Max 60 characters (optional)
  • header: Supports text, image, video, or document

Catalog Messages

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 characters
  • thumbnail_product_retailer_id: Product SKU to display as thumbnail (required)
  • footer_text: Max 60 characters (optional)

Template Messages

# 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' }
      ]
    }
  ]
)

Message Reactions

# 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
)

Message Status

# 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'
)

Media Management

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)

Template Management

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'
)

Advanced Features (Kapso Proxy)

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']
)

Configuration

Global Configuration

KapsoClientRuby.configure do |config|
  config.debug = true
  config.timeout = 60
  config.open_timeout = 10
  config.max_retries = 3
  config.retry_delay = 1.0
end

Client Configuration

client = KapsoClientRuby::Client.new(
  access_token: 'token',
  debug: true,
  timeout: 30,
  logger: Logger.new('whatsapp.log')
)

Debug Logging

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
)

Error Handling

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

Error Categories

  • :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

Automatic Retry Logic

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
end

Webhook Handling

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

Testing

Run 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 rubocop

Testing with VCR

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

Examples

See the examples directory for comprehensive usage examples:

Requirements

  • Ruby >= 2.7.0
  • Faraday >= 2.0
  • A WhatsApp Business Account with Cloud API access
  • Valid access token from Meta or Kapso API key

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/gokapso/whatsapp-cloud-api-ruby.

Development Setup

git clone https://github.com/gokapso/whatsapp-cloud-api-ruby.git
cd whatsapp-cloud-api-ruby
bundle install

Running Tests

# 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

Code Style

# Check style
bundle exec rubocop

# Auto-fix issues
bundle exec rubocop -A

License

This gem is available as open source under the terms of the MIT License.

Support

Changelog

See CHANGELOG.md for version history and updates.


Built with ❀️ for the Kapso team

About

Unofficial Ruby SDK API for kapso.ai platform

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published