Skip to Content
SandboxPayment Flows

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:

  1. The payment is stored in memory and a paymentId is returned
  2. No real user authorization is required — the payment is immediately approved
  3. You can retrieve the payment via GET, poll its status, and delete/cancel it
  4. The authUrl returned during initiation points to a sandbox polling page that redirects to your redirectUrl

State Behavior

After Initiation

EndpointBehavior
GET /payments/{type}/{paymentId}Returns the payment details matching the values you provided during initiation
GET /polling/{paymentId}/statusReturns APPROVED with the redirectUrl you supplied
GET /payments/standing-order (list)Includes the newly created standing order

After Deletion / Cancellation

Payment TypeGETPollingList
Standing OrdersReturns error (not found)AWAITING_APPROVAL, empty redirectUrlOrder is removed from list
SE Bank Giro404 Not FoundAWAITING_APPROVAL, empty redirectUrl
SE Plus Giro404 Not FoundAWAITING_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 .
StatusredirectUrlMeaning
APPROVEDYour redirect URLPayment was initiated and auto-approved
AWAITING_APPROVALEmptyPayment was not found or was deleted

Differences from Production

BehaviorSandboxProduction
User authorizationSkipped (auto-approved)Required via Lunar app
Business validationBasic request validation onlyFull validation (funds, routing, limits)
Payment executionNot executed (status stays at initiation state)Executed by payment rails
State persistenceIn-memory, ~24h TTLPersistent database storage
Redirect flowauthUrl → sandbox polling page → your redirectUrlauthUrl → Lunar app → your redirectUrl
Last updated on