Authentication
Comprehensive guide for Open Banking API authentication using OAuth 2.0 with mutual TLS.
🔐 Authentication Overview
- Overview
- Requirements
Authentication Methods
Open Banking API uses dual authentication:
-
🔑 Certificate Authentication (mTLS)
- QWAC certificate for TLS client authentication
- Required for all API requests
- QSealC request signing is not used — the API does not read signature headers
-
🔐 OAuth 2.0 Authorization
- Authorization Code flow with PKCE
- User consent and authentication
- Access token for API requests
Technical Requirements
Certificates:
- ✅ Valid QWAC certificate (eIDAS qualified)
- ⬜ QSealC certificate — not used; the API does not process request signatures
- ✅ Certificate chain validation
- ✅ TLS 1.2 minimum (1.3 recommended)
OAuth Implementation:
- ✅ Authorization Code flow
- ✅ PKCE (RFC 7636) support
- ✅ State parameter for CSRF protection
- ✅ Secure token storage
Security:
- ✅ HTTPS only communication
- ✅ Certificate pinning (mobile apps)
- ✅ Token rotation on refresh
- ✅ Secure redirect URI
OAuth 2.0 Implementation
- Authorization Flow
- 1️⃣ Authorization
- 2️⃣ Token Exchange
- 3️⃣ API Requests
- 🔄 Token Refresh
Authorization Code Flow with PKCE
Configuration Endpoint
The OAuth endpoints are not version-specific. The metadata document lives at the host root, outside
the /xs2a/... paths, and both profiles use the same one.
https://open-banking-api.paysera.com/.well-known/oauth-authorization-server
The scaOAuth link returned with a payment or consent points straight at it. Read the document and
use the authorization_endpoint and token_endpoint it advertises rather than building them
yourself.
Step 1: Authorization Request
Generate PKCE parameters and redirect user:
// Generate PKCE parameters
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));
const state = base64url(crypto.randomBytes(16));
// Build the authorization URL (the metadata document's authorization_endpoint)
const authUrl = new URL('https://open-banking-api.paysera.com/oauth/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('scope', `AIS:${consentId}`); // or `PIS:${paymentId}` for a payment
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
// Redirect user
window.location.href = authUrl.toString();
Parameters:
| Parameter | Description | Required |
|---|---|---|
response_type | Must be code | ✅ |
client_id | Your client identifier | ✅ |
redirect_uri | Registered callback URL | ✅ |
scope | AIS:<consentId> for the consent being authorised, or PIS:<paymentId> for the payment | ✅ |
state | CSRF protection token | ✅ |
code_challenge | PKCE challenge | ✅ |
code_challenge_method | Must be S256 | ✅ |
Step 2: Exchange Code for Token
After user authorization, exchange the code:
// Handle callback
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const returnedState = urlParams.get('state');
// Verify state
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
// Exchange code for token (the metadata document's token_endpoint)
const tokenResponse = await fetch('https://open-banking-api.paysera.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: savedCodeVerifier
})
});
const tokens = await tokenResponse.json();
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200a1b2c3...",
"scope": "PSP_AI AIS:3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Step 3: Making API Requests
Use the access token in API calls:
// API request with access token (use your API version's base path)
const response = await fetch('https://open-banking-api.paysera.com/xs2a/berlin/1.3/v1/accounts', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Consent-ID': consentId, // the consent the access token was issued for
'X-Request-ID': generateUUID(),
'Accept': 'application/json'
},
// mTLS configuration
agent: new https.Agent({
cert: fs.readFileSync('qwac-cert.pem'),
key: fs.readFileSync('qwac-key.pem'),
ca: fs.readFileSync('ca-bundle.pem')
})
});
const accounts = await response.json();
Required Headers:
| Header | Description |
|---|---|
Authorization | Bearer token |
Consent-ID | The consent the token was issued for (account information requests) |
X-Request-ID | Unique request identifier |
Accept | Content type (application/json) |
Content-Type | For POST/PUT requests |
Refreshing Access Tokens
Tokens expire after 1 hour. Use refresh token to get new access token:
// Refresh token request (the metadata document's token_endpoint)
const refreshResponse = await fetch('https://open-banking-api.paysera.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: savedRefreshToken,
client_id: 'YOUR_CLIENT_ID'
})
});
const newTokens = await refreshResponse.json();
// Update stored tokens
saveTokens({
accessToken: newTokens.access_token,
refreshToken: newTokens.refresh_token,
expiresAt: Date.now() + (newTokens.expires_in * 1000)
});
Token Lifecycle:
- Access token: 1 hour validity
- Refresh token: 1 month validity
- Automatic rotation on refresh
Mutual TLS (mTLS)
🔑 Certificate Configuration
mTLS Setup
Configure your HTTP client with certificates:
- Node.js
- Python
- Java
const https = require('https');
const fs = require('fs');
// Certificate configuration
const httpsAgent = new https.Agent({
cert: fs.readFileSync('path/to/qwac-cert.pem'),
key: fs.readFileSync('path/to/qwac-key.pem'),
ca: fs.readFileSync('path/to/ca-bundle.pem'),
rejectUnauthorized: true
});
// Using with fetch
const response = await fetch('https://open-banking-api.paysera.com/xs2a/berlin/1.3/v1/accounts', {
agent: httpsAgent,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Consent-ID': consentId,
'X-Request-ID': generateUUID()
}
});
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager
import ssl
import uuid
class SSLAdapter(HTTPAdapter):
def __init__(self, certfile, keyfile, cafile, *args, **kwargs):
self.certfile = certfile
self.keyfile = keyfile
self.cafile = cafile
super().__init__(*args, **kwargs)
def init_poolmanager(self, *args, **kwargs):
context = ssl.create_default_context(cafile=self.cafile)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
kwargs['ssl_context'] = context
return super().init_poolmanager(*args, **kwargs)
# Setup session with certificates
session = requests.Session()
adapter = SSLAdapter(
certfile='qwac-cert.pem',
keyfile='qwac-key.pem',
cafile='ca-bundle.pem'
)
session.mount('https://', adapter)
# Make request
response = session.get(
'https://open-banking-api.paysera.com/xs2a/berlin/1.3/v1/accounts',
headers={
'Authorization': f'Bearer {access_token}',
'Consent-ID': consent_id,
'X-Request-ID': str(uuid.uuid4()),
}
)
import javax.net.ssl.*;
import java.security.KeyStore;
import java.io.FileInputStream;
// Load client certificate
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("qwac-keystore.p12"),
"password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keyStore, "password".toCharArray());
// Load CA certificates
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("ca-truststore.jks"),
"password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
// Create SSL context
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(),
tmf.getTrustManagers(),
new SecureRandom());
// Configure HTTP client
HttpsURLConnection.setDefaultSSLSocketFactory(
sslContext.getSocketFactory()
);
Certificate Validation
Paysera validates:
- ✅ Certificate chain to trusted CA
- ✅ Certificate validity period
- ✅ TPP authorization number in certificate
- ✅ Certificate not revoked (OCSP check)
Scopes and Permissions
Paysera does not use permission-style scopes such as accounts or payments. The scope of the
authorization request (Step 1) names the one resource the PSU is authorising:
| Scope | Authorises |
|---|---|
AIS:<consentId> | The account information consent with that ID |
PIS:<paymentId> | The payment with that ID |
Request one scope per authorisation: each consent and each payment is authorised on its own. To read accounts and initiate a payment, authorise the consent and the payment separately.
What you may read is set by the consent, not by the scope. The access object of the
consent request lists the accounts and whether you
need their balances, their transactions, or both.
The scope returned with the access token can carry additional values next to the one you
requested.
Bulk payments, periodic payments and payment cancellation are not supported, so there is no scope for them.
Error Handling
⚠️ Common Errors and Solutions
OAuth Errors
| Error | Description | Solution |
|---|---|---|
invalid_request | Missing or invalid parameter | Check required parameters |
unauthorized_client | Client not authorized | Verify client_id |
access_denied | User denied consent | Handle gracefully |
invalid_scope | Requested scope invalid | Use AIS:<consentId> or PIS:<paymentId> |
server_error | Internal server error | Retry with backoff |
Certificate Errors
| Error | Description | Solution |
|---|---|---|
certificate_invalid | Invalid certificate | Check certificate validity |
certificate_expired | Certificate expired | Renew certificate |
certificate_not_found | Certificate not registered | Register with Paysera |
certificate_revoked | Certificate revoked | Obtain new certificate |
Token Errors
| Error | Description | Solution |
|---|---|---|
invalid_token | Token invalid or expired | Refresh token |
insufficient_scope | Token lacks required scope | Request new consent |
token_expired | Access token expired | Use refresh token |
invalid_grant | Refresh token invalid | Re-authenticate user |
Error Response Format
{
"error": "invalid_request",
"error_description": "The redirect_uri is missing",
"error_uri": "https://docs.paysera.com/errors#invalid_request"
}
Testing & Debugging
Common Issues
Certificate Issues:
# Test certificate connectivity
openssl s_client -connect open-banking-api.paysera.com:443 \
-cert qwac-cert.pem \
-key qwac-key.pem \
-CAfile ca-bundle.pem
Token Issues:
// Debug token expiry
console.log('Token expires at:', new Date(tokenExpiryTimestamp));
console.log('Time remaining:', tokenExpiryTimestamp - Date.now());
Request Debugging:
// Log all requests for debugging
console.log('Request:', {
url: request.url,
headers: request.headers,
method: request.method
## Resources
- 📖 [Getting Started Guide](/guides/open-banking/getting-started)
- 🔐 [Security Requirements](/guides/open-banking/getting-started/security)
- 💻 [Code Examples](/guides/open-banking/examples)
- 📚 [API Reference](/api/open-banking)
- ❓ [FAQ](/guides/open-banking/resources/faq)
:::tip[Pro Tips]
- Always implement PKCE for OAuth flows
- Cache discovery endpoint response
- Implement automatic token refresh before expiry
- Use SDK libraries when available
- Monitor certificate expiry dates proactively
:::