Arafa

Arafa gives you a single, uniform Ruby interface to multiple SMS/USSD/Airtime gateways used in Kenya, starting with Africa's Talking and Wasiliana. Every send-shaped call returns a Dry::Monads::ResultSuccess(Arafa::SendResult) or Failure(Arafa::Error subclass) — instead of raising on expected provider errors, so a rejected phone number or a bad API key is something you pattern-match on, not rescue.

Installation

Install the gem and add it to the application's Gemfile by executing:

bundle add arafa

If bundler is not being used to manage dependencies, install the gem by executing:

gem install arafa

Configuration

Configure credentials once, wherever your app boots (e.g. config/initializers/arafa.rb in Rails); provider instances then pick them up automatically instead of needing credentials passed on every call:

Arafa.configure do |config|
  config.wasiliana.api_key = ENV["WASILIANA_API_KEY"]
  config.africas_talking.api_key = ENV["AT_API_KEY"]
  config.africas_talking.username = ENV["AT_USERNAME"]
end

Testing against Africa's Talking's sandbox app? Set config.africas_talking.sandbox = true to route SMS sends through https://api.sandbox.africastalking.com/version1/messaging instead of the production bulk endpoint, and use the sandbox app's own username (sandbox) and API key. Note the sandbox only delivers to phone numbers registered as Simulator Numbers in the sandbox app's dashboard — any other number comes back as a Failure(Arafa::InvalidRequestError).

Sending an SMS

provider = Arafa::Wasiliana.new(text: "Hello", to: "0728833100")
result = provider.send
result.success? # => true
result.value!.message_id

Wasiliana requires a Sender ID/ShortCode on every send — pass it explicitly with from:, or set config.default_sender once in Arafa.configure so every call can omit it, as above.

to: accepts a single number or an array, in any of the accepted Kenyan formats (07..., 01..., +254..., 254...) — Arafa normalizes each one to 2547XXXXXXXX / 2541XXXXXXXX before it's sent. The same shape works for Africa's Talking:

Arafa::AfricasTalking.new(text: "Hello", to: %w[0711123456 0722334455], from: "MyApp").send

Prefer a one-off call without instantiating the provider yourself? Use the class-level shortcut:

Arafa::Wasiliana.send(text: "Hello", to: "0728833100")

Or dispatch to a provider chosen at runtime (e.g. from config/env) by name, via the provider registry:

Arafa.send(:wasiliana, text: "Hello", to: "0728833100")
Arafa.send(:africas_talking, text: "Hello", to: "0728833100", from: "MyApp")

Reading a validation failure

Requests are validated locally before any HTTP call is made — e.g. Wasiliana requires a from: Sender ID/ShortCode on every send, so leaving it out (with no default_sender configured) comes back as Failure(Arafa::ValidationError) without hitting the network:

result = Arafa::Wasiliana.new(text: "Hello", to: "0728833100").send

result.success? # => false
result.failure  # => #<Arafa::ValidationError: {from: ["must be filled"]}>
result.failure.message # => "{from: [\"must be filled\"]}"

Note that malformed input the phone number type itself rejects (e.g. to: "not-a-number") raises Dry::Types::ConstraintError at construction time instead, since it fails before a Dry::Monads::Result even exists to wrap it — only failures caught by a provider's Contract (§5.3) come back as result.failure.

result.failure is always an Arafa::Error subclass — ValidationError for a rejected payload, or AuthenticationError / InvalidRequestError / RateLimitError / ProviderServerError / NetworkError once the request actually reaches the provider (see lib/arafa/errors.rb).

USSD (callback-only — no #send)

Neither provider exposes an outbound "send USSD" call — USSD is inbound-only. You register a service code and callback URL with the provider; they POST each subscriber interaction to your app, and you must respond within 10 seconds with a CON /END -prefixed plain-text body. Arafa only helps with parsing that inbound payload and building the reply — there is no Arafa::Ussd::Wasiliana.new(...).send-style call, since there's nothing to send:

# Inside your controller/route handling the provider's callback POST:
request = Arafa::Ussd::Request.from_africas_talking(params)
# or: Arafa::Ussd::Request.from_wasiliana(params)

if request.text.empty?
  render plain: Arafa::Ussd::Response.continue("Welcome\n1. Check balance\n2. Buy airtime")
else
  render plain: Arafa::Ussd::Response.end("Thanks for using our service")
end

Airtime

Top up a subscriber's airtime directly, without going through the SMS/USSD flow. Both providers share the same request shape (phone_number:, currency_code:, amount:) and return the same Arafa::SendResult:

Arafa::Airtime::Wasiliana.new(
  phone_number: "0712345678",
  currency_code: "KES",
  amount: "100.50",
  callback: "https://example.com/callback"
).send

Arafa::Airtime::AfricasTalking.new(
  phone_number: %w[0712345678 0722345678],
  currency_code: "KES",
  amount: "100.50"
).send

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kamalogudah/arafa. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Arafa project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.