PPayNow Docs
Menu — Server-Side Initiation

Integration Guide

Server-Side Initiation

Every checkout starts with a server-to-server call to /web-payment/initiate. Here's why and how.

Your storefront's "Place order" button must hit your own server, not OnePay directly. The server then calls /web-payment/initiate with the merchant Api-Key / Secret-Key, receives a paymentId and a redirect URL, and hands those back to the browser.

Why server-side

Calling OnePay from the browser would put your merchant credentials in every visitor's network tab. The whole point of the Api-Key / Secret-Key pair is that it's yours, not the customer's.

The merchant demo (examples/paynow_jaspr_ecommerce_demo) ships a working reference: lib/merchant_initiate_handler.dart is a shelf middleware mounted on /api/initiate-payment.

The middleware (annotated)

import 'dart:convert';
            import 'dart:io';
            
            import 'package:paynow_core/paynow_core.dart';
            import 'package:shelf/shelf.dart';
            
            Middleware merchantInitiateMiddleware({
              String path = '/api/initiate-payment',
              PayNowOnePayApi? api,
              Map<String, String>? environment,
            }) {
              // CRITICAL: read credentials from the process env at request time.
              // Never use String.fromEnvironment — that bakes them into the JS bundle.
              final env = environment ?? Platform.environment;
              final resolvedApi = api ?? _buildApiFromEnv(env);
            
              return (Handler inner) {
                return (Request request) async {
                  if (request.method != 'POST' || request.url.path != path.substring(1)) {
                    return inner(request);
                  }
            
                  final payload = jsonDecode(await request.readAsString())
                      as Map<String, dynamic>;
            
                  final result = await resolvedApi!.initiateWebPayment(
                    WebPaymentInitiationRequest(
                      orderId: payload['orderId'] as String,
                      amountMinor: payload['amountMinor'] as int,
                      currency: payload['currency'] as String,
                      customerPhone: payload['customerPhone'] as String?,
                      callbackUrl: env['MERCHANT_RETURN_BASE'],
                      merchantId: payload['merchantId'] as String?,
                    ),
                  );
            
                  // Build the gateway redirect URL. The browser navigates to this.
                  final redirectUrl = _buildGatewayRedirectUrl(
                    gatewayBase: env['PAYNOW_GATEWAY_BASE'],
                    paymentId: result.paymentId,
                    // ...other query params...
                    bearerToken: result.bearerToken,
                  );
            
                  return Response.ok(
                    jsonEncode({
                      'paymentId': result.paymentId,
                      'redirectUrl': redirectUrl,
                      'token': result.bearerToken,
                    }),
                    headers: const {'content-type': 'application/json'},
                  );
                };
              };
            }
            

Wire it into your server

In your Jaspr server's bin/server.dart, register the middleware before runApp:

import 'package:jaspr/server.dart';
            import 'merchant_initiate_handler.dart';
            
            void main() {
              Jaspr.initializeApp();
              ServerApp.addMiddleware(merchantInitiateMiddleware());
              runApp(const PayNowEcommerceDocument());
            }
            

What the browser does

// On "Place order" click
            const response = await fetch('/api/initiate-payment', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                orderId: 'inv_abc_001',
                amountMinor: 25900,         // 25.900 LYD (3 fraction digits)
                currency: 'LYD',
                customerPhone: '+218910000000',
                merchantId: 'your_merchant_id',
              }),
            });
            const { redirectUrl, paymentId } = await response.json();
            window.location.href = redirectUrl;
            

The server returns the gateway URL with the paymentId, bearer JWT, and any locale / return-URL query params already attached. The browser navigates there and the customer enters the QR / account / OTP flow.

Idempotency

/web-payment/initiate is not idempotent on its own — repeated calls with the same orderId create duplicate sessions. Make sure your server runs the initiate exactly once per order, e.g. by short-circuiting on a stored paymentId in your orders table.