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.