Sandbox Payment Flows
The sandbox supports all payment types available in production. The key difference is that sandbox payments are auto-approved and stored in memory, allowing you to test the full payment lifecycle without real user authorization.
How Sandbox Payments Work
When you initiate a payment in the sandbox:
- The payment is stored in memory and a
paymentIdis returned - No real user authorization is required — the payment is immediately approved
- You can retrieve the payment via GET, poll its status, and delete/cancel it
- The
authUrlreturned during initiation points to a sandbox polling page that redirects to yourredirectUrl
State Behavior
After Initiation
| Endpoint | Behavior |
|---|---|
GET /payments/{type}/{paymentId} | Returns the payment details matching the values you provided during initiation |
GET /polling/{paymentId}/status | Returns APPROVED with the redirectUrl you supplied |
GET /payments/standing-order (list) | Includes the newly created standing order |
After Deletion / Cancellation
| Payment Type | GET | Polling | List |
|---|---|---|---|
| Standing Orders | Returns error (not found) | AWAITING_APPROVAL, empty redirectUrl | Order is removed from list |
| SE Bank Giro | 404 Not Found | AWAITING_APPROVAL, empty redirectUrl | — |
| SE Plus Giro | 404 Not Found | AWAITING_APPROVAL, empty redirectUrl | — |
Sandbox payment state is held in memory and automatically expires after approximately 24 hours. Restarting the service clears all payment state. Do not rely on sandbox state for long-term storage.
Supported Payment Types
All payment types reflect the values you provide during initiation. The sandbox does not validate business rules (e.g., sufficient funds, valid BBAN routing) beyond basic request validation.
DK Payment Slip
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 .Domestic Credit Transfer
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}'",
"recipientBBAN": "12341234567",
"amount": "250.00",
"currency": "DKK",
"message": "Test transfer",
"title": "Test DCT",
"redirectUrl": "https://localhost:8080/callback",
"date": "2026-05-01"
}' \
https://sandbox.openbanking.prod.lunar.app/payments/domestic-credit-transfer | jq .Standing Order
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 '{
"senderAccountId": "'${ACCOUNT_ID}'",
"senderMessage": "Monthly rent",
"receiverAccount": "12341234567",
"receiverMessage": "Rent payment",
"amount": 5000,
"currency": "DKK",
"activeFrom": "2026-05-01",
"frequency": { "monthly": { "interval": 1, "paymentDay": "1" } },
"terminationPolicy": { "untilFurtherNotice": true },
"redirectUrl": "https://localhost:8080/callback"
}' \
https://sandbox.openbanking.prod.lunar.app/payments/standing-order | jq .After creation, verify the standing order appears in the list:
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/standing-order | jq .Delete the standing order:
curl \
--silent \
--cert client.pem \
--key client.key \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "X-Request-Id: $(uuidgen)" \
--request DELETE \
"https://sandbox.openbanking.prod.lunar.app/payments/standing-order/${PAYMENT_ID}" | jq .SE Bank Giro
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 '{
"giroNumber": "5894-2590",
"ocrReference": "1234567890",
"amount": "100.00",
"currency": "SEK",
"senderAccountId": "'${SEK_ACCOUNT_ID}'",
"redirectUrl": "https://localhost:8080/callback"
}' \
https://sandbox.openbanking.prod.lunar.app/payments/se-bank-giro | jq .SE Plus Giro
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 '{
"giroNumber": "1234567",
"ocrReference": "9876543210",
"amount": "100.00",
"currency": "SEK",
"senderAccountId": "'${SEK_ACCOUNT_ID}'",
"redirectUrl": "https://localhost:8080/callback"
}' \
https://sandbox.openbanking.prod.lunar.app/payments/se-plus-giro | jq .Polling Endpoint
The polling endpoint works for all payment types:
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 .| Status | redirectUrl | Meaning |
|---|---|---|
APPROVED | Your redirect URL | Payment was initiated and auto-approved |
AWAITING_APPROVAL | Empty | Payment was not found or was deleted |
Differences from Production
| Behavior | Sandbox | Production |
|---|---|---|
| User authorization | Skipped (auto-approved) | Required via Lunar app |
| Business validation | Basic request validation only | Full validation (funds, routing, limits) |
| Payment execution | Not executed (status stays at initiation state) | Executed by payment rails |
| State persistence | In-memory, ~24h TTL | Persistent database storage |
| Redirect flow | authUrl → sandbox polling page → your redirectUrl | authUrl → Lunar app → your redirectUrl |