Skip to content

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.

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.

Terminal window
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_secret was looked up by key_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-Key was 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.

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.

Terminal window
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, underlying merchant_secret still ALLOWED.
  • 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 a 400 with error_code: "1101", "1102", or "1103".
  • It created a transaction and dispatched to the wallet processor.
  • A pending status came back synchronously. Settlement will land asynchronously.

You have two options. Use webhooks in production; polling is fine for local development.

Within a few seconds the gateway POSTs to your notify_url:

POST /pdirect/webhook HTTP/1.1
Host: merchant.example.com
Content-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.

Terminal window
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/submit successfully (which marks it CONSUMED) or it expires.
  • For mobile-money flows, prefer /payments/mobile-money/status/{payment_id} — it’s optimised for live polling with an estimated_completion hint.
  1. Sessions are minted server-side, never on the client. Your merchant_secret does not leave your backend.
  2. Sessions bind amount, currency, and customer_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 hard 400.
  3. Idempotency keys make retries safe — same key + same body replays the cached response within 24 h.
  4. The same session reads status (until consumed or expired).
  5. Settlement is asynchronous. Webhooks beat polling for production, but polling works in a pinch.