Ruby integration
Send WhatsApp messages with Ruby on Rails
A complete, runnable Ruby on Rails example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Ruby on Rails talks to the API over ordinary HTTP, so you can paste this straight into an existing project.
About 5 minutes to your first delivered message
1. Setup
Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your Ruby on Rails code as an environment variable. Never commit the key or ship it in client-side code.
# net/http is part of the standard library.
# Store credentials with Rails' encrypted secrets:
bin/rails credentials:edit2. Send a text message
One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.
# app/services/spwp.rb
require "net/http"
require "json"
class Spwp
API_BASE = "https://api.spwp.app/api/v1".freeze
def self.send_text(to:, text:)
uri = URI("#{API_BASE}/public/messages/text")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = Rails.application.credentials.dig(:spwp, :api_key)
request["Content-Type"] = "application/json"
request.body = { to: to, text: text }.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 10) do |http|
http.request(request)
end
body = JSON.parse(response.body)
raise "SPWP #{body['code']}: #{body['message']}" if response.code.to_i >= 400
body["data"]
end
end
# Spwp.send_text(to: "9647503505440", text: "Your order has shipped.")3. Send an image, document or other media
Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.
def self.send_image(to:, source:, caption: nil)
uri = URI("#{API_BASE}/public/messages/image")
request = Net::HTTP::Post.new(uri)
request["X-API-Key"] = Rails.application.credentials.dig(:spwp, :api_key)
request["Content-Type"] = "application/json"
request.body = { to: to, source: source, caption: caption }.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
JSON.parse(response.body)["data"]
end4. Receive replies and delivery receipts
Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.
# app/controllers/spwp_webhooks_controller.rb
class SpwpWebhooksController < ActionController::API
before_action :verify_signature
def create
event = JSON.parse(request.raw_post)
# …enqueue a job, then return immediately…
head :ok
end
private
def verify_signature
ts = request.headers["X-WA-Timestamp"]
v1 = request.headers["X-WA-Signature"].to_s[/v1=([a-f0-9]+)/, 1]
return head :bad_request if ts.blank? || v1.blank?
# Replay protection: five-minute window.
return head :bad_request if ((Time.now.to_f * 1000) - ts.to_i).abs > 5 * 60_000
secret = Rails.application.credentials.dig(:spwp, :webhook_secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{request.raw_post}")
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, v1)
end
endError handling
Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.
Next steps
The full reference covers every endpoint, field and error code, plus the webhook payload shapes.