Your first request
This page traces a single successful payment from the moment your backend mints a session to the moment the customer’s wallet shows the new balance. We use cURL throughout so you can copy each request verbatim — the Quickstart covers the same flow in Flutter and Angular.
Sandbox base URL throughout: https://app.api.gtwy.pdirect.com.
The scenario
Section titled “The scenario”A merchant called Acme Coffee wants to charge a customer $12.50
in USD for an order keyed by cust_abc123. The customer pays
from their Pdirect wallet.
1 — Mint the session
Section titled “1 — Mint the session”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 }'Response (HTTP 201):
{ "session_id": "607f1f77bcf86cd799439011", "session_token": "sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789", "issued_at": "2026-05-05T10:15:00Z", "expires_at": "2026-05-05T10:45:00Z"}What just happened:
- Your
merchant_secretwas looked up bykey_id(mch_3f9a2e1b) and verified with Argon2id. - A 32-byte random token was generated, hashed (SHA-256), and stored. Only the cleartext crosses the wire — exactly once.
- The session row was bound to
(12.50, usd, cust_abc123). Idempotency-Keywas cached against(session_create, body_hash)for 24 h. If you replay this exact request you’ll get the same response without minting a new session.
2 — Start the payment
Section titled “2 — Start the payment”The customer’s app — or your cURL terminal — now uses the session
token. Note that the body does not carry amount, currency,
or customer_reference: those are bound to the session and the
gateway reads them off the session row.
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" }'Response (HTTP 200):
{ "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"}What just happened:
- The gateway extracted the bearer, hashed it, and looked up the
session row. Status =
ACTIVE, expiry not reached, underlyingmerchant_secretstillALLOWED. - It read
(amount, currency, customer_reference)straight off the session’s bound values. Had the body re-sent any of those three (legacy clients still do), the gateway would have soft-checked them byte-for-byte against the binding — drift returns a400witherror_code: "1101","1102", or"1103". - It created a transaction and dispatched to the wallet processor.
- A
pendingstatus came back synchronously. Settlement will land asynchronously.
3 — Wait for settlement
Section titled “3 — Wait for settlement”You have two options. Use webhooks in production; polling is fine for local development.
Option A — Webhook (preferred)
Section titled “Option A — Webhook (preferred)”Within a few seconds the gateway POSTs to your notify_url:
POST /pdirect/webhook HTTP/1.1Host: merchant.example.comContent-Type: application/json
{ "transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d", "customer_reference": "cust_abc123", "status": "approved", "amount": "12.50", "currency": "usd", "completed_at": "2026-05-05T10:15:08Z"}Read Webhooks overview for delivery guarantees, retry policy, and the event types catalogue.
Option B — Poll
Section titled “Option B — Poll”curl https://app.api.gtwy.pdirect.com/api/v1/payments/status/txn_8f3a4c2e9b1d7a6f5c0e8d \ -H "Authorization: Bearer sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"Response:
{ "transaction_id": "txn_8f3a4c2e9b1d7a6f5c0e8d", "status": "approved", "amount": "12.50", "currency": "usd", "created_at": "2026-05-05T10:15:00Z", "completed_at": "2026-05-05T10:15:08Z"}Notes:
- The session you minted in step 1 is the same session you use to
read status — it’s still active until you call
/payments/submitsuccessfully (which marks itCONSUMED) or it expires. - For mobile-money flows, prefer
/payments/mobile-money/status/{payment_id}— it’s optimised for live polling with anestimated_completionhint.
What you’ve learned
Section titled “What you’ve learned”- Sessions are minted server-side, never on the client. Your
merchant_secretdoes not leave your backend. - Sessions bind
amount,currency, andcustomer_reference. The SDK doesn’t re-send them — the gateway reads them from the session. Legacy clients that still include them are soft-checked; drift is a hard400. - Idempotency keys make retries safe — same key + same body replays the cached response within 24 h.
- The same session reads status (until consumed or expired).
- Settlement is asynchronous. Webhooks beat polling for production, but polling works in a pinch.
What to read next
Section titled “What to read next”- Authentication — the full
three-keys model including
webhook_secretand rotation. - Errors — the error envelope and the four-digit code groups.
- Idempotency — replay semantics in detail.
- Guides → Collect payments — same flow with all the optional fields and edge cases.