How to Send OTP in Ruby on Rails (2026 Guide)
Send and verify SMS OTPs from a Ruby on Rails app using the StartMessaging API. Includes Net::HTTP examples, a service object, controller actions, and rspec tests.
StartMessaging Team
Engineering
Ruby on Rails is still a fast way to ship a backend, and adding phone verification doesn’t need to be complicated. This guide wires up the StartMessaging OTP API from a Rails app using only Net::HTTP and a clean service object.
Prerequisites
- Rails 6, 7, or 8 with Ruby 3.0+.
- A StartMessaging account and an API key.
Add the key to your Rails credentials or to .env:
# config/credentials.yml.enc
startmessaging:
api_key: sm_live_xxxxxxxxxxxxxxxxxxxxCreate a Rails Service Object
Create app/services/start_messaging_otp.rb:
require "net/http"
require "json"
require "uri"
class StartMessagingOtp
BASE_URL = "https://api.startmessaging.com".freeze
def self.send_otp(phone_number, idempotency_key: SecureRandom.uuid)
post("/otp/send", {
phoneNumber: phone_number,
idempotencyKey: idempotency_key
})
end
def self.verify_otp(request_id, code)
post("/otp/verify", { requestId: request_id, otpCode: code })
end
def self.post(path, body)
uri = URI(BASE_URL + path)
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["X-API-Key"] = Rails.application.credentials.dig(:startmessaging, :api_key)
req.body = body.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
raise "OTP API error: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body, symbolize_names: true)
end
endSend an OTP
result = StartMessagingOtp.send_otp("+919876543210")
request_id = result.dig(:data, :requestId)
expires_at = result.dig(:data, :expiresAt)Verify the OTP
verified = StartMessagingOtp.verify_otp(request_id, "482910").dig(:data, :verified)Controller Actions
# app/controllers/api/auth_controller.rb
class Api::AuthController < ApplicationController
def send_otp
res = StartMessagingOtp.send_otp(params.require(:phone_number))
session[:otp_request_id] = res.dig(:data, :requestId)
render json: { ok: true, expires_at: res.dig(:data, :expiresAt) }
rescue => e
render json: { error: e.message }, status: :bad_gateway
end
def verify_otp
request_id = session.delete(:otp_request_id)
res = StartMessagingOtp.verify_otp(request_id, params.require(:code))
if res.dig(:data, :verified)
render json: { verified: true }
else
render json: { verified: false }, status: :unauthorized
end
end
endTesting with RSpec
Stub the HTTP call so your tests don’t bill your wallet:
# spec/services/start_messaging_otp_spec.rb
require "rails_helper"
require "webmock/rspec"
RSpec.describe StartMessagingOtp do
it "sends an OTP" do
stub_request(:post, "https://api.startmessaging.com/otp/send")
.to_return(status: 200, body: { data: { requestId: "abc", expiresAt: "...", attemptsLeft: 3 } }.to_json)
res = described_class.send_otp("+919876543210")
expect(res.dig(:data, :requestId)).to eq("abc")
end
endBest Practices
- Always use idempotency keys — see our idempotency guide.
- Throttle send actions with rack-attack or your own rate limiter to prevent abuse.
- Validate Indian numbers with the
phonelibgem before sending. - Never log OTP codes or request bodies in production.
FAQ
See pricing or read our end-to-end OTP verification flow guide for the architectural picture.
Related Articles
Step-by-step Node.js tutorial to send and verify OTP via SMS using the StartMessaging API. Includes fetch examples, error handling, and verification flow.
Python tutorial to send and verify OTP via SMS using the requests library and StartMessaging API. Includes Flask and Django integration examples.
Architecture guide for building a production-ready OTP verification flow covering generation, delivery, verification, retry logic, expiry, and security best practices.
Ready to Send OTPs?
Integrate StartMessaging in 5 minutes. No DLT registration required.