PlaidEx.Webhooks.Plug (plaid_ex v1.0.0)

Copy Markdown View Source

Phoenix Plug for Plaid webhook ingestion.

Handles the complete webhook lifecycle:

  1. Body preservation — reads and caches raw body for signature verification
  2. Signature verification — HMAC or JWT verification against your webhook secret
  3. Deduplication — ETS-backed sliding window dedup (handles Plaid re-deliveries)
  4. Immediate ACK — responds 200 OK before processing (Plaid requires fast ACK)
  5. Async dispatch — routes typed events to your handler via Task.Supervisor or Oban

Usage in your Phoenix router

# router.ex
pipeline :plaid_webhooks do
  plug :accepts, ["json"]
end

scope "/webhooks/plaid" do
  pipe_through :plaid_webhooks
  forward "/", PlaidEx.Webhooks.Plug,
    config: Application.fetch_env!(:my_app, :plaid_config),
    handler: MyApp.PlaidWebhookHandler
end

Handler behaviour

Implement PlaidEx.Webhooks.Handler in your handler module:

defmodule MyApp.PlaidWebhookHandler do
  @behaviour PlaidEx.Webhooks.Handler

  @impl true
  def on_transactions_sync(%PlaidEx.Webhooks.Schemas.TransactionsSyncEvent{} = event) do
    # Trigger sync for this item
    PlaidEx.Sync.TransactionSync.trigger_sync(event.item_id)
    :ok
  end

  @impl true
  def on_item_error(%PlaidEx.Webhooks.Schemas.ItemErrorEvent{} = event) do
    MyApp.Items.mark_error(event.item_id, event.error)
    :ok
  end

  # Default no-op for unhandled events
  @impl true
  def on_unknown(event) do
    require Logger
    Logger.debug("Unhandled Plaid webhook: " <> inspect(event))
    :ok
  end
end

Oban integration

If Oban is available, webhook processing is automatically made durable. Enable by setting oban_queue in your config.

Raw body requirement

Plaid signature verification requires the raw (unparsed) request body. If you use Plug.Parsers in your pipeline, it consumes the body. This plug reads the body before parsing using Plug.Conn.read_body/2.

Important: Do not put Plug.Parsers before this plug in the pipeline.