Integration Guide
Error Handling
PaymentApiException, retry policy, and how the engine surfaces failures so the UI clears its loading state.
Every backend call goes through PaymentEngine, which wraps PaymentApi calls in a configurable RetryPolicy and surfaces failures as a typed exception.
PaymentApiException
class PaymentApiException implements Exception {
final String message; // human-readable
final bool isRetryable; // safe to retry?
final int? statusCode; // HTTP status when applicable
final String? code; // typed code, e.g. MERCHANT_CREDENTIALS_MISSING
}
Common code values:
| Code | Meaning |
|---|---|
MERCHANT_CREDENTIALS_MISSING |
Api-Key or Secret-Key not configured. Check your env. |
GATEWAY_AUTH_REJECTED |
OnePay returned 4xx with no body — usually invalid credentials or unprovisioned merchant. |
SESSION_NOT_FOUND |
Tried to act on a paymentId the SDK has no context for. Initiate first. |
INVALID_RESPONSE |
Backend returned a non-JSON body where JSON was expected. |
CONTEXT_MISSING |
Internal — the per-paymentId scratch state was lost (usually a stale tab). |
Retry policy
RetryPolicy ships with sensible defaults:
const RetryPolicy(
maxAttempts: 3,
baseDelay: Duration(milliseconds: 250),
);
Delay grows exponentially (baseDelay × 2^(attempt-1)). Override by passing your own to PaymentEngine — useful for tests that want zero delay.
A retry is only attempted when both:
error.isRetryableis true (the API marks 408/425/429/5xx as retryable; 4xx as non-retryable).RetryPolicy.shouldRetry(error, attempt)returns true (or is null — defaults to "yes").
How the gateway surfaces errors
In gateway_host_app.dart, _runAction wraps every user-initiated call:
Future<void> _runAction(Future<void> Function() action) async {
try {
await action();
} catch (error) {
final friendly = _getUserFriendlyErrorMessage(error);
setState(() {
_statusLine = friendly;
_errorMessage = friendly;
});
// Re-hydrate the session so CheckoutPage receives a PaymentEvent and
// clears its loading modal — the engine doesn't emit on direct API
// failures from pre-initiated paths.
if (_session != null) component.sdk.restoreSession(_session!);
}
}
The pattern: catch the exception, show the message, and re-emit the current session so any "loading" UI clears.
Offline action queue (optional)
PaymentEngine accepts an OfflineActionStore. If a retryable error exhausts its retries while the customer's device is offline (no wifi, no data), the engine queues the action with its idempotency key. When the device comes back, call engine.replayQueuedActions() to drain the queue.
The default in-memory engine doesn't ship with a store; mobile hosts (when the Flutter SDK lands) plug in an SQLite-backed implementation.
Idempotency
Every state-changing request carries an idempotencyKey you control. Reusing the same key replays the response from the OnePay side instead of double-charging. Mint keys per logical action, not per HTTP retry — the engine's retry layer reuses the same key, which is correct.
sdk.generateQr(GenerateQrRequest(
paymentId: 'wpr_42',
idempotencyKey: 'idem_qr_$orderId',
qrProvider: 'OnePay',
));