Skip to content

Latest commit

 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

anu

Hex.pm Docs License

Composable Elixir SDK for the WhatsApp Business Cloud API.

Docs · Landing page

Installation

Requires Elixir 1.19+ and OTP 27+.

def deps do
  [
    {:anu, "~> 0.1"}
  ]
end

Anu does not start any processes. Start a Finch instance in your own supervision tree:

children = [
  {Finch, name: MyApp.Finch}
]

Quick start

Delivery always goes through an explicit Anu.Client holding the credentials for one WhatsApp phone number:

client =
  Anu.Client.new(
    finch: MyApp.Finch,
    access_token: System.fetch_env!("WHATSAPP_ACCESS_TOKEN"),
    phone_number_id: System.fetch_env!("WHATSAPP_PHONE_NUMBER_ID")
  )

Anu.Message.new("5511999999999")
|> Anu.Message.text("Your order has shipped!")
|> Anu.Message.buttons([
  {"Track", :track_order},
  {"Cancel", :cancel}
])
|> Anu.deliver(client)

Multiple numbers / WABAs

Build one client per phone number (or per WABA, when you hold several access tokens) and deliver through the matching one:

support = Anu.Client.new(finch: MyApp.Finch, access_token: "EAAG...", phone_number_id: "111")
sales   = Anu.Client.new(finch: MyApp.Finch, access_token: "EAAG...", phone_number_id: "222")

Anu.Message.new(to) |> Anu.Message.text("hi") |> Anu.deliver(support)

See Anu.Client for all options (:adapter, :api_version, :cloud_url, :cloud_api_key).

Composing messages

Messages are built by piping through composable functions, with no macros or DSLs:

# Rich interactive message
Anu.Message.new(to)
|> Anu.Message.header_image("https://example.com/menu.jpg")
|> Anu.Message.body("Check out our new menu")
|> Anu.Message.footer("Open daily 8am-10pm")
|> Anu.Message.buttons([
  {"Order now", :order},
  {"View hours", :hours}
])
|> Anu.deliver(client)

# List message with sections
Anu.Message.new(to)
|> Anu.Message.body("What can I help you with?")
|> Anu.Message.button_text("Choose an option")
|> Anu.Message.sections([
  Anu.Section.new("Orders", [
    Anu.Row.new("order_status", "Order status"),
    Anu.Row.new("order_cancel", "Cancel order")
  ])
])
|> Anu.deliver(client)

# Location
Anu.Message.new(to)
|> Anu.Message.location(-23.5505, -46.6333, name: "Sao Paulo", address: "SP, Brazil")
|> Anu.deliver(client)

# React to a message
Anu.Message.new(to) |> Anu.Message.react("👍", message_id: original_msg_id) |> Anu.deliver(client)

# Template
Anu.Message.new(to)
|> Anu.Message.template("order_confirmation", "pt_BR", [
  Anu.Template.body_param("Joao"),
  Anu.Template.body_param("#12345")
])
|> Anu.deliver(client)

Webhook handling

Drop the plug into your Phoenix or Plug router:

forward "/webhooks/whatsapp", Anu.Webhook.Plug,
  handler: MyApp.WhatsAppHandler,
  secret: System.fetch_env!("WHATSAPP_APP_SECRET"),
  verify_token: System.fetch_env!("WHATSAPP_VERIFY_TOKEN")

:secret and :verify_token are optional and fall back to config :anu, app_secret: ... / config :anu, verify_token: .... In multi-app setups, mount one forward per app with its own secrets.

Inbound events carry phone_number_id and display_phone_number from the payload metadata, so you can route them back to the right client.

Implement the handler behaviour:

defmodule MyApp.WhatsAppHandler do
  @behaviour Anu.Webhook.Handler

  @impl true
  def handle_event(:message_received, %Anu.Event.Message{} = msg) do
    msg.from
    |> Anu.Message.new()
    |> Anu.Message.text("Thanks!")
    |> Anu.deliver(client_for(msg.phone_number_id))
  end

  def handle_event(_event, _payload), do: :ok
end

AI primitives (Anu.AI)

Optional cloud-backed AI helpers (classify/3, extract/3, reply/3, summarize/2) powered by anu_cloud. Put your anu_sk_... key on the client:

client =
  Anu.Client.new(
    finch: MyApp.Finch,
    access_token: "...",
    phone_number_id: "...",
    cloud_api_key: System.fetch_env!("ANU_API_KEY")
  )

{:ok, %{"intent" => intent}} =
  Anu.AI.classify("my order is late", [intents: ["support", "sales", "spam"]], client)

{:ok, %{"reply" => reply}} = Anu.AI.reply("when does my package arrive?", [tone: "warm"], client)

All Anu.AI functions return {:ok, decoded_map} (string keys) or {:error, %Anu.Error{} | :cloud_not_configured}. See Anu.AI for the full reference.

Adapters

Like Swoosh, Anu supports multiple adapters, chosen per client:

# Production - Meta Cloud API (default)
Anu.Client.new(finch: MyApp.Finch, access_token: "...", phone_number_id: "...")

# Development - logs messages to console
Anu.Client.new(adapter: Anu.Adapters.Local, finch: MyApp.Finch)

# Test - stores messages in-process
Anu.Client.new(adapter: Anu.Adapters.Test, finch: MyApp.Finch)

In tests:

import Anu.TestAssertions

test "sends order confirmation" do
  client = Anu.Client.new(adapter: Anu.Adapters.Test, finch: MyApp.Finch)

  MyApp.send_confirmation(order, client)

  assert_message_sent(to: order.customer_phone, body: "Your order has shipped!")
end

Built with 💜 by @zoedsoupe

About

whatsapp api for devs, like slack build kit

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages