Configuration
Whatsapp.configure do |config|
# Connection
config.auth_dir = "./my_session" # Session persistence directory
config.bridge_timeout = 15 # Seconds to wait for bridge startup
config.node_command = "node" # Path to node binary
config.auto_reconnect = true # Auto-reconnect on disconnect
config.max_reconnect_attempts = 5
config.reconnect_interval = 3 # Seconds between retries
# HTTP timeouts for bridge requests
config.http_open_timeout = 5 # Connection timeout (seconds)
config.http_read_timeout = 10 # Read timeout (seconds)
config.http_write_timeout = 10 # Write timeout (seconds)
# Safety preset (applies rate limits + anti-ban settings)
config.safety_preset = :strict # :strict, :moderate, :manual, :off
# Webhook signature verification
config.webhook_secret = ENV["WHATSAPP_WEBHOOK_SECRET"] # HMAC-SHA256
# Heartbeat
config.heartbeat_enabled = true # Periodic health checks
config.heartbeat_interval = 30 # Seconds
# Message handler (Rails — receives all incoming messages)
config.message_handler = "WhatsappMessageHandler"
# Logging
config.log_level = :info # :debug, :info, :warn, :error, :fatal
config.log_path = $stdout # IO, filepath string, or Logger instance
end
Safety presets
Safety presets configure all rate limiting and anti-ban settings at once. The default is :strict.
config.safety_preset = :strict # conservative — recommended for new accounts
config.safety_preset = :moderate # relaxed — for established accounts
config.safety_preset = :manual # no changes — set everything manually
config.safety_preset = :off # no limits — dev/testing only!
| Setting | :strict | :moderate | :off |
|---|---|---|---|
| Messages/min | 6 | 10 | unlimited |
| Messages/hour | 40 | 80 | unlimited |
| Messages/day | 150 | 300 | unlimited |
| Per recipient/hour | 5 | 10 | unlimited |
| New chats/day | 10 | 20 | unlimited |
| Min delay between msgs | 3.0s | 2.0s | 0s |
| Delay jitter (gaussian) | 5.0s | 3.0s | 0s |
| Post-connect delay | 60s | 30s | 0s |
| Typing simulation | yes | no | no |
| Auto read receipts | yes | no | no |
| Ramp-up after idle | yes | yes | no |
| Account warmup (days) | 7 | 3 | 0 |
You can override individual settings after applying a preset:
config.safety_preset = :strict
config.max_new_chats_per_day = 15 # override just this
All safety settings
Whatsapp.configure do |config|
config.safety_preset = :manual
# Rate limits
config.rate_limiting_enabled = true
config.rate_limit_per_minute = 8
config.rate_limit_per_hour = 60
config.rate_limit_per_day = 200
config.rate_limit_per_recipient_per_hour = 5
config.min_delay_between_messages = 3.0 # seconds
config.delay_jitter = 5.0 # max gaussian jitter
# Anti-ban
config.max_new_chats_per_day = 10 # new conversations per day
config.post_connect_delay = 60.0 # wait before first message
config.ramp_up_enabled = true # warmup after idle periods
config.idle_threshold = 1800.0 # idle threshold (seconds)
config.warmup_messages = 5 # messages to ramp up over
config.warmup_delay_factor = 3.0 # extra delay multiplier during warmup
# Behavior simulation
config.typing_simulation = true # auto "composing" before text messages
config.auto_read_receipts = true # auto read receipts for incoming
config.auto_presence_updates = false # auto available/unavailable on connect
# Progressive account warmup
config.ramp_up_days = 7 # day 1 = 1/7 limits, day 7 = full
config.ramp_up_started_at = nil # auto-set on first message
end
Progressive account warmup
New accounts start with reduced limits that increase daily over ramp_up_days:
- Day 1: 1/7 of all limits (~21 msgs/day, ~1 new chat)
- Day 4: 4/7 of all limits (~85 msgs/day, ~6 new chats)
- Day 7+: full limits (150 msgs/day, 10 new chats)
ramp_up_started_at is auto-set when the first message is sent. To reset the warmup:
Whatsapp.configuration.ramp_up_started_at = Time.now
Safety monitoring
client.safety_status
# => {
# risk: :low, # :low, :medium, :high
# preset: :strict,
# warmup_day: 3,
# warmup_factor: 0.43,
# limits: {
# per_day: { used: 12, limit: 64, usage_pct: 18.8 },
# per_hour: { used: 5, limit: 17, usage_pct: 29.4 },
# new_chats: { used: 2, limit: 4, usage_pct: 50.0 }
# },
# features: {
# typing_simulation: true,
# auto_read_receipts: true,
# ramp_up_enabled: true,
# post_connect_delay: 60.0
# }
# }
Bypass safety
For a single urgent message, skip all protections:
client.send_message(to: "+33612345678", text: "Urgent", unsafe: true)
unsafe: truebypasses rate limiting, typing simulation, and all anti-ban protections. Use only when absolutely necessary.
Rate limiting
Two layers of protection
- Ruby side (
Whatsapp::RateLimiter) — primary control with multi-level limits, gaussian jitter, warmup, and new chat tracking. - Bridge side (Node.js) — last line of defense, returns HTTP 429 if limits are exceeded.
Handling rate limits
begin
client.send_message(to: "+33...", text: "hello")
rescue Whatsapp::RateLimited => e
e.period # :minute, :hour, :day
e.limit # 6
e.current # 6
e.recipient # nil (global) or "+33..." (per-recipient)
end
Monitoring
client.rate_limit_stats
# => { last_minute: 3, last_hour: 25, last_day: 102, ..., new_chats_today: 4, warmup_day: 3 }
Health check
A background heartbeat thread pings the bridge and WhatsApp connection periodically. Starts on connect, stops on disconnect.
session.heartbeat.check_now
session.heartbeat.healthy? # => true/false
session.heartbeat.last_status # => :ok, :bridge_down, :whatsapp_disconnected, ...
session.heartbeat.consecutive_failures # => 0
# Via AccountProxy
hotel.whatsapp.healthy?
hotel.whatsapp.health
Health events
session.on('health.ok') { |data| ... }
session.on('health.failure') { |data| ... }
session.on('health.recovered') { |data| ... }
Failure reasons: :bridge_down, :bridge_not_initialized, :bridge_unhealthy, :whatsapp_disconnected, :error.
Security
HTTP timeouts
All bridge HTTP requests have configurable timeouts to prevent hanging connections:
Whatsapp.configure do |config|
config.http_open_timeout = 5 # default: 5s
config.http_read_timeout = 10 # default: 10s
config.http_write_timeout = 10 # default: 10s
end
Webhook signature verification
Verify the authenticity of incoming webhook payloads with HMAC-SHA256:
Whatsapp.configure do |config|
config.webhook_secret = ENV["WHATSAPP_WEBHOOK_SECRET"]
end
Verify manually in your controller:
payload = request.raw_post
signature = request.headers["X-Whatsapp-Signature"]
Whatsapp::WebhookSignature.verify!(payload, signature, Whatsapp.configuration.webhook_secret)
Methods available:
| Method | Description |
|---|---|
WebhookSignature.sign(payload, secret) | Generate a sha256=... signature |
WebhookSignature.valid?(payload, sig, secret) | Returns true/false |
WebhookSignature.verify!(payload, sig, secret) | Raises WebhookSignatureError on failure |
Session file permissions
Auth session files are automatically secured:
- Auth directory: created with
0700permissions (owner-only access) - Auth files (creds, keys): set to
0600after each credential update - Both Ruby and Node.js sides enforce these permissions
Path traversal protection
Session names are validated to prevent directory traversal attacks:
Whatsapp.session("../../etc/passwd")
# => ArgumentError: Invalid session name "../../etc/passwd": must not contain .., / or \
Valid names: alphanumeric, hyphens, underscores (e.g. hotel_42, my-session).
Core dump protection
Core dumps are disabled at bridge startup to prevent session credentials from leaking to disk:
Process.setrlimit(Process::RLIMIT_CORE, 0, 0)
Log secret filtering
Sensitive keys are automatically redacted in log output:
logger.info "config", webhook_secret: "my_key"
# => [whatsapp-ruby] [14:30:00] config webhook_secret="[FILTERED]"
Filtered keys: secret, token, api_key, password, credentials, creds, webhook_secret, auth_token, access_token, refresh_token.
Other built-in protections
| Protection | Details |
|---|---|
| No eval | No dynamic code execution from user input |
| No shell interpolation | All process spawning uses array form (Process.spawn, system) |
| Localhost binding | Bridge listens on 127.0.0.1 only, never 0.0.0.0 |
| Auth folder isolation | Auth directory never served via HTTP, never exposed in error responses |
| Input sanitization | Phone numbers stripped to digits only via gsub(/[^\d]/, "") |