DigetPay for Flutter
Accept card payments in your Flutter app in minutes. DigetPay defaults to a
hosted, PCI-safe WebView checkout — raw card data never passes through your
app, so you keep your PCI scope small — plus a full REST API for money
movement, recurring charges, and transaction history. Pure Dart, built on
http and webview_flutter; no native code to write or register.
await DigetPaySdk.cardPay()
.setOrder(DigetPaySaleOrder(id: orderId, description: 'Coffee', currency: 'SAR', amount: 15))
.setPayer(payer)
.onTransactionSuccess((res) => print('Paid ✓ ${res.transactionId}'))
.onTransactionFailure((res) => print('Failed: ${res.declineReason}'))
.onDismiss(() => print('User cancelled'))
.start(context);
Contents
- Install
- Quick start
- What you can do
- Hosted checkout
- Direct (S2S) card sale
- Paying a checkout session
- Transaction history & lookup by id
- Listing checkout sessions
- Money movement
- Recurring charges & subscriptions
- Handling transaction data
- Debug logging
- Platform setup
- Security — mobile API key
- Not yet available
Install
dependencies:
digetpay_plugin: ^0.5.0
flutter pub add digetpay_plugin
Quick start
Initialize once at startup, then call the static API anywhere:
import 'package:digetpay_plugin/digetpay_plugin.dart';
DigetPaySdk.initialize(
apiKey: '<your-mobile-api-key>',
baseUrl: kDigetPayDefaultBaseUrl, // sandbox: https://fin-api.digetpay.com/v1
// production: https://api.digetpay.com/v1
);
Every call before
initialize()throwsDigetPaySdkIsNotInitializedException.
initialize()has no re-entrancy guard — call it again (e.g. after a user picks a different environment or updates their API key) to reconfigure the SDK at any time.
That's it — you're ready to open the checkout.
What you can do
| ✅ Available now | Purpose |
|---|---|
cardPay() |
Hosted, PCI-safe card checkout (WebView) |
sale(request) |
Headless S2S card sale/authorization — ⚠️ widens PCI scope, see below |
payCheckoutSession(...) |
Pay an existing session & enroll a recurring plan — ⚠️ widens PCI scope, see below |
getCheckoutStatus(sessionId) |
Confirm a checkout's outcome (single transaction) |
getStatusCheckout(sessionId) |
A session's transactions, paginated — see below |
getCheckoutSessions([filter]) |
List your checkout sessions (paginated) |
getTransactionById(id) |
Full transaction details by gateway transaction id |
getTransactionHistory([filter]) |
List raw transaction records (paginated) |
capture · voidd |
Move money on an existing transaction |
refundS2s(...) · refundCheckout(...) |
Refund — pick the variant matching how you charged |
recurringS2s(request) · recurringCheckout(request) |
Charge a recurringToken — pick the variant matching how you charged |
getRecurringSubscriptions([filter]) |
List saved-card billing plans (paginated) |
chargeSubscription(...) |
Charge an existing subscription — no card data needed |
| 🚧 Not yet available | Behaviour |
|---|---|
applePay · externalPayment |
Throws UnsupportedError — no backend endpoint yet |
getTransactionByOrderId · getTransactionByRrn |
Throws UnsupportedError — no endpoint yet |
Hosted checkout (recommended card flow)
await DigetPaySdk.cardPay()
.setOrder(DigetPaySaleOrder(
id: orderId, description: 'Coffee', currency: 'SAR', amount: 15))
.setPayer(DigetPayPayer(
firstName: 'Demo', lastName: 'User', address: 'Riyadh',
country: 'SA', city: 'Riyadh', zip: '00000',
email: 'demo@example.com', phone: '+966500000000'))
// .setResultUrls(...) is optional — omit to use the SDK defaults.
.onTransactionSuccess((res) { /* res.status, res.transactionId ... */ })
.onTransactionFailure((res) { /* res.declineReason, res.errorCode */ })
.onDismiss(() { /* user backed out */ })
.start(context);
start() calls POST /payment/checkout/intiate, opens the returned
redirectUrl in a WebView and — once the hosted page reaches a result page —
fetches GET /sdk/status?sessionId= and dispatches success or failure from the
transaction's paymentStatus. Exactly one callback fires.
Flutter app
│ DigetPaySdk.cardPay()…start(context)
▼
POST /payment/checkout/intiate ──▶ { id, redirectUrl }
│ push CheckoutPage → load redirectUrl
▼
WebView (hosted card form + 3-D Secure)
│
├─ reaches a result page ──▶ GET /sdk/status?sessionId=<id> (authoritative)
│ ├─ paymentStatus == APPROVED ─▶ onTransactionSuccess(res)
│ └─ otherwise ─▶ onTransactionFailure(res)
│
└─ user backs out (no result page) ──▶ onDismiss()
intiateis spelled that way on the backend — kept verbatim so the docs match the actual wire call (it is not a typo in your code).- The status response is authoritative — the SDK does not trust the result
URL alone, because the gateway can route to
/successbriefly before settling on/failure.
Direct (S2S) card sale
⚠️ PCI scope.
cardPay()is the recommended default — raw card data never passes through your app.sale()is an explicit opt-in for a headless integration: your app collects the PAN/CVV directly, which widens your PCI-DSS scope. Only use it if you've independently assessed that tradeoff.
final response = await DigetPaySdk.sale(
SaleRequest(
orderId: orderId,
amount: 15,
currency: 'SAR',
auth: false, // true → authorization-only hold instead of a sale
customer: Customer(
name: 'Demo User',
email: 'demo@example.com',
phone: '+966500000000',
),
successUrl: 'https://example.com/success',
failureUrl: 'https://example.com/failure',
card: CardDto(
cardNumber: '4111111111111111',
cardHolder: 'Demo User',
cardExpiryMonth: '12',
cardExpiryYear: '2030',
cardCvv: '123',
),
),
);
sale() POSTs to /payment/s2s/sale. The integrity hash the backend requires
is computed automatically — MD5 of reverse(email) + apiKey + reverse(first6+last4 of the card number), uppercased — from
customer.email, card.cardNumber, and your configured API key. You never
need to compute or pass it yourself (set SaleRequest.hash only if you need
to override it, e.g. for auditing). The hash, PAN, and CVV are never logged.
Set auth: true to place an authorization-only hold instead of an immediate
sale — the response's data['action'] reads "SALE" or "AUTH" accordingly.
Completing 3-D Secure
Most cards require a 3-D Secure challenge before sale()'s result is final.
When that's needed, the response carries a self-submitting collector page:
response.needs3ds is true and response.html holds the page markup — the
payment stays PENDING until that page's form is submitted, which happens
automatically once it's loaded in a WebView. Reuse CheckoutPage (the same
widget cardPay() uses internally) to present it, passing raw html instead
of a checkoutUrl:
if (response.needs3ds) {
String? terminalUrl;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CheckoutPage(
html: response.html,
successUrl: successUrl, // the same URLs passed on the SaleRequest
failureUrl: failureUrl,
onFinished: (url) => terminalUrl = url,
),
),
);
// terminalUrl is now the success/failure return URL the challenge reached.
// For the authoritative outcome, look the payment up — e.g.
// DigetPaySdk.getTransactionById(response.paymentId!) — falling back to
// isSuccessUrl(terminalUrl!, successUrl) if that lookup is inconclusive.
}
See the example app's S2S Card Sale screen for a complete version of this,
including the getTransactionById fallback.
Paying a checkout session
payCheckoutSession() pays an existing checkout session with card details
you collected yourself, enrolling the card into a recurring plan at the same
time. Build the body with the constructor matching the plan you want:
⚠️ PCI scope. Like
sale(), this carries raw card data through your app. PrefercardPay()unless you have independently assessed PCI-DSS compliance.
// Charge now, and save the card for later on-demand charges:
final response = await DigetPaySdk.payCheckoutSession(
sessionId: sessionId,
request: const CheckoutPayRequest.unscheduled(
amount: 100,
cardNumber: '5123450000000008',
cardHolderName: 'Fares Mohamed',
cardExpiryMonth: '01',
cardExpiryYear: '2039',
cardCvv: '100',
email: 'customer@example.com',
),
);
// …or split the session across a fixed schedule:
const scheduled = CheckoutPayRequest.scheduled(
frequency: CheckoutBillingFrequency.weekly, // MONTHLY or WEEKLY
installments: 8,
cardNumber: '5123450000000008',
cardHolderName: 'Fares Mohamed',
cardExpiryMonth: '01',
cardExpiryYear: '2039',
cardCvv: '100',
email: 'customer@example.com',
);
The response usually carries a 3-D Secure collector page — handle it exactly
like sale() does:
if (response.needs3ds) {
// present response.html in a CheckoutPage(html: ...)
}
To read back everything recorded against a session, getStatusCheckout() hits
GET /payment/checkout/status and answers with a page of transactions:
final page = await DigetPaySdk.getStatusCheckout(sessionId);
for (final txn in page?.content ?? const <Transaction>[]) {
print('${txn.transactionType}: ${txn.paymentStatus}');
}
getStatusCheckout()vsgetCheckoutStatus()— they are different endpoints, not aliases.getCheckoutStatus()hits/sdk/statusand resolves a singleTransaction;getStatusCheckout()hits/payment/checkout/statusand returns aPageDto<Transaction>.
Transaction history & lookup by id
getTransactionById(id) fetches full details for a single transaction by its
gateway transaction id (GET /payment/transactions/digetpay/{id}/details),
including its businessUnitHierarchy. getTransactionHistory([filter]) lists
raw transaction records the same way, paginated:
final txn = await DigetPaySdk.getTransactionById(gatewayTransactionId);
final history = await DigetPaySdk.getTransactionHistory(
// pageNumber is 0-based — the first page is 0.
const TransactionFilter(pageNumber: 0, pageSize: 30, status: 'SUCCESS'),
);
Which method do I use?
getCheckoutSessions()lists checkout sessions (PageDto<CheckoutSession>);getTransactionHistory()lists raw transactions (PageDto<Transaction>— purchases, recurring charges, and refunds all appear here). They are not interchangeable — pick based on which shape you need.
Listing checkout sessions
getCheckoutSessions() returns a paginated PageDto<CheckoutSession> from
GET /payment/checkout/sessions:
final page = await DigetPaySdk.getCheckoutSessions(
const CheckoutSessionFilter(page: 1, limit: 20),
);
for (final session in page?.content ?? const <CheckoutSession>[]) {
print('${session.merchantOrderId} · ${session.amount} ${session.currency} · ${session.status}');
}
⚠️ Which id do I use?
A
CheckoutSessioncarries two ids. Forcapture·refundCheckout·voidd, always usesession.gatewayTransactionId— notsession.id(which identifies the checkout session, not the transaction).// ✅ correct — these sessions were paid via hosted checkout, so use the Checkout variant await DigetPaySdk.refundCheckout(transactionId: session.gatewayTransactionId!, amount: session.amount!); // ❌ wrong — session.id is NOT a transaction id
PageDto exposes content, number (the backend's 1-based page), size,
totalElements, totalPages, first, and last.
Money movement
Operate on an existing transaction by its gateway transaction id (no card data involved):
await DigetPaySdk.capture(transactionId: id, amount: 10); // POST /payment/s2s/capture
await DigetPaySdk.voidd(id); // POST /payment/s2s/void
Refunds have two endpoints — pick the one matching how the payment was taken, so the refund lands on the right rail:
// Charged with sale() — the direct/S2S rail:
await DigetPaySdk.refundS2s(transactionId: id, amount: 10); // POST /payment/s2s/refund
// Charged with cardPay() — the hosted-checkout rail:
await DigetPaySdk.refundCheckout(transactionId: id, amount: 10); // POST /payment/refund
refund(...)still works and is not deprecated — it is shorthand forrefundCheckout(...), exactly the endpoint it has always used. Prefer the explicit names in new code so the target is obvious at the call site.
These return a typed DigetPayResponse. HTTP errors and declines come back as an
unsuccessful response — they do not throw:
DigetPayResponse |
Meaning |
|---|---|
isSuccess |
2xx and not declined/failed |
status / result |
Gateway status text |
transactionId · orderId · rrn |
Identifiers |
message · errorCode · declineReason |
Failure detail |
data |
The raw decoded payload (for anything not typed above) |
Recurring charges & subscriptions
A recurring charge reuses a recurringToken from an earlier transaction — no
card data involved. Like refunds, this has two endpoints; pick the one
matching where the token came from:
final request = RecurringRequest(
transactionId: initialTransactionId,
orderId: 'ORD-2025-887899',
recurringToken: recurringToken,
amount: 100,
currency: 'SAR',
// Optional — when supplied it is nested verbatim as the `order` object;
// omit it and the key is left out of the request body entirely.
order: const Order(
number: 'ORD-2025-887899',
amount: 100,
currency: 'SAR',
description: 'Monthly subscription payment',
),
);
// Token from a direct sale() sent with recurringInit: true:
await DigetPaySdk.recurringS2s(request); // POST /payment/s2s/recurring
// Token from a hosted checkout (see Transaction.recurringToken):
await DigetPaySdk.recurringCheckout(request); // POST /payment/checkout/recurring
recurring(...)still works and is not deprecated — it is shorthand forrecurringCheckout(...), exactly the endpoint it has always used.
For saved-card billing plans (subscriptions), list them and charge one on demand — again, no card data needed, the subscription's saved token is charged server-side:
final page = await DigetPaySdk.getRecurringSubscriptions(
const RecurringSubscriptionFilter(page: 1, limit: 20),
);
for (final sub in page?.content ?? const <RecurringSubscription>[]) {
if (sub.isCompleted) continue;
final result = await DigetPaySdk.chargeSubscription(
subscriptionId: sub.id!,
amount: 50,
email: 'customer@example.com',
);
print(result?.isSuccess); // SubscriptionChargeResult — a flat, non-enveloped body
}
Handling transaction data
getCheckoutStatus returns a typed Transaction? (a DECLINED transaction is
still returned — inspect paymentStatus). Some fields are sensitive; the SDK
delivers them to you but never logs them:
pan— already masked by the backend (4323 2** **** 0853); safe to show.cardHolderName— PII; display it, but don't log/persist broadly.recurringToken— sensitive: it can initiate future charges. Keep it server-side, never log it, and don't surface it in the UI.
Debug logging
Pass an optional logger to initialize for a redacted, step-by-step trace
— HTTP method + path + status, checkout milestones, and the terminal outcome. It
never receives the API key, card data, request/response bodies, or query values.
import 'dart:developer' as developer;
DigetPaySdk.initialize(
apiKey: '<key>',
logger: (message) => developer.log(message, name: 'DigetPay'),
);
💳 checkout: creating session for order 6f… (15.0 SAR)
➡️ → POST /payment/checkout/intiate
⬅️ ← 201 OK POST /payment/checkout/intiate {code=201, session=ab02…, redirect=https://fin-admin.digetpay.com/pay/checkout}
🌐 webview → https://fin-admin.digetpay.com/pay/checkout/success
➡️ → GET /sdk/status?sessionId
⬅️ ← 200 OK GET /sdk/status {code=200, status=APPROVED, txn=070ae…, msg=Success}
✅ checkout: APPROVED → onTransactionSuccess
Leave logger unset (the default) for zero logging in production.
Platform setup
Pure-Dart plugin — no native code to write. You only need standard network/WebView configuration.
Android
-
minSdkVersion24 (Android 7.0) inandroid/app/build.gradle. -
INTERNET permission in
android/app/src/main/AndroidManifest.xml(the release manifest does not include it automatically):<uses-permission android:name="android.permission.INTERNET"/>
iOS
- Deployment target iOS 13.0+ in
ios/Podfileand the Runner target. - DigetPay endpoints and the hosted page are served over HTTPS, so the
default App Transport Security policy works with no changes. Avoid a blanket
NSAllowsArbitraryLoadsin production.
Hosted checkout targets Android and iOS (via webview_flutter); Flutter web
and desktop are not supported in this version.
Security — mobile API key
DigetPayConfig.apiKey is embedded in your app binary, and anything shipped in
a mobile app can be extracted. Always initialize with a publishable /
restricted mobile key scoped to client operations — never a full server
secret. Rotate the key if it is ever exposed, and keep privileged operations
behind your own server.
For card entry, prefer cardPay() —
raw card data never touches your app process, so your PCI scope stays small.
sale() is an explicit, opt-in exception to that: it
carries raw PAN/CVV through your app, which widens your PCI-DSS scope.
Only reach for it if you've independently assessed that tradeoff for your
integration.
Not yet available
applePay, externalPayment, getTransactionByOrderId, and
getTransactionByRrn throw UnsupportedError until the corresponding backend
endpoints exist (see the TODO(endpoint) markers in the source).
getTransactionByOrderId — list sessions with
getCheckoutSessions() and match on
CheckoutSession.merchantOrderId, or filter
getTransactionHistory() instead.
MIT © DigetPay · Issues & source: https://github.com/DigetPay/digetpay_plugin
Libraries
- digetpay_plugin
- DigetPay — pure-Dart Flutter payment plugin.