Payments
The payments library provides a PSP-agnostic checkout abstraction.
Swap between Mollie, Stripe, and a development mock by changing a single config value —
no application code changes required.
|
Note
|
This library handles the checkout-session flow (create → redirect → webhook → status).
For communication channels (email, SMS), see the external library.
|
Supported providers
|
Auto-approves all payments. No network calls. Use in development and tests. |
|
Mollie REST API. Webhook via form-POST. No HMAC signing. |
|
Stripe Checkout Sessions. Webhook verified with HMAC-SHA256. |
Step 1 — Configure Integrant
Add one key to your config.edn:
;; resources/conf/dev/config.edn — development
:wagoe/payment-provider
{:provider :mock}
;; resources/conf/prod/config.edn — Mollie
:wagoe/payment-provider
{:provider :mollie
:api-key #env MOLLIE_API_KEY
:webhook-base-url #env APP_BASE_URL} ; e.g. "https://app.example.com"
;; resources/conf/prod/config.edn — Stripe
:wagoe/payment-provider
{:provider :stripe
:api-key #env STRIPE_SECRET_KEY
:webhook-secret #env STRIPE_WEBHOOK_SECRET}
Inject it wherever payment operations are needed:
:wagoe/order-service
{:payment-provider #ig/ref :wagoe/payment-provider
...}
Step 2 — Create a checkout session
(require '[wagoe.payments.ports :as psp])
(defn initiate-payment! [payment-provider order]
(let [{:keys [checkout-url provider-checkout-id]}
(psp/create-checkout-session payment-provider
{:amount-cents (:total-cents order)
:currency "EUR"
:description (str "Order #" (:id order))
:redirect-url (str base-url "/orders/" (:id order) "/confirmation")
:webhook-url (str base-url "/api/v1/payments/webhook")
:metadata {:order-id (str (:id order))}})]
;; Store provider-checkout-id on the order so you can look it up later
(save-checkout-id! order provider-checkout-id)
;; Redirect the user to the PSP
checkout-url))
Amount rules
-
Always use integer cents —
11900for €119,00. Never pass floats. -
Currency as ISO 4217 string —
"EUR","USD".
Step 3 — Add a webhook endpoint
(require '[wagoe.payments.ports :as psp])
(defn payment-webhook-handler [request payment-provider]
;; IMPORTANT: read raw body BEFORE any body-parsing middleware
(let [raw-body (slurp (:body request))
headers (:headers request)]
(if (psp/verify-webhook-signature payment-provider raw-body headers)
(let [{:keys [event-type provider-checkout-id]}
(psp/process-webhook payment-provider raw-body headers)]
(case event-type
:payment.paid (activate-order! provider-checkout-id)
:payment.failed (mark-order-failed! provider-checkout-id)
:payment.cancelled (cancel-order! provider-checkout-id)
nil) ; ignore unrecognized events
{:status 200 :body ""})
{:status 400 :body "Invalid signature"})))
Route definition:
{:path "/payments/webhook"
:methods {:post {:handler payment-webhook-handler
:summary "PSP webhook receiver"}}}
|
Warning
|
The webhook route must receive the raw body string before body-parsing middleware runs. Stripe computes the HMAC over the raw request body. Parsing it first invalidates the signature. |
Normalized event types
|
Payment successfully completed |
|
Authorized but not yet captured |
|
Payment attempt failed |
|
Cancelled by user or PSP |
Step 4 — Check payment status
Use get-payment-status to poll a payment (e.g. on the confirmation page):
(let [{:keys [status provider-payment-id]}
(psp/get-payment-status payment-provider provider-checkout-id)]
(case status
:paid (render-success-page order)
:pending (render-pending-page order)
:failed (render-failure-page order)
:cancelled (render-cancelled-page order)))
Provider notes
Mock
The mock provider is ideal for development and automated tests:
# Mock checkout URL lands on your own handler — add it to your dev routes
GET /web/payment/mock-return?checkout-id=<uuid>
;; In tests
(require '[wagoe.payments.shell.adapters.mock :as mock])
(def test-provider (mock/->MockPaymentProvider))
Mollie
-
Mollie does NOT sign webhooks with HMAC —
verify-webhook-signaturealways returnstrue. -
Verify by calling
get-payment-statuswith the payment ID received in the webhook body. -
:webhook-base-urlis your application’s public base URL; the adapter appends/api/v1/payments/webhook.
Stripe
-
Stripe signs webhooks with HMAC-SHA256 — always call
verify-webhook-signaturefirst. -
Set
STRIPE_WEBHOOK_SECRETfrom the Stripe dashboard (Webhooks → signing secret). -
In development, use the Stripe CLI to forward events to
localhost:
stripe listen --forward-to localhost:3000/api/v1/payments/webhook
The CLI prints the webhook signing secret — use it as STRIPE_WEBHOOK_SECRET in dev.
Testing
# All payments tests
clojure -M:test:db/h2 :payments
# Only pure function tests
clojure -M:test:db/h2 --focus-meta :unit
Always use the mock provider in tests:
(require '[wagoe.payments.shell.adapters.mock :as mock])
(deftest checkout-test
(let [provider (mock/->MockPaymentProvider)
result (psp/create-checkout-session provider
{:amount-cents 4900
:currency "EUR"
:description "Test order"
:redirect-url "https://example.com/done"})]
(is (string? (:checkout-url result)))
(is (string? (:provider-checkout-id result)))))
See libs/payments/AGENTS.md for the full library reference.