Packages

phoenix_kit

1.7.11
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web controllers billing_webhook_controller.ex
Raw

lib/phoenix_kit_web/controllers/billing_webhook_controller.ex

defmodule PhoenixKitWeb.Controllers.BillingWebhookController do
@moduledoc """
Handles webhooks from payment providers (Stripe, PayPal, Razorpay).
This controller receives webhook events from payment providers,
verifies their signatures, and processes them through the WebhookProcessor.
## Webhook URLs
Configure these URLs in your payment provider dashboards:
- Stripe: `https://yourdomain.com/phoenix_kit/webhooks/billing/stripe`
- PayPal: `https://yourdomain.com/phoenix_kit/webhooks/billing/paypal`
- Razorpay: `https://yourdomain.com/phoenix_kit/webhooks/billing/razorpay`
## Security
All webhooks verify signatures to ensure they come from legitimate sources.
Invalid signatures result in 401 Unauthorized responses.
## Idempotency
Events are logged in the `phoenix_kit_webhook_events` table with their
event IDs. Duplicate events are detected and ignored to prevent
double-processing.
"""
use Phoenix.Controller,
formats: [:json]
alias PhoenixKit.Billing.Providers
alias PhoenixKit.Billing.WebhookProcessor
alias PhoenixKit.Settings
require Logger
@doc """
Handles Stripe webhooks.
Expects the raw body in `conn.assigns.raw_body` (set by a custom Plug).
Signature is read from the `stripe-signature` header.
"""
def stripe(conn, _params) do
handle_webhook(conn, :stripe, "stripe-signature")
end
@doc """
Handles PayPal webhooks.
PayPal verification requires multiple headers for signature verification.
"""
def paypal(conn, _params) do
handle_webhook(conn, :paypal, "paypal-transmission-sig")
end
@doc """
Handles Razorpay webhooks.
Signature is read from the `x-razorpay-signature` header.
"""
def razorpay(conn, _params) do
handle_webhook(conn, :razorpay, "x-razorpay-signature")
end
# ===========================================
# Private Implementation
# ===========================================
defp handle_webhook(conn, provider, signature_header) do
with {:ok, raw_body} <- get_raw_body(conn),
{:ok, signature} <- get_signature(conn, signature_header),
{:ok, secret} <- get_webhook_secret(provider),
:ok <- verify_signature(provider, raw_body, signature, secret),
{:ok, payload} <- decode_payload(raw_body),
{:ok, event} <- Providers.handle_webhook_event(provider, payload),
{:ok, _result} <- WebhookProcessor.process(event) do
Logger.info("Webhook processed successfully: #{provider} - #{event.type}")
conn
|> put_status(200)
|> json(%{status: "ok"})
else
{:error, :invalid_signature} ->
Logger.warning("Invalid webhook signature from #{provider}")
conn
|> put_status(401)
|> json(%{error: "Invalid signature"})
{:error, :duplicate_event} ->
# Duplicate events are OK - return 200 to prevent retries
Logger.debug("Duplicate webhook event from #{provider}")
conn
|> put_status(200)
|> json(%{status: "duplicate"})
{:error, :unknown_event} ->
# Unknown events are OK - return 200 to prevent retries
Logger.debug("Unknown webhook event type from #{provider}")
conn
|> put_status(200)
|> json(%{status: "ignored"})
{:error, :not_configured} ->
Logger.warning("Webhook received for unconfigured provider: #{provider}")
conn
|> put_status(400)
|> json(%{error: "Provider not configured"})
{:error, reason} ->
Logger.error("Webhook processing failed for #{provider}: #{inspect(reason)}")
conn
|> put_status(400)
|> json(%{error: "Processing failed"})
end
end
defp get_raw_body(conn) do
case conn.assigns[:raw_body] do
nil ->
# Try to read from body_params if raw_body not set
# This is a fallback - ideally raw_body should be set by a Plug
{:error, :no_raw_body}
raw_body when is_binary(raw_body) ->
{:ok, raw_body}
end
end
defp get_signature(conn, header_name) do
case get_req_header(conn, header_name) do
[signature | _] -> {:ok, signature}
[] -> {:error, :no_signature}
end
end
defp get_webhook_secret(provider) do
key = "billing_#{provider}_webhook_secret"
case Settings.get_setting(key, "") do
"" -> {:error, :not_configured}
secret -> {:ok, secret}
end
end
defp verify_signature(provider, raw_body, signature, secret) do
Providers.verify_webhook_signature(provider, raw_body, signature, secret)
end
defp decode_payload(raw_body) do
case Jason.decode(raw_body) do
{:ok, payload} -> {:ok, payload}
{:error, _} -> {:error, :invalid_json}
end
end
end