Elixir SDK
The Elixir SDK provides a typed client for the Surge API plus a Phoenix-friendly WebhookPlug and WebhookHandler behaviour for handling inbound events. It's published as surge_api on Hex and supports Elixir 1.14 and newer.
This page covers installing the package, configuring the client for Phoenix, sending your first request, handling errors, and wiring webhook routes with either the plug or the behaviour.
Install and authenticate
Add to your mix.exs:
defp deps do
[
{:surge_api, "~> 0.2.0"}
]
endmix deps.getSet your API key in config:
config :surge_api, api_key: System.get_env("SURGE_API_KEY")Or create a client directly:
client = Surge.Client.new("sk_live_your_key_here")Your first request
Using the default client (reads from application config):
{:ok, message} = Surge.Messages.create(
"acct_01jrzhe8d9enptypyx360pcmxj",
%{to: "+18015551234", body: "Your appointment is confirmed for Friday at 2pm."}
)
IO.puts(message.id) # "msg_01j..."
IO.puts(message.status) # "queued"Or with an explicit client:
client = Surge.Client.new("sk_live_your_key_here")
{:ok, message} = Surge.Messages.create(
client,
"acct_01jrzhe8d9enptypyx360pcmxj",
%{to: "+18015551234", body: "Your appointment is confirmed for Friday at 2pm."}
)Retrieving a message
{:ok, message} = Surge.Messages.get("msg_01kqbhwra9egg8sdcsp9veg391")
IO.inspect(message.body)Error handling
The SDK returns tagged tuples:
case Surge.Messages.create("acct_01j...", %{to: "+18015551234", body: "Hello"}) do
{:ok, message} ->
IO.inspect(message.id)
{:error, %Surge.Error{type: "opted_out", message: msg}} ->
Logger.warning("Contact opted out: #{msg}")
{:error, %Surge.Error{type: type, message: msg}} ->
Logger.error("Surge error #{type}: #{msg}")
endWebhook handling
WebhookPlug for Phoenix
The SDK includes Surge.WebhookPlug that handles signature verification and event parsing. Add it to your endpoint.ex before Plug.Parsers:
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: System.get_env("SURGE_WEBHOOK_SECRET")For runtime configuration, pass a tuple or function as the secret:
plug Surge.WebhookPlug,
at: "/webhook/surge",
handler: MyAppWeb.SurgeHandler,
secret: {Application, :get_env, [:myapp, :surge_webhook_secret]}WebhookHandler behaviour
Implement the Surge.WebhookHandler behaviour to process events. The callback receives a Surge.Events.Event struct with an atom type and a data field:
defmodule MyAppWeb.SurgeHandler do
@behaviour Surge.WebhookHandler
@impl true
def handle_event(%Surge.Events.Event{type: :message_received} = event) do
IO.inspect(event.data.body, label: "Received")
:ok
end
@impl true
def handle_event(%Surge.Events.Event{type: :contact_opted_out} = event) do
MyApp.handle_opt_out(event.data)
:ok
end
@impl true
def handle_event(_event), do: :ok
endThe callback should return :ok or {:ok, term} for success, or :error or {:error, reason} to signal a failure (which returns HTTP 400 to Surge).
Manual signature verification
The Elixir SDK currently consumes the older surge-signature header (format t=<ts>,v1=<hex>) with a different signed-payload composition than the Standard Webhooks spec used by Python / TypeScript / Ruby. Migrating to Standard Webhooks (webhook-signature) this week — this Note will be removed once the migration ships. See Signature validation for both formats.
If you're not using Surge.WebhookPlug, verify signatures with Surge.Webhook.construct_event/3:
case Surge.Webhook.construct_event(raw_body, signature_header, webhook_secret) do
{:ok, %Surge.Events.Event{} = event} ->
handle_event(event)
{:error, reason} ->
Logger.warning("Invalid webhook signature: #{inspect(reason)}")
endThe signature_header is the value of the surge-signature HTTP header, formatted as t=timestamp,v1=hex_signature.