Packages

A complete, production-grade Elixir client for the Mercury Banking API (accounts, transactions, recipients, invoices, payments, treasury, webhooks, and more), with typed errors, retry with backoff, and lazy auto-paginating streams.

Current section

Files

Jump to
mercury_client lib mercury oauth2.ex
Raw

lib/mercury/oauth2.ex

defmodule Mercury.OAuth2Token do
@moduledoc "The response from a successful OAuth2 token exchange."
@type t :: %__MODULE__{
access_token: String.t(),
token_type: String.t(),
expires_in: integer(),
scope: String.t() | nil
}
defstruct [:access_token, :token_type, :expires_in, :scope]
@doc false
def from_json(map) when is_map(map) do
%__MODULE__{
access_token: map["access_token"],
token_type: map["token_type"],
expires_in: map["expires_in"],
scope: map["scope"]
}
end
end
defmodule Mercury.OAuth2 do
@moduledoc """
Helpers for Mercury's OAuth2 authorization flow (partner integrations).
Unlike every other resource module, these functions do not send the
client's API key — `authorize_url/2` and `exchange_code/2` talk to the
public OAuth endpoints directly, using only the client's configured
`:base_url` and `:timeout`.
"""
alias Mercury.{Client, OAuth2Token, Support}
@doc """
Builds the authorization redirect URL for the OAuth2 consent flow.
Redirect users to this URL to begin authorization. `GET /oauth/authorize`
## Options
* `:client_id` — required.
* `:redirect_uri` — required.
* `:scope` — optional space-separated scope string.
* `:state` — optional opaque state parameter for CSRF protection.
"""
@spec authorize_url(client :: Client.t(), opts :: keyword()) :: String.t()
def authorize_url(%Client{base_url: base_url}, opts) do
client_id = Keyword.fetch!(opts, :client_id)
redirect_uri = Keyword.fetch!(opts, :redirect_uri)
query =
[client_id: client_id, redirect_uri: redirect_uri, response_type: "code"]
|> maybe_put(:scope, Keyword.get(opts, :scope))
|> maybe_put(:state, Keyword.get(opts, :state))
|> URI.encode_query()
base_url <> "/oauth/authorize?" <> query
end
@doc """
Exchanges an authorization code for an access token. `POST /oauth/token`
## Options
* `:code` — required, the authorization code received on your redirect URI.
* `:redirect_uri` — required, must match the one used in `authorize_url/2`.
* `:client_id` — required.
* `:client_secret` — required.
* `:grant_type` — defaults to `"authorization_code"`.
"""
@spec exchange_code(client :: Client.t(), opts :: keyword()) ::
{:ok, OAuth2Token.t()} | {:error, Exception.t()}
def exchange_code(%Client{base_url: base_url, timeout: timeout}, opts) do
body = %{
grant_type: Keyword.get(opts, :grant_type, "authorization_code"),
code: Keyword.fetch!(opts, :code),
redirect_uri: Keyword.fetch!(opts, :redirect_uri),
client_id: Keyword.fetch!(opts, :client_id),
client_secret: Keyword.fetch!(opts, :client_secret)
}
result =
Req.request(
method: :post,
url: base_url <> "/oauth/token",
json: body,
receive_timeout: timeout,
headers: [{"x-mercury-sdk", Mercury.sdk_version()}]
)
case result do
{:ok, %Req.Response{status: status, body: resp_body}} when status in 200..299 ->
{:ok, OAuth2Token.from_json(resp_body)}
{:ok, %Req.Response{status: status, body: resp_body}} ->
{:error,
%Mercury.APIError{
message: "OAuth2 token exchange failed: #{inspect(resp_body)}",
code: :oauth_error,
status: status,
body: (is_map(resp_body) && resp_body) || nil
}}
{:error, %{reason: :timeout}} ->
{:error, %Mercury.TimeoutError{timeout_ms: timeout}}
{:error, exception} ->
{:error, %Mercury.NetworkError{message: Exception.message(exception), reason: exception}}
end
end
@doc "Same as `exchange_code/2` but raises on error."
@spec exchange_code!(client :: Client.t(), opts :: keyword()) :: OAuth2Token.t()
def exchange_code!(client, opts), do: Support.bang!(exchange_code(client, opts))
defp maybe_put(list, _key, nil), do: list
defp maybe_put(list, key, value), do: Keyword.put(list, key, value)
end