Quickstart
You will mint a session token from your merchant backend, then use
it to start a payment. By the end you will have a transaction_id
and a pending status from the sandbox gateway.
This page is the same flow in three stacks. Pick the tab that matches yours.
Step 1 — Mint a session
Section titled “Step 1 — Mint a session”Your merchant backend does this. The session token is bound to
the amount, currency, and customer_reference you pass.
curl https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create \ -X POST \ -H "Authorization: Bearer mch_3f9a2e1b:sk_test_x8a3pqr2zv9b1n4wmd6t7k0c8jh5y2u" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c2e9a1d-4b6f-4c8e-9a5f-1d3e2c7b9a4d" \ -d '{ "amount": "12.50", "currency": "usd", "customer_reference": "cust_abc123", "ttl_seconds": 1800 }'This call is server-side, even when integrating with a Flutter client. Add a tiny endpoint to your existing backend that mints the session and hands the token to your app.
The Flutter SDK does not mint sessions itself — that would require
embedding merchant_secret in the app binary, which is exactly what
Auth-v3 prevents.
// your-backend/routes/checkout.ts (Node example)import crypto from "node:crypto";
export async function startCheckout(req, res) { const { amount, currency, customerReference } = req.body;
const pdirectRes = await fetch( "https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create", { method: "POST", headers: { Authorization: `Bearer ${process.env.PDIRECT_MERCHANT_KEY_ID}:${process.env.PDIRECT_MERCHANT_SECRET}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ amount, currency, customer_reference: customerReference, ttl_seconds: 1800, }), }, ); const { session_token, expires_at } = await pdirectRes.json(); res.json({ session_token, expires_at });}Your Flutter app then calls your endpoint
(POST /api/checkout/start) — never /internal/sessions/create
directly.
The Angular SDK is on Auth-v3 — same model as Flutter and cURL. Mint
the session server-side (the SDK does not, and must not, hold
your merchant_secret):
// your-backend/routes/checkout.ts (Node example)import crypto from "node:crypto";
export async function startCheckout(req, res) { const { amount, currency, customerReference } = req.body;
const pdirectRes = await fetch( "https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create", { method: "POST", headers: { Authorization: `Bearer ${process.env.PDIRECT_MERCHANT_KEY_ID}:${process.env.PDIRECT_MERCHANT_SECRET}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ amount, currency, customer_reference: customerReference, ttl_seconds: 1800, }), }, ); const { session_token, expires_at } = await pdirectRes.json(); res.json({ session_token, expires_at });}Your Angular app then calls your endpoint to retrieve the
session_token and hands it to PdirectPayConfig.token.
You get back, exactly once:
{ "session_id": "607f1f77bcf86cd799439011", "session_token": "sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789", "issued_at": "2026-05-05T10:15:00Z", "expires_at": "2026-05-05T10:45:00Z"}Hand the session_token to your client. Do not log it.
Step 2 — Start a payment
Section titled “Step 2 — Start a payment”curl https://app.api.gtwy.pdirect.com/api/v1/payments/collect \ -X POST \ -H "Authorization: Bearer sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8d3f0e2b-5c7d-4e9f-aab2-2e4f3d8c0b6e" \ -d '{ "payment_method": "wallet", "user_info": "full", "payment_method_info": "full", "fee_covered_by": "buyer", "delivery_behaviour": "direct_delivery", "notify_url": "https://merchant.example.com/pdirect/webhook", "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "phone_number": "+243998857000", "wallet_number": "3005710720108954", "wallet_pin": "048698" }'amount, currency, and customer_reference are bound to the
session — omit them from this body. (They’re still accepted for
backwards compat; drift from the session binding is a 400.)
import 'package:flutter/material.dart';import 'package:pdirect_pay/pdirect_pay.dart';
void main() { // Once at app startup PdirectPay.init(const PdirectPayConfig( environment: PdirectPayEnvironment.sandbox, defaultLocale: 'en', )); runApp(const MyApp());}
class CheckoutScreen extends StatelessWidget { final String sessionToken; // fetched from your backend in step 1
const CheckoutScreen({super.key, required this.sessionToken});
@override Widget build(BuildContext context) { return PdirectPayCheckout( configs: PdirectPayConfigs( sessionToken: sessionToken, acceptLanguage: 'en', ), paymentBody: PdirectPaymentBody( // amount / currency / customerReference are bound to the // session at mint time — omit them from the body. paymentMethod: 'wallet', userInfo: 'full', paymentMethodInfo: 'full', feeCoveredBy: 'buyer', deliveryBehaviour: 'direct_delivery', notifyUrl: 'https://merchant.example.com/pdirect/webhook', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', phoneNumber: '+243998857000', walletNumber: '3005710720108954', walletPin: '048698', ), onResponse: (response) { debugPrint('payment ok: ${response.transactionId} ${response.paymentStatus}'); }, onError: (error) { debugPrint('payment failed: ${error.errorCode} ${error.message}'); }, ); }}The SDK auto-attaches Idempotency-Key (UUID v4) and
X-Device-Fingerprint (SHA-256 of stable device attributes) to every
POST. You don’t wire them yourself.
import { Component } from "@angular/core";import { PdirectPayCheckoutComponent, PdirectPayConfig, PdirectPaymentBody, PdirectPayOnResponse, PdirectPayOnError,} from "@mms/pdirect-pay";
@Component({ selector: "app-checkout", standalone: true, imports: [PdirectPayCheckoutComponent], template: ` <pdirect-pay-checkout [configs]="configs" [paymentBody]="paymentBody" (response)="onResponse($event)" (error)="onError($event)" /> `,})export class CheckoutComponent { // `token` carries the session_token your backend minted in step 1. // The SDK sends it as Authorization: Bearer <session_token>. configs: PdirectPayConfig = { token: sessionTokenFromYourBackend, isProduction: false, acceptLanguage: "en", };
paymentBody: PdirectPaymentBody = { // amount / currency / customerReference are bound to the session // — leave them unset and the SDK will omit them on the wire. paymentMethod: "wallet", userInfo: "full", paymentMethodInfo: "full", feeCoveredBy: "buyer", deliveryBehaviour: "direct_delivery", notifyUrl: "https://merchant.example.com/pdirect/webhook", firstName: "John", lastName: "Doe", email: "john.doe@example.com", phoneNumber: "+243998857000", walletNumber: "3005710720108954", walletPin: "048698", };
onResponse(r: PdirectPayOnResponse) { console.log("payment ok:", r.transactionId, r.paymentStatus); } onError(e: PdirectPayOnError) { console.warn("payment failed:", e.errorCode, e.message); }}The gateway responds with a transaction ID and a pending status:
{ "success": true, "transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d", "status": "pending", "message": "Payment request received", "amount": "12.50", "currency": "usd", "fee_amount": "0.50", "total_amount": "13.00", "created_at": "2026-05-05T10:15:00Z"}Sandbox wallet payments using +243998857000 and the test PIN above
will move to approved within a few seconds.
What just happened
Section titled “What just happened”- Your backend authenticated to the gateway with
Authorization: Bearer <key_id>:<merchant_secret>and declared(amount, currency, customer_reference)for this checkout. - The gateway minted a
session_tokenbound to those values, with a 30-minute TTL. - Your client presented the
session_tokenand submitted a matching payment body. The gateway accepted because the bound values matched. - The gateway forwarded to the underlying processor and returned a
transaction_idyou can poll withGET /payments/status/{transaction_id}or wait for a webhook on yournotify_url.
Next steps
Section titled “Next steps”- Receive the result. Set up a webhook endpoint to receive asynchronous status updates. See Webhooks.
- Handle errors. Read the Errors page to recognise the four-digit code system.
- Go beyond wallet. Card payments,
mobile money, and
B2C payouts follow the same
mint session → submit body → poll or webhookpattern. - Read the SDK reference. Flutter / Angular.