Packages
A production-grade, fully-typed Elixir client for the SumUp API (Checkouts, Readers, Customers, Transactions, Payouts, Receipts, Members, Memberships, Roles and Merchants) with telemetry, retries, cursor streaming, webhook handling, and RFC 9457 aware error handling.
Current section
Files
Jump to
Current section
Files
lib/sumup/config.ex
defmodule Sumup.Config do
raw_schema = [
api_key: [
type: :any,
type_doc: "`String.t()` or `{:system, String.t()}` or `{module(), atom(), list()}`",
doc:
"Your SumUp secret API key (`sk_live_...` / `sk_test_...`), or a restricted key. Mutually exclusive with `:access_token` -- if both are given, `:access_token` wins. Accepts a raw string, a `{:system, \"ENV_VAR\"}` tuple resolved at config-build time, or an `{module, function, args}` tuple for e.g. secrets managers."
],
access_token: [
type: :any,
type_doc: "`String.t()` or `{:system, String.t()}` or `{module(), atom(), list()}`",
doc: "An OAuth2 access token, resolved the same way as `:api_key`."
],
base_url: [
type: :string,
default: "https://api.sumup.com",
doc: "API base URL. Override for testing against a mock server."
],
receive_timeout: [type: :pos_integer, default: 30_000, doc: "Response timeout in ms."],
connect_timeout: [type: :pos_integer, default: 10_000, doc: "Connection timeout in ms."],
max_retries: [
type: :non_neg_integer,
default: 3,
doc: "Max retry attempts for retryable errors (429 and 5xx)."
],
retry_base_delay: [
type: :pos_integer,
default: 250,
doc: "Base delay in ms for exponential backoff (doubles per attempt, with jitter)."
],
retry_max_delay: [type: :pos_integer, default: 5_000, doc: "Maximum backoff delay in ms."],
telemetry_prefix: [
type: {:list, :atom},
default: [:sumup],
doc: "Prefix used for `:telemetry.span/3` events, see `Sumup.Telemetry`."
],
user_agent: [type: :string, doc: "Override the User-Agent header."],
req_options: [
type: :keyword_list,
default: [],
doc:
"Extra options merged into every `Req.new/1` call (e.g. `:plug` for testing, `:finch` pool name)."
],
finch_pool: [type: :atom, doc: "Name of a Finch pool to use instead of the default one."]
]
@schema NimbleOptions.new!(raw_schema)
@moduledoc """
Validated configuration for a `Sumup` client.
Build a config once (typically at application boot, or per-request if you
serve multiple SumUp merchants from one app) and pass it to every
resource function:
config = Sumup.Config.new!(api_key: {:system, "SUMUP_API_KEY"})
{:ok, checkout} = Sumup.Checkouts.get(config, "checkout_id")
## Options
#{NimbleOptions.docs(@schema)}
"""
@type auth ::
{:system, String.t()}
| {:system, String.t(), String.t()}
| {module(), atom(), list()}
| String.t()
@type t :: %__MODULE__{
api_key: String.t() | nil,
access_token: String.t() | nil,
base_url: String.t(),
receive_timeout: pos_integer(),
connect_timeout: pos_integer(),
max_retries: non_neg_integer(),
retry_base_delay: pos_integer(),
retry_max_delay: pos_integer(),
telemetry_prefix: [atom()],
user_agent: String.t(),
req_options: keyword(),
finch_pool: atom() | nil
}
defstruct api_key: nil,
access_token: nil,
base_url: "https://api.sumup.com",
receive_timeout: 30_000,
connect_timeout: 10_000,
max_retries: 3,
retry_base_delay: 250,
retry_max_delay: 5_000,
telemetry_prefix: [:sumup],
user_agent: "sumup-elixir/#{Application.spec(:sumup, :vsn) || "1.0.0"}",
req_options: [],
finch_pool: nil
@doc false
def schema, do: @schema
@doc """
Builds a config, raising `NimbleOptions.ValidationError` on invalid input.
"""
@spec new!(keyword()) :: t()
def new!(opts) do
opts = NimbleOptions.validate!(opts, @schema)
struct(__MODULE__, opts)
|> resolve_secret(:api_key)
|> resolve_secret(:access_token)
|> validate_auth!()
end
@doc """
Builds a config, returning `{:ok, config}` or `{:error, exception}`.
"""
@spec new(keyword()) :: {:ok, t()} | {:error, Exception.t()}
def new(opts) do
{:ok, new!(opts)}
rescue
e in [NimbleOptions.ValidationError, ArgumentError] -> {:error, e}
end
defp resolve_secret(%__MODULE__{} = config, field) do
case Map.get(config, field) do
nil -> config
value -> Map.put(config, field, resolve(value))
end
end
defp resolve({:system, var}), do: System.get_env(var)
defp resolve({:system, var, default}), do: System.get_env(var, default)
defp resolve({mod, fun, args}) when is_atom(mod) and is_atom(fun), do: apply(mod, fun, args)
defp resolve(value) when is_binary(value), do: value
defp validate_auth!(%__MODULE__{api_key: nil, access_token: nil}) do
raise ArgumentError, "Sumup.Config requires either :api_key or :access_token to be set"
end
defp validate_auth!(%__MODULE__{} = config), do: config
@doc false
@spec auth_header(t()) :: {String.t(), String.t()}
def auth_header(%__MODULE__{access_token: token}) when is_binary(token) do
{"authorization", "Bearer " <> token}
end
def auth_header(%__MODULE__{api_key: key}) when is_binary(key) do
{"authorization", "Bearer " <> key}
end
end