Send WhatsApp with Ruby: REST API example (Net::HTTP and Rails)
To send WhatsApp with Ruby you POST with Net::HTTP to /api/v1/{instanceId}/send/text, with the x-api-key and x-api-secret headers and a JSON body with number and text. Net::HTTP ships with the standard library, so there is no gem to install. Below: text, media, retry, a Rails job and a Sinatra webhook.
Published on · 2 min read
What you need before starting
A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication uses x-api-key and x-api-secret. Keep credentials in ENV or in Rails credentials.
Test with cURL first, to tell code errors from account errors.
cURL
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"number":"5511999999999","text":"Olá! Teste de envio pela API."}'How to send a text message with Net::HTTP
The class below reuses the configuration, sets open_timeout and read_timeout and raises an error with status and body when the response is not 2xx. Use JSON.generate for the body.
Ruby
require "net/http"
require "json"
class ZapZap
class Erro < StandardError
attr_reader :status, :corpo
def initialize(status, corpo)
@status, @corpo = status, corpo
super("ZapZap #{status}: #{corpo}")
end
end
def initialize(instancia: ENV.fetch("ZAPZAP_INSTANCE_ID"),
key: ENV.fetch("ZAPZAP_API_KEY"),
secret: ENV.fetch("ZAPZAP_API_SECRET"))
@base = "https://api.zapzapapi.com/api/v1/#{instancia}"
@headers = { "Content-Type" => "application/json",
"x-api-key" => key, "x-api-secret" => secret }
end
def enviar_texto(numero, texto)
post("/send/text", number: numero, text: texto)
end
def post(caminho, dados)
uri = URI(@base + caminho)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 5
http.read_timeout = 30
r = http.post(uri.path, JSON.generate(dados), @headers)
raise Erro.new(r.code.to_i, r.body) unless r.is_a?(Net::HTTPSuccess)
JSON.parse(r.body)
end
end
puts ZapZap.new.enviar_texto("5511999999999", "Olá! Seu pedido saiu para entrega.")How to send an image, PDF and audio with Ruby
The route is /send/media. The type field sets the kind (image, video, audio, ptt, document, sticker and others), file takes the public URL, text is the caption and docName the document name. For a PDF, type document.
Ruby
class ZapZap
def enviar_pdf(numero, url, nome, legenda = "")
post("/send/media", number: numero, type: "document",
file: url, docName: nome, text: legenda)
end
endHow to retry and send from Rails with Active Job
Retry only transient errors (429 and 5xx) with growing waits. In Rails, use a job: Active Job's retry_on repeats by itself; the job only lets the error rise for 429 and 5xx and just logs 4xx, which will never work. That way the user's request does not wait for WhatsApp.
Ruby (Rails)
class EnviarWhatsappJob < ApplicationJob
queue_as :default
retry_on ZapZap::Erro, Net::OpenTimeout, Net::ReadTimeout,
wait: :polynomially_longer, attempts: 4
def perform(numero, texto)
ZapZap.new.enviar_texto(numero, texto)
rescue ZapZap::Erro => e
raise if e.status == 429 || e.status >= 500 # o retry_on repete
Rails.logger.error(e.message) # 4xx: repetir não resolve, só registra
end
end
# Uso: EnviarWhatsappJob.perform_later("5511999999999", "Seu pedido foi enviado.")How to receive replies with a Sinatra webhook
Set the URL in configure webhook and turn on the messages event. The incoming message is in data.message (text, chatid, fromMe). Ignore fromMe true and reply 200 right away. In Rails, the equivalent is a controller with skip_forgery_protection.
Signature validation and deduplication follow the webhooks in production guide.
Ruby (Sinatra)
require "sinatra"
require "json"
post "/webhooks/zapzap" do
evento = JSON.parse(request.body.read)
msg = evento.dig("data", "message")
if evento["event"] == "messages" && msg && !msg["fromMe"]
Thread.new { puts "mensagem de #{msg["chatid"]}: #{msg["text"]}" }
end
status 200
endChecklist before going to production
Credentials in ENV or credentials; open and read timeouts; errors with status and body; retry only for 429 and 5xx; sending through a job; a validated webhook signature; and sending only to people who agreed to receive.
Other languages: Python, Node.js, PHP, Go. All endpoints are in the API reference.
Quick summary
- Net::HTTP from the standard library is enough: no gem is needed.
- Set open_timeout and read_timeout and raise errors with status and body.
- In Rails, send through Active Job with retry_on for transient failures.
- In the webhook, ignore fromMe and reply 200 before processing.
Keep reading
Also read
Frequently asked questions
Can I use Faraday or HTTParty instead of Net::HTTP?
Yes. The API is REST: the x-api-key and x-api-secret headers and the JSON with number and text are the same in any HTTP client.
How do I store credentials in Rails?
In encrypted credentials or environment variables. Never in the repository or the front end.
Was the example run against the API?
The code uses the endpoints and fields from the API reference. Run the cURL at the top on your instance, then the Ruby, and check the response before going to production.
Can using an unofficial API get my number banned?
It can. An unofficial API goes against WhatsApp's terms and the number can be limited or banned. Opt-in, a human pace, warm-up and a clean list reduce the risk, but there is no guarantee. For a sanctioned channel, use Meta's official API.
Run Ruby on your instance
Create the account, connect a number and send the first message. R$15 per active number per month, no per-message fee.
