The Payment Journey
A step-by-step walkthrough of what happens when a user makes an Open Banking payment
Overview
This page walks you through exactly what happens when a user makes a payment using Open Banking. We'll look at it from three perspectives: what the user sees, what's happening technically, and what your code needs to do.
By the end, you'll understand the complete journey from "Pay" button click to payment confirmation.
The Big Picture
An Open Banking payment involves four main stages:
- Setup - Your application prepares the payment request
- Authorisation - User consents to the payment at their bank
- Execution - The payment is processed
- Confirmation - Payment status is verified
Time: The entire process typically takes 30 seconds to 2 minutes.
Stage 1: Setup (Preparing the Payment)
What the User Sees
The user is on your application and clicks a "Pay" or "Checkout" button.
What's Happening Technically
Your application needs to:
- Define the payment details (amount, recipient, reference)
- Create or select a payee (recipient)
- Authenticate with Moneyhub (client credentials)
- Generate an authorisation URL
What Your Code Does
// 1. Define payment details
const paymentDetails = {
amount: 5000, // £50.00 in pence
payeeId: "payee-123", // Created earlier or inline
payerRef: "Order-12345", // Reference
};
// 2. Create authorization URL
const authUrl = await moneyhub.getPaymentAuthorizeUrl({
bankId: "hsbc", // Or "test" for test banks
amount: paymentDetails.amount,
payeeId: paymentDetails.payeeId,
payerRef: paymentDetails.payerRef,
state: generateRandomState(),
nonce: generateRandomNonce(),
});
// 3. Redirect user to authUrl
res.redirect(authUrl);Behind the Scenes: Authorisation URL Creation
The authorisation URL contains (encoded as a JWT or in the URL parameters):
{
"client_id": "your-client-id",
"scope": "openid payment id:hsbc",
"redirect_uri": "https://yourapp.com/callback",
"state": "random-security-token",
"nonce": "another-random-token",
"claims": {
"id_token": {
"mh:payment": {
"essential": true,
"value": {
"amount": 5000,
"payeeId": "payee-123",
"payerRef": "Order-12345"
}
}
}
}
}What each part means:
client_id- Your application identifierscope- What you're requesting (openid + payment + specific bank)redirect_uri- Where to send user backstate- Security token (you'll verify this later)nonce- Another security tokenclaims.mh:payment- The exact payment details
Stage 2: Authorisation (User Consent)
This is where the user leaves your application and goes to their bank.
Step 2.1: Moneyhub Consent Screen
What the User Sees:
After being redirected, the user first sees a Moneyhub consent screen explaining:
- Who is requesting the payment (your application)
- What they'll be asked to do
- Privacy and security information
What's Happening:
Moneyhub is:
- Validating the authorisation request
- Preparing to connect to the user's bank
- Logging the consent request
Duration: 5-10 seconds
Step 2.2: Bank Selection
What the User Sees:
If you didn't specify a specific bank (used id:test or id:all), the user sees a list of banks to choose from.
What's Happening:
Moneyhub displays available banks that support payments. User clicks their bank.
Duration: 5-20 seconds
Step 2.3: Redirect to Bank
What the User Sees:
The user is redirected to their bank's website or app. The URL changes to their bank's domain.
What's Happening:
Moneyhub redirects the user to the bank's OAuth authorisation endpoint with:
- The payment details
- A request for the user to authenticate
- Required scopes and permissions
Technical: This is a standard OAuth 2.0 redirect with Open Banking-specific parameters.
Step 2.4: Bank Authentication
What the User Sees:
The user is on their bank's website/app. They:
- Enter their banking credentials (username, password)
- Complete 2-factor authentication (SMS, app notification, etc.)
What's Happening:
The bank:
- Authenticates the user
- Verifies they have the authority to make payments
- Checks the account has sufficient funds (some banks)
Important: Your application and Moneyhub NEVER see the user's bank credentials. Authentication happens entirely on the bank's systems.
Duration: 10-60 seconds
Step 2.5: Payment Consent Screen
What the User Sees:
After authentication, the bank displays a consent screen showing:
- The payment amount (e.g., "£50.00")
- The recipient details
- The account the payment will come from
- The payment reference
- An "Approve" and "Cancel" button
Example:
┌────────────────────────────────────────┐
│ HSBC Payment Consent │
├────────────────────────────────────────┤
│ │
│ You are authorizing a payment of: │
│ │
│ £50.00 │
│ │
│ To: ABC Company Ltd │
│ Sort Code: 12-34-56 │
│ Account: 12345678 │
│ │
│ From: Your Current Account │
│ ****1234 │
│ │
│ Reference: Order-12345 │
│ │
│ ┌────────┐ ┌────────┐ │
│ │ Cancel │ │ Approve│ │
│ └────────┘ └────────┘ │
│ │
└────────────────────────────────────────┘
What's Happening:
The bank:
- Displays the exact payment details from the authorisation request
- Waits for user confirmation
- Records the user's decision
User Choice:
- Approve - Payment is authorized, proceed to execution
- Cancel - User is redirected back with an error
Duration: 10-30 seconds
Step 2.6: Authorisation Code Returned
What the User Sees:
After approving, the user is redirected back to your application. The URL will look like:
https://yourapp.com/callback?
code=auth-code-abc123&
state=random-security-token&
id_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
What's Happening:
The bank has:
- Generated a single-use authorisation code
- Redirected the user back to your
redirect_uri - Included the code and state in the URL
Important Parameters:
code- Short-lived authorisation code (expires in ~60 seconds)state- The security token you sent earlier (VERIFY THIS)id_token- Contains payment ID and connection info
Stage 3: Execution (Completing the Payment)
What the User Sees
The user is back on your application. They might see a loading spinner or "Processing payment..." message.
What Your Code Does
// 1. Extract parameters from callback
const { code, state, id_token } = req.query;
// 2. VERIFY STATE (security check)
if (state !== storedState) {
throw new Error('Invalid state - possible CSRF attack');
}
// 3. Exchange code for tokens
const tokens = await moneyhub.exchangeCodeForTokens({
paramsFromCallback: { code, state, id_token },
localParams: { state, nonce },
});
// tokens contains:
// - access_token: Use to check payment status
// - id_token: Contains payment ID
// - token_type: "Bearer"
// 4. Decode ID token to get payment ID
const decoded = jwt.decode(tokens.id_token);
const paymentId = decoded['mh:payment'];
const connectionId = decoded['mh:con_id'];What's Happening Behind the Scenes
When you exchange the code, several things happen:
- Moneyhub verifies the code is valid and hasn't been used
- Moneyhub contacts the bank to complete the authorisation
- The bank initiates the payment in their systems
- Moneyhub creates a payment record and returns tokens
- The payment ID is encoded in the ID token for you to track
Token Exchange Request
Here's what actually gets sent:
POST /oidc/token HTTP/1.1
Host: identity.moneyhub.co.uk
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=auth-code-abc123&
redirect_uri=https://yourapp.com/callback&
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid payment id:hsbc"
}Decoded ID Token:
{
"sub": "user-id",
"mh:con_id": "connection-id-xyz",
"mh:payment": "payment-id-789",
"aud": "your-client-id",
"exp": 1234567890,
"iat": 1234564290,
"iss": "https://identity.moneyhub.co.uk/oidc"
}Key Claims:
mh:payment- The payment ID you need to trackmh:con_id- The bank connection IDsub- User ID (if user was pre-created)
Stage 4: Confirmation (Verifying the Payment)
What the User Sees
Your application displays the payment status:
- "Payment pending..."
- "Payment completed!"
- "Payment failed"
What Your Code Does
// Use the payment ID from the ID token
const paymentId = decoded['mh:payment'];
// Check payment status
const payment = await moneyhub.getPayment({
paymentId: paymentId,
token: tokens.access_token,
});
console.log(payment.status);
// Possible statuses:
// - "Pending"
// - "AcceptedSettlementInProcess"
// - "AcceptedSettlementCompleted"
// - "Rejected"Payment Status Flow
┌──────────────┐
│ Pending │
└──────┬───────┘
│
┌────────────┼────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌──────────────┐
│AcceptedSettlement │ │ Rejected │
│ InProcess │ │ │
└─────────┬─────────┘ └──────────────┘
│
▼
┌───────────────────┐
│AcceptedSettlement │
│ Completed │
└───────────────────┘
Status Meanings:
- Pending - Payment initiated, waiting for bank processing
- AcceptedSettlementInProcess - Bank accepted, processing the settlement
- AcceptedSettlementCompleted - Payment successfully completed
- Rejected - Payment was rejected (insufficient funds, technical issue, etc.)
Polling for Status Updates
Some payments take a few seconds to process. You may need to poll:
async function waitForPaymentCompletion(paymentId, accessToken) {
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
const payment = await moneyhub.getPayment({
paymentId,
token: accessToken,
});
if (payment.status === 'AcceptedSettlementCompleted') {
return { success: true, payment };
}
if (payment.status === 'Rejected') {
return { success: false, payment };
}
// Still pending, wait and try again
await sleep(2000); // Wait 2 seconds
attempts++;
}
return { success: false, error: 'Timeout waiting for payment' };
}The Complete Flow Visualized
┌─────────────┐
│ │ 1. User clicks "Pay"
│ Your App │◄─────────────────────────────┐
│ │ │
└──────┬──────┘ │
│ │
│ 2. Create authorization URL │
│ │
▼ │
┌─────────────┐ │
│ │ │
│ Moneyhub │ │
│ Identity │ │
│ │ │
└──────┬──────┘ │
│ │
│ 3. Redirect to bank │
│ │
▼ │
┌─────────────┐ │
│ │ 4. User logs in │
│ Bank │◄──────────────┐ │
│ │ │ │
└──────┬──────┘ │ │
│ User │
│ 5. User approves │ │
│ payment │ │
│ │ │
▼ │ │
│ 6. Redirect with │ │
│ auth code │ │
│ │ │
└──────────────────────┴───────────────┘
│
│ 7. Exchange code
│
┌──────────────────────┘
│
▼
┌─────────────┐
│ │ 8. Return tokens
│ Moneyhub │ (with payment ID)
│ │
└──────┬──────┘
│
│ 9. Check payment status
│
▼
┌─────────────┐
│ │
│ Your App │ 10. Show confirmation
│ │ to user
└─────────────┘
Timeline Example
Here's a real-world example of how long each step typically takes:
00:00 - User clicks "Pay with Bank"
00:01 - Authorization URL created
00:02 - User redirected to Moneyhub
00:05 - User redirected to HSBC
00:15 - User enters credentials (2FA)
00:25 - User sees payment consent screen
00:30 - User approves payment
00:32 - User redirected back to your app
00:33 - Code exchanged for tokens
00:34 - Payment status: Pending
00:36 - Payment status: AcceptedSettlementInProcess
00:45 - Payment status: AcceptedSettlementCompleted
00:46 - User sees "Payment successful!"
Total time: 46 seconds
What Can Go Wrong?
User Cancels
If the user clicks "Cancel" at any point:
Redirect: https://yourapp.com/callback?error=access_denied&state=...
Your code should:
- Check for
errorparameter - Show appropriate message
- Log the cancellation
Insufficient Funds
If the account doesn't have enough money:
The payment status will be: Rejected
Your code should:
- Check payment status
- Handle rejection gracefully
- Offer alternative payment methods
Technical Errors
Various technical issues can occur:
- Bank systems temporarily unavailable
- Network timeouts
- Invalid payment details
Your code should:
- Implement retry logic
- Handle errors gracefully
- Provide clear error messages to users
State Mismatch
If the state parameter doesn't match:
This is a security issue. Reject the request immediately.
if (state !== storedState) {
// Log security incident
// Reject the request
throw new Error('State mismatch - possible CSRF attack');
}Key Differences: One-Time vs Ongoing Access
One-Time Payment (What We've Described)
- User authorizes a specific payment
- Consent is single-use
- No refresh token
- Payment executes once
Ongoing Access (e.g., Account Data)
- User grants access for a period (e.g., 90 days)
- Consent is reusable
- Refresh token provided
- You can fetch data multiple times
The authorisation flow is similar, but the scopes and tokens differ.
Security Highlights
Throughout this journey, security is maintained:
- User credentials never shared - Only the bank sees them
- Encrypted communication - All requests use HTTPS/TLS
- State validation - Prevents CSRF attacks
- Short-lived codes - Authorisation codes expire in ~60 seconds
- Single-use codes - Can't be reused
- Explicit consent - User sees exact payment details
- Audit trail - All actions logged
Next Steps
Now that you understand the complete payment journey, you're ready to:
- Review the Key Terminology Glossary for any unfamiliar terms
- Learn about Security Concepts in more detail
- Try the [Your First Payment](../getting started/your-first-payment) tutorial
Quick Reference
Authorisation URL → User Consent → Code Exchange → Payment Status → Confirmation
Time: 30 seconds - 2 minutes
Tokens: Access token (API calls), ID token (payment ID)
Status: Pending → AcceptedSettlementInProcess → AcceptedSettlementCompleted
Errors: Check for error parameter in redirect, handle rejected payments
Further Reading
- OAuth and OIDC Basics - Understand the authentication standards
- Security Concepts - Deep dive into Open Banking security
- [Troubleshooting Common Errors](../getting started/troubleshooting-common-errors) - Fix common issues
Updated 9 months ago
