Surge

Elixir SDK

Install and authenticate

Add to your mix.exs:

defp deps do
  [
    {:surge_api, "~> 0.2.0"}
  ]
end
mix deps.get

Set your API key in config:

# config/runtime.exs
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}")
end

Webhook 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:

# endpoint.ex
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
end

The 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

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)}")
end

The signature_header is the value of the surge-signature HTTP header, formatted as t=timestamp,v1=hex_signature.