Sandbox Walkthrough
This guide walks you through a complete integration with the Lunar Open Banking sandbox, from registration to making your first API calls.
This walkthrough assumes you have curl, jq, and uuidgen installed on
your system. On macOS, you may need to install curl with OpenSSL support:
brew install curl-openssl
Prerequisites
Before starting, ensure you have:
- A valid eIDAS QWAC certificate from a Qualified Trust Service Provider (QTSP)
- A terminal with TLS 1.3 support
curl(with OpenSSL for TLS 1.3)jqfor JSON parsinguuidgenfor generating request IDs
See the Security model for details on certificate requirements.
Step 1: Register as a TPP
With your eIDAS certificate ready, register as a Third Party Provider to receive client credentials.
# Create a directory for your sandbox files
mkdir -p sandbox-test && cd sandbox-test
# Define your registration payload
# Replace the redirect URI with your actual callback URL
registration_payload=$(cat <<EOF
{
"name": "mycompany",
"roles": ["PSP_AI", "PSP_PI"],
"redirectUris": ["https://localhost:8080/callback"]
}
EOF
)
# Register with the sandbox
curl \
--silent \
--header "Content-Type: application/json" \
--cert client.pem \
--key client.key \
--request POST \
--data "$registration_payload" \
https://tpp.openbanking-sandbox.prod.lunar.tech/tpp | jq .Save the response—you’ll need the clientId and clientSecret for authentication:
{
"clientId": "11111111-2222-3333-4444-555555555555",
"clientSecret": "aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"
}The clientSecret is only returned once. Store it securely—if you lose it,
you’ll need to re-register.
# Store credentials as environment variables for later use
export CLIENT_ID="your-client-id-here"
export CLIENT_SECRET="your-client-secret-here"Step 2: Get a TPP access token
For TPP management operations, obtain an access token using the client credentials grant:
# Base64 encode credentials for Basic auth
AUTH=$(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64)
# Request access token
curl \
--silent \
--header "Authorization: Basic ${AUTH}" \
--header "Content-Type: application/x-www-form-urlencoded" \
--cert client.pem \
--key client.key \
--request POST \
--data "grant_type=client_credentials" \
https://auth.sandbox.openbanking.prod.lunar.app/oauth2/token | jq .Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}Step 3: Start the user authorization flow
To access user account data, you need to initiate an OAuth2 authorization code flow. In a real application, you would redirect the user to this URL:
# Construct the authorization URL
AUTH_URL="https://auth.sandbox.openbanking.prod.lunar.app/oauth2/auth"
REDIRECT_URI="https://localhost:8080/callback"
SCOPES="PSP_AI%20PSP_PI%20offline"
STATE=$(uuidgen)
echo "Open this URL in a browser:"
echo "${AUTH_URL}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPES}&state=${STATE}"In sandbox, the consent flow is simulated. Follow the on-screen instructions to complete the authorization. You’ll receive an authorization code in the callback URL.
Step 4: Exchange authorization code for tokens
After the user authorizes access, you’ll receive an authorization code in your redirect URI. Exchange it for access and refresh tokens:
# Replace with the actual code from your callback
AUTHORIZATION_CODE="your-authorization-code"
curl \
--silent \
--header "Authorization: Basic ${AUTH}" \
--header "Content-Type: application/x-www-form-urlencoded" \
--request POST \
--data "grant_type=authorization_code&code=${AUTHORIZATION_CODE}&redirect_uri=${REDIRECT_URI}" \
https://auth.sandbox.openbanking.prod.lunar.app/oauth2/token | jq .Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xRxGGEpVawiUak6He367W3oeOfh...",
"scope": "PSP_AI PSP_PI offline"
}# Store the access token
export ACCESS_TOKEN="your-access-token-here"Step 5: List user accounts
With a valid access token, you can now access user account information:
curl \
--silent \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "X-Request-Id: $(uuidgen)" \
https://sandbox.openbanking.prod.lunar.app/accounts | jq .Example response:
{
"accounts": [
{
"id": "acc-12345678-1234-1234-1234-123456789012",
"name": "Main Account",
"currency": "DKK",
"iban": "DK1234567890123456",
"balanceAmount": 10000.0,
"balanceAvailableAmount": 9500.0,
"type": "CHECKING",
"supportsPayments": true,
"supportsTransfers": true,
"ownerName": "Test User"
}
]
}Step 6: Get account transactions
Retrieve transaction history for a specific account:
ACCOUNT_ID="acc-12345678-1234-1234-1234-123456789012"
curl \
--silent \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "X-Request-Id: $(uuidgen)" \
"https://sandbox.openbanking.prod.lunar.app/accounts/${ACCOUNT_ID}/transactions?limit=10" | jq .Example response:
{
"transactions": [
{
"id": "tx-98765432-1234-1234-1234-123456789012",
"transactionTime": "2024-01-15T10:30:00Z",
"billingAmount": -150.0,
"title": "Coffee Shop",
"type": "card",
"status": "financial"
}
]
}Step 7: Initiate a payment
With both PSP_AI and PSP_PI scopes, you can initiate payments in the sandbox. Payments are auto-approved — no user interaction is needed.
DK Payment Slip
# Pick the first DKK account from Step 5
ACCOUNT_ID="your-account-id-here"
curl \
--silent \
--cert client.pem \
--key client.key \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "X-Request-Id: $(uuidgen)" \
--request POST \
--data '{
"accountId": "'${ACCOUNT_ID}'",
"amount": 100,
"currency": "DKK",
"creditorId": "12345678",
"debtorId": "123456789",
"type": "71",
"redirectUrl": "https://localhost:8080/callback",
"title": "Test Payment",
"message": "Sandbox test",
"date": "2026-05-01"
}' \
https://sandbox.openbanking.prod.lunar.app/payments/dk-payment-slip | jq .Response:
{
"authUrl": "https://sandbox.openbanking.prod.lunar.app/aisp-pisp/polling?paymentID=...",
"paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}Step 8: Retrieve the payment
Use the paymentId from the initiation response to retrieve the payment details:
PAYMENT_ID="your-payment-id-here"
curl \
--silent \
--cert client.pem \
--key client.key \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "X-Request-Id: $(uuidgen)" \
"https://sandbox.openbanking.prod.lunar.app/payments/dk-payment-slip/${PAYMENT_ID}" | jq .The response includes the payment details with a status of COMPLETED (sandbox payments are auto-approved).
Step 9: Poll payment status
You can also poll the payment status using the polling endpoint:
curl \
--silent \
--cert client.pem \
--key client.key \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "X-Request-Id: $(uuidgen)" \
"https://sandbox.openbanking.prod.lunar.app/polling/${PAYMENT_ID}/status" | jq .Response:
{
"status": "APPROVED",
"redirectUrl": "https://localhost:8080/callback"
}In sandbox, all initiated payments are immediately approved. In production,
the user must authorize the payment through the Lunar app before the status
changes from AWAITING_APPROVAL to APPROVED.
Step 10: Refresh your access token
When your access token expires (after expires_in seconds), use the refresh token to get a new one:
REFRESH_TOKEN="your-refresh-token-here"
curl \
--silent \
--header "Authorization: Basic ${AUTH}" \
--header "Content-Type: application/x-www-form-urlencoded" \
--request POST \
--data "grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}" \
https://auth.sandbox.openbanking.prod.lunar.app/oauth2/token | jq .Troubleshooting
Certificate errors
If you receive TLS or certificate errors:
- Ensure you’re using curl with TLS 1.3 support
- Check that your
client.pemandclient.keyfiles exist and are valid - Verify your eIDAS certificate hasn’t expired
- Ensure your certificate chain is complete (leaf to root)
401 Unauthorized
- Ensure your access token hasn’t expired
- Verify you’re using the correct authorization header format
- Check that your client credentials are correct
403 Forbidden
- Confirm your TPP registration includes the required roles (
PSP_AIorPSP_PI) - Verify your certificate is valid and matches your registration
- Check that the certificate roles match or are a superset of the requested TPP roles
Next steps
- Explore the Accounts API for detailed endpoint documentation
- Learn about Payment Initiation to initiate transfers
- Review the full API Reference for complete specifications
- Learn about Client Management to update redirect URIs or add certificates