All integration guides

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

2. 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"]
end

4. 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
end

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

Other languages and frameworks