Mock Bank & SCA Testing
Simulate UK Open Banking redirect and Strong Customer Authentication (SCA) locally before connecting to real ASPSP sandboxes.
Consumer checkout flow
| Route | Role |
|---|---|
/checkout | Bank picker — 16 UK banks, searchable |
/checkout ST-03 | Handoff — QR code (desktop) or deep link (mobile) |
/payments/return | Callback — polls status, shows receipt with fee breakdown |
/redirect-stub | Mock ASPSP — simulates bank SCA redirect |
Example URL:
https://dev-checkout.bopenbanking.com/checkout
?amount=150.00¤cy=GBP&merchant=Demo+Store
&return_url=https://merchant.example.com/thanks
Architecture
/checkout (bank picker)
│ POST /api/v1/payments (Bearer token)
▼
payment-service ──→ bopen.payment_consents (AWAITING_AUTHORISATION)
│ ──→ bopen.payment_intents (AwaitingAuthorisation)
│
▼ redirect_url
/redirect-stub (simulate bank SCA)
│
▼ ?code=...&state=...
/payments/return
│ POST /api/v1/payments/:id/sca-complete
│ GET /api/v1/payments/:id (poll)
▼
Receipt — gross/fee/net breakdown, end_to_end_id, status history
Mock merchant harness
/dev/merchant-harness is the primary end-to-end dev entry point:
| Route | Role |
|---|---|
/dev/merchant-harness | POST /api/v1/payments and redirect to mock bank |
/dev/merchant-success | Mock thank-you page after SCA completes |
Harness features
- Editable amount, currency, idempotency key, and merchant client ID
- Bank selection: Monzo, Barclays, NatWest
- Auto-redirect toggle — disable to test idempotency (click Initiate twice with the same key; expect HTTP
200and the samepayment_id) - Full SCA toggle — swaps
/redirect-stubfor/dev/mock-bank-sca
Default merchant ID: merchant_acquirer_alpha.
Embeddable widget testing
For widget-specific testing, open /widget-demo.html:
- Adjust amount, currency, merchant name, and API URL
- Click Launch widget — the iframe loads with the full 4-screen flow
- The event log captures all
BOPEN_*lifecycle events in real time - Integration snippet updates live — copy it into a merchant page prototype
Mock bank credentials
| Step | Value |
|---|---|
| Username | customer |
| Password | password123 |
| OTP | 123456 |
Quick actions on the mock bank page:
- Simulate SCA success — skips forms, returns a mock
code - Simulate cancellation — returns
error=access_denied
API authentication
The payment API requires a Keycloak Bearer token (when KEYCLOAK_URL is set):
# Get a token from your Keycloak realm
TOKEN=$(curl -s -X POST \
http://localhost:8080/realms/test-corp/protocol/openid-connect/token \
-d "grant_type=password&client_id=bopen-portal&username=admin@test.com&password=..." \
| python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
# Create a payment
curl -X POST http://localhost:8081/api/v1/payments \
-H "Authorization: Bearer $TOKEN" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount":"150.00","currency":"GBP","redirect_uri":"http://localhost:3000/payments/return"}'
When KEYCLOAK_URL is not set (dev without Keycloak), the service falls back to the X-Merchant-Client-Id header:
curl -X POST http://localhost:8081/api/v1/payments \
-H "X-Merchant-Client-Id: dev-acquirer-client-0001" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount":"150.00","currency":"GBP","redirect_uri":"http://localhost:3000/payments/return"}'
Payment status transitions
| Scenario | Final status | Audit events |
|---|---|---|
| Successful SCA | Settled | AwaitingAuthorisation → Authorised → Settled |
| User cancelled | Failed | AwaitingAuthorisation → Failed |
| Merchant refund | Refunded | Settled → Refunded |
Each event records changed_by (MERCHANT_API or BANK_CALLBACK) and an ordinal for deterministic ordering. payment_events.raw_payload_sha256 contains a PostgreSQL-computed SHA-256 hash of the raw payload for cryptographic non-repudiation.
Initiating a refund
Once a payment reaches Settled, merchants can initiate a full refund:
curl -X POST http://localhost:8081/api/v1/payments/{payment_id}/refund \
-H "Authorization: Bearer $TOKEN"
# → 200 { "payment_id": "...", "status": "Refunded" }
The payment.refunded webhook fires immediately after the refund is processed.
Swapping to a real ASPSP sandbox
When integrating TrueLayer, Yapily, or the OBIE directory:
- Replace the mock bank URL in
payment-servicewith the ASPSP authorisation URL returned by the external consent API - The Next.js callback route (
/payments/return) and PostgreSQL state machine are unchanged - The
end_to_end_idfield is already injected into all payment intents — banks will pass it through for reconciliation