Checkout & payments
BNPL (Klarna Advanced Tier)
Indicative price, not an offer: €99/mo + 1% per transaction; credential: Klarna API key
Generated from docs/plan/paid/bnpl/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Klarna Advanced gives merchants control over BNPL terms and payment plans. This paid credential module adds Klarna API integration for session creation, payment method configuration, and per-order authorization.
The problem
Customers want payment plans; basic Klarna covers small orders, but I can’t offer financing on larger purchases or configure which products qualify. Advanced Klarna gives me control, but integration requires API credentials and ongoing support.
What it does
- Klarna merchant account configuration: username, password, API credentials stored securely
- Session creation: POST to Klarna API, pass order amount/currency/items, retrieve session token
- Payment method control: choose which Klarna products (Pay Later, Slice, Financing) show at checkout per order
- Eligibility checking: query Klarna eligibility before checkout; warn customer if order exceeds limits
- Webhook handling: Klarna sends push notifications on authorization, capture, refund; validate and apply state changes
- Refund routing: POST refunds to Klarna API; track partial and full refunds
- Regional support: configure Klarna by region (EU, NA, OC) and language; show methods only in supported markets
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Klarna account management — merchant logs into Klarna’s dashboard for account settings, payout setup, and settlement. AstroBaaS only uses the API.
- Marketing materials — Klarna’s badge and promotional banners are merchant’s responsibility to place in storefront HTML; this module provides data only.
- Financing terms customization — Klarna’s interest rates and terms are controlled by Klarna. AstroBaaS just offers the methods that Klarna’s account permits.
Data model
No new collections. On Order: payment_provider (‘klarna’), payment_method (‘klarna_pay_later’ | ‘klarna_slice_it’ | ‘klarna_financing’), payment_reference (Klarna authorization token). payment_status follows standard flow: ‘unpaid’ → ‘pending’ (AUTHORIZED) → ‘paid’ (CAPTURED). payment_events array tracks webhook event ids for idempotency.
API
- POST /api/payments/start — body: { order_number, email, provider: ‘klarna’, payment_method: ‘klarna_pay_later’ | ‘klarna_slice_it’ } → { redirect_url }
- GET /api/payments/klarna/eligibility — { amount_cents, country } → { eligible: true, available_methods: […] }
- POST /api/payments/webhook/klarna — Klarna calls this on authorization/capture/refund; validate, idempotent update
- POST /api/orders/{id}/refund — forward to Klarna API when order was placed via Klarna
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Payments dashboard shows Klarna configuration: ‘Region: EU’, ‘Live Mode: Yes’, ‘Status: Configured’. Test-mode toggle for sandbox. Order detail shows Klarna authorization token and financing terms (if available from webhook). Refund history shows Klarna refund status.
The seam — why this is paid
Core owns Order model, webhook system, and refund ledger. Paid module owns Klarna API calls, eligibility checks, and method routing. Core’s generic POST /api/payments/start handles any provider; Klarna is one implementation. Klarna’s terms (interest rates, durations) are entirely Klarna’s — AstroBaaS just reflects what Klarna’s API says is available.
Credential: Klarna API management, payment-plan configuration, regulatory support
Dependencies
- payment-status-tracking — relies on payment_status state machine and refund tracking
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Merchant configures Klarna (username, password, API key, region ‘EU’); /api/payments methods include ‘klarna_pay_later’, ‘klarna_slice_it’
- Order for €800; /api/payments/klarna/eligibility returns { eligible: true, available_methods: [‘klarna_pay_later’, ‘klarna_financing’] }
- Order for €15,000; eligibility returns { eligible: false, reason: ‘exceeds_region_limit’ }; ‘klarna_financing’ still not shown at checkout
- Customer chooses Klarna Slice It; redirected to Klarna’s form, authorizes; webhook arrives with AUTHORIZATION_APPROVED → order becomes ‘paid’ + ‘processing’
- Webhook replayed (same event id) → no-op, order state unchanged
- Merchant refunds €200 (partial); API call to Klarna, refund_status=‘pending’; Klarna webhook confirms success
- Merchant cancels order before Klarna authorizes; webhook arrives with AUTHORIZATION_DECLINED → order becomes ‘failed’, stock released
Risks
Klarna’s API credentials in env vars must never appear in logs or error messages — use constant-time comparison for secrets and filter logs to strip them. Idempotency: if Klarna sends the same authorization event twice, both must result in the same order state; use event id as the idempotency key. Webhook replay: if Klarna resends after a delay, the order may have been already captured; the ‘capture after authorization’ decision must be explicit (either automatic or admin-triggered), not implicit, so replay doesn’t accidentally double-capture.
Commercial context
| Suggested price | €99/mo + 1% per transaction; credential: Klarna API key |
| Rival anchor | Klarna: free (basic) + 0% merchant fee; Advanced: €99/mo + 0-1.5% transaction |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.