Skip to content

Merchant Integration Guide

Accept payments from any participating bank with a single integration. No per-bank setup.

Overview

1. Your backend creates a payment session
2. You redirect the customer (or embed the widget)
3. Customer authenticates with their bank via OTP
4. Gateway deducts funds and delivers a webhook to your endpoint
5. You fulfil the order

Step 1 — Get Your API Key

Contact your gateway operator (or your bank) to receive a Merchant API Key (mk_live_...).

All requests are authenticated with:

http
Authorization: Bearer mk_live_...

Test Keys

Use mk_test_... keys during development. Test mode payments never touch real funds.

Step 2 — Create a Payment Session

From your backend (never from the browser — your API key stays server-side):

http
POST /api/v1/payments/sessions
Authorization: Bearer mk_live_...
Content-Type: application/json

{
  "amount": 50000,
  "currency": "LYD",
  "destination": {
    "type": "alias",
    "value": "mtellesy"
  },
  "description": "Order #1042",
  "reference": "order_1042",
  "redirect_url": "https://mystore.com/orders/1042/result",
  "webhook_url": "https://mystore.com/webhooks/openwave"
}

Amount is in minor units

50000 = 500.00 LYD. See Amount Convention.

Response:

json
{
  "session_id": "ops_01HZGV...",
  "status": "PENDING",
  "checkout_url": "https://gateway.example.com/pay/ops_01HZGV...",
  "expires_at": "2026-04-24T05:00:00Z"
}

Step 3 — Redirect the Customer

Send the customer to checkout_url. The gateway handles:

  • Alias/IBAN entry
  • Bank selection
  • OTP authentication
  • Redirect back to your redirect_url with ?session_id=...&status=completed

Or embed the widget (coming with the Web SDK):

html
<script src="https://js.openwave.ly/v1/openwave.js"></script>
<script>
  openwave.checkout({
    sessionId: 'ops_01HZGV...',
    onSuccess: (e) => window.location.href = e.receipt_url,
    onFailed: (e) => showError(e.message),
  })
</script>

Step 4 — Receive Webhooks

Configure a public HTTPS endpoint to receive events. Always verify the signature:

js
// Express.js example
app.post('/webhooks/openwave', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-openwave-signature']
  const secret = process.env.WEBHOOK_SECRET

  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(req.body)
    .digest('hex')

  if (sig !== expected) return res.status(401).send('Invalid signature')

  const event = JSON.parse(req.body)

  if (event.event === 'payment.completed') {
    const { session_id, reference } = event.data
    // fulfil order using reference
    await fulfillOrder(reference)
  }

  res.json({ received: true })
})

Payment Events

EventWhen
payment.completedFunds deducted, transfer confirmed ✅
payment.failedOTP failure, timeout, or CBS error ❌
payment.expiredSession timed out before completion ⏱️

Step 5 — Verify (Optional Double-Check)

After receiving the webhook, you can verify the session status directly:

http
GET /api/v1/payments/sessions/{session_id}
Authorization: Bearer mk_live_...

Destination Types

TypeExampleNotes
aliasmtellesyNPT username, resolved to IBAN
alias with bankmtellesy@andalusSpecific bank routing
ibanLY83002700100099900001Direct IBAN

How Your Account Gets Credited

When a customer pays you, the money flows differently depending on whether you and the customer bank with the same institution:

ScenarioWhat happensTiming
Same bankInternal CBS book transfer — instant debit and credit< 1 second
Different banksCustomer's bank debits, sends via CBL LyPay to your bank, your bank credits2–10 seconds

You only need to act on payment.completed — this webhook fires only after the credit at your bank is confirmed. Never fulfil an order on payment.initiated or payment.processing alone.

json
{
  "event": "payment.completed",
  "data": {
    "session_id": "ops_01HZGV...",
    "reference": "order_1042",
    "settlement_type": "lypay",
    "creditor_bank": "andalus"
  }
}

The settlement_type field is internal for same-bank payments and lypay for cross-bank. Your bank handles the actual credit — the gateway simply confirms it happened and fires this webhook.

Cross-bank payments are still real-time

LyPay is a real-time settlement rail. Cross-bank payments typically complete in under 10 seconds — you don't need to poll or wait.

Recurring Payments

Set up a mandate for recurring charges (subscriptions, instalments):

http
POST /recurring/mandates
{
  "customer_alias": "mtellesy",
  "amount": 5000,
  "currency": "LYD",
  "interval": "monthly",
  "description": "Monthly subscription",
  "start_date": "2026-05-01"
}

Send the customer to the returned consent_url or open it in the official SDK/webview. The customer reviews the amount limit and frequency, then approves with bank OTP or push inside the hosted OpenWave surface. Do not collect OTPs in your merchant UI and do not call mandate approval endpoints from your backend.

Once active, you charge via:

http
POST /recurring/mandates/{mandate_id}/charge
{ "amount": 5000, "reference": "sub_may_2026" }

API Key Security

  • Never expose your API key in frontend code or mobile apps
  • Store it as an environment variable: OPENWAVE_API_KEY
  • Rotate your key immediately if you suspect it's compromised
  • Use mk_test_... keys in development and CI

Two-Tier Access Model

Merchant API keys cannot drive the checkout flow

resolve-payer, select-auth, and confirm are customer-facing endpoints. Any call to these with a merchant API key is rejected with 403 CHECKOUT_STEP_FORBIDDEN.

Your integration follows this split:

What you do (server-side)What the customer does (in browser)
Create session with merchant keyEnter alias/IBAN in the hosted page
Redirect to checkout_urlReceive OTP, confirm payment
Poll status or wait for webhook

This design ensures:

  • Your server never sees the customer's IBAN or phone — that data lives in the browser session only.
  • The customer controls the payment — you cannot trigger the debit from your backend after session creation.
  • Standard compliance — follows SCA / PSD2 principles for delegated authentication.

Use the Official SDK

js
// ✅ Correct — merchant server creates the session
const session = await astro.payments.createSession({ amount, currency, destination })
// Then redirect: window.location.href = session.checkout_url

// ❌ Wrong — merchant cannot call checkout steps
await astro.payments.resolvePayer(sessionId, { payer_alias: '09...' }) // 403 Forbidden

The checkout_url is the single handoff point: your server creates the session, the customer completes it.