Surge

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"}
  ]
end
mix deps.get

Set 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}")
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:

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

Note

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

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