Phoenix Plug for Plaid webhook ingestion.
Handles the complete webhook lifecycle:
- Body preservation — reads and caches raw body for signature verification
- Signature verification — HMAC or JWT verification against your webhook secret
- Deduplication — ETS-backed sliding window dedup (handles Plaid re-deliveries)
- Immediate ACK — responds
200 OKbefore processing (Plaid requires fast ACK) - 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
endHandler 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
endOban 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.