Skip to content

Authentication

Pdirect uses a two-tier authentication model. Your merchant backend holds a long-lived merchant_secret and uses it once per checkout to mint a short-lived session_token. Your client — SDK or otherwise — only ever sees the session_token. This isolates card-data flows from credential theft on the client and prevents amount or currency tampering after the merchant has committed to a price.

Your merchant backend holds a long-lived merchant_secret and uses it once per checkout to mint a short-lived session_token from the gateway. Your mobile or web SDK never touches the merchant_secret — it only receives the session_token and presents it as Authorization: Bearer <session_token> on every payment call. The session is bound to a specific amount, currency, customer reference, and (optionally) device fingerprint. A new transaction needs a new session.

┌──────────────────┐ 1. start checkout ┌──────────────────┐
│ Mobile / Web │ ──────────────────────▶ │ Your merchant │
│ Pdirect SDK │ │ backend │
└──────────────────┘ └──────────────────┘
▲ │
│ │ 2. POST /api/v1/internal/sessions/create
│ │ Authorization: Bearer <merchant_secret>
│ ▼
│ ┌──────────────────┐
│ 3. session_token (one-time) │ Pdirect Gateway │
│ ◀──────────────────────────────── │ /api/v1/internal │
│ └──────────────────┘
│ ▲
│ │
│ │ 4. /api/v1/payments/collect, /submit, ...
│ 5. Authorization: Bearer <session_token> │ Idempotency-Key, X-Device-Fingerprint
└────────────────────────────────────────────┘
KeyWhere it livesWhat it does
merchant_secretYour merchant backend (env var)Mints session tokens via POST /internal/sessions/create
webhook_secretYour merchant backend (env var)Verifies the HMAC signature on inbound webhooks from the gateway. (Currently not yet enforced — see overview.)
session_tokenYour mobile / web SDK (per-checkout, ephemeral)Authenticates payment-collect / submit calls

merchant_secret and webhook_secret are shown exactly once when generated or rotated. Argon2id (merchant_secret) and Fernet encryption (webhook_secret) make the cleartext unrecoverable from the database after that point.

One call per checkout. Auth is HTTP Bearer with the value <key_id>:<merchant_secret> — the colon separator lets the gateway look up the key row by its public identifier before running the (intentionally slow) Argon2id verify.

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create \
-X POST \
-H "Authorization: Bearer mch_3f9a2e1b:sk_live_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 (returned 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 over your normal checkout-init API. The gateway will reject any attempt to reuse it for a different amount, currency, or customer.

The client presents the session_token as Authorization: Bearer <session_token> and submits the payment-method-specific fields. It does not re-send amount, currency, or customer_reference — the gateway reads those off the session.

Once your backend has minted a token and passed it to the client, the client calls the payment endpoints directly:

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"
}'

amount, currency, and customer_reference are bound to the session at mint time. Sending them in this body is still accepted (soft-checked against the session binding) but redundant — omit them.

  • Body rebindingamount, currency, and customer_reference are bound to the session at mint time. The gateway reads them directly from the session row, so the SDK does not need to re-send them. These fields remain accepted in the /payments/collect body for backwards compatibility (soft-checked against the session — drift returns a 400 with error_code: "1101", "1102", or "1103").
  • Device fingerprint binding — the first request on a session locks the device hash; subsequent requests must match. Default rollout mode is warn for 14 days before flipping to enforce.
  • Single-use sessions — once /payments/submit succeeds, the session is marked CONSUMED. Retries need a new session.
  • Idempotency-Key replay protection — same key + same body within 24 h replays the cached response; same key + different body returns 409. See Idempotency.
  • Status / revocation gates — both the session and the underlying merchant_secret must be in ALLOWED status with no revoked_at stamp. A revocation mid-flight returns 401 on the next request without waiting for TTL.

The old flow was:

  • The SDK sent app-key: <merchant_secret> as a request header.
  • The body of /payments/collect carried customer_reference, amount, and currency as required fields.

The new flow is:

  • Your merchant backend mints a session_token server-to-server by calling POST /api/v1/internal/sessions/create with Authorization: Bearer <key_id>:<merchant_secret> and (amount, currency, customer_reference) in the body.
  • The SDK presents the returned session_token as Authorization: Bearer <session_token>.
  • The body of /payments/collect omits amount, currency, and customer_reference — the gateway reads them from the session row.

The old contract still works: the gateway accepts the three legacy fields in the body when present and soft-checks them against the session binding. This makes the migration delta-friendly — bump your SDK at your own pace.

Migration checklist:

  1. Generate a merchant_secret (and webhook_secret once webhook signing ships) in the dashboard.
  2. Paste the cleartext + identifier into your merchant backend’s environment: PDIRECT_MERCHANT_KEY_ID and PDIRECT_MERCHANT_SECRET.
  3. Update your “checkout init” endpoint to call /api/v1/internal/sessions/create instead of returning a long-lived app_key.
  4. Hand the returned session_token to your client.
  5. Bump your mobile app to pdirect_pay 3.0.0 and rename token: to sessionToken: on PdirectPayConfigs. The legacy field still works in 3.x with a deprecation warning; it’s removed in 4.0.
  6. Drop customerReference, amount, and currency from PdirectPaymentBody (Flutter) / PdirectPaymentBody (Angular) — the SDK omits them on the wire when unset.