TrueLayer webhook signature verification, replay-attack protection, and typed dispatch.
Security model
- HMAC-SHA256 verification — constant-time comparison prevents timing attacks
- Replay protection — events older than
:webhook_replay_tolerance_secare rejected - Typed dispatch — handlers registered per event type via
on/3
Usage in a Phoenix controller
Add a CacheBodyReader plug to preserve the raw body for signature verification,
then call process/4 in your controller:
defmodule MyAppWeb.WebhookController do
use MyAppWeb, :controller
def truelayer(conn, _params) do
raw_body = conn.assigns[:raw_body]
sig = get_req_header(conn, "tl-signature") |> List.first()
ts = get_req_header(conn, "tl-timestamp") |> List.first()
case TruelayerClient.Webhooks.process(client, raw_body, sig, ts) do
:ok -> send_resp(conn, 200, "")
{:error, :bad_sig} -> send_resp(conn, 401, "invalid signature")
{:error, :replay} -> send_resp(conn, 401, "event too old")
{:error, reason} -> send_resp(conn, 500, inspect(reason))
end
end
endRegistering handlers
TruelayerClient.Webhooks.on(client, TruelayerClient.Webhooks.payment_executed(), fn event ->
%{"payload" => %{"payment_id" => id}} = event
MyApp.Payments.finalize(id)
:ok
end)Event type constants
Use the named functions (payment_executed/0, etc.) rather than raw strings
to avoid typos:
TruelayerClient.Webhooks.on(client, TruelayerClient.Webhooks.refund_executed(), handler)
Summary
Functions
Account holder verification completed.
Account holder verification failed.
Signup+ authorization URI expired.
Mandate authorized by the PSU.
Mandate failed.
Mandate revoked.
Merchant account payment failed.
Merchant account payment settled.
Create a new handler registry (ETS :bag table). Called once per client.
Register a handler for a specific event type.
Register a fallback handler called for any event type with no registered handler.
Payment authorized by the PSU.
Payment successfully executed.
Payment failed.
Payment link payment executed.
Payment funds settled into the merchant account.
Payout executed.
Payout failed.
Verify and dispatch a raw webhook payload.
Refund executed.
Refund failed.
VRP payment executed.
VRP payment failed.
Types
Functions
Account holder verification completed.
Account holder verification failed.
Signup+ authorization URI expired.
Mandate authorized by the PSU.
Mandate failed.
Mandate revoked.
Merchant account payment failed.
Merchant account payment settled.
@spec new_registry() :: registry()
Create a new handler registry (ETS :bag table). Called once per client.
@spec on(TruelayerClient.t(), event_type(), handler_fn()) :: :ok
Register a handler for a specific event type.
Multiple handlers per event type are supported; all are called in registration
order. If a handler returns {:error, reason}, dispatch halts and the error
is propagated, causing TrueLayer to retry the webhook delivery.
Example
TruelayerClient.Webhooks.on(client, TruelayerClient.Webhooks.payment_executed(), fn event ->
id = get_in(event, ["payload", "payment_id"])
MyApp.Payments.handle_executed(id)
:ok
end)
@spec on_fallback(TruelayerClient.t(), handler_fn()) :: :ok
Register a fallback handler called for any event type with no registered handler.
Payment authorized by the PSU.
Payment successfully executed.
Payment failed.
Payment link payment executed.
Payment funds settled into the merchant account.
Payout executed.
Payout failed.
@spec process(TruelayerClient.t(), binary(), String.t() | nil, String.t() | nil) :: :ok | {:error, term()}
Verify and dispatch a raw webhook payload.
Parameters
client—TruelayerClient.t()with webhook configurationbody— raw request body binary (must not be parsed before calling this)signature— value of theTl-Signaturerequest headertimestamp— value of theTl-Timestamprequest header (RFC 3339)
Return values
:ok— verified and successfully dispatched{:error, :bad_sig}— signature verification failed{:error, :replay}— event is outside the replay tolerance window{:error, {:decode_error, reason}}— JSON parsing failed{:error, reason}— a registered handler returned an error
Refund executed.
Refund failed.
VRP payment executed.
VRP payment failed.