Skip to content

Card payments with 3DS

Card payments use Mastercard’s Hosted Session model. The customer enters their PAN, expiry, CVV directly into a Mastercard-hosted form; those values never touch your servers or ours. Only an opaque session_id flows through.

When the issuer requires 3-D Secure, the gateway drives the challenge — the customer is redirected, completes the challenge, and Mastercard posts back to /payments/card/3ds-callback.

1. backend POST /internal/sessions/create → session_token
2. client POST /payments/collect → transaction_id, status: pending
payment_method: "card"
mastercard_payment_method: "HOSTED_SESSION"
3. (Mastercard hosted form opens — customer enters PAN/exp/CVV)
4. client POST /payments/submit → status: processing
session_id (from hosted form)
mastercard_payment_method: "HOSTED_SESSION"
5. (3DS challenge — if issuer requires)
6. Mastercard POST /payments/card/3ds-callback → status: approved | declined
7. webhook POST notify_url → final state
OR
client GET /payments/check-status/{order_id}

Same as for any other payment method. Bind (amount, currency, customer_reference).

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create \
-X POST \
-H "Authorization: Bearer mch_3f9a2e1b:sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "amount": "100.00", "currency": "usd", "customer_reference": "cust_abc123" }'

Set payment_method: "card" and mastercard_payment_method: "HOSTED_SESSION". Pass user info; do not pass card fields (the hosted form will collect those). amount, currency, and customer_reference are read off the session — omit them from the body.

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/collect \
-X POST \
-H "Authorization: Bearer sess_..." \
-H "Content-Type: application/json" \
-d '{
"payment_method": "card",
"user_info": "full",
"payment_method_info": "none",
"fee_covered_by": "buyer",
"delivery_behaviour": "direct_delivery",
"notify_url": "https://merchant.example.com/pdirect/webhook",
"success_redirect_url": "https://merchant.example.com/checkout/success",
"fail_redirect_url": "https://merchant.example.com/checkout/fail",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_number": "+243998857000",
"mastercard_payment_method": "HOSTED_SESSION"
}'

The response includes a payment_url or session id you’ll hand to the hosted form.

The SDK widgets handle this automatically — they mount a WebView (Flutter) or use window.location (Angular) to navigate to the hosted form. The customer enters card details and is returned to your app or page.

If you’re driving the flow manually, you’ll need to mount Mastercard’s hosted-session widget yourself. The session ID from the hosted form becomes session_id on /submit.

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/submit \
-X POST \
-H "Authorization: Bearer sess_..." \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_encrypted_abc123",
"payment_method_id": "pm_card_001",
"mastercard_payment_method": "HOSTED_SESSION",
"session_id": "SESSION0002826410421L29410421"
}'

The gateway then drives the rest:

  • If the issuer doesn’t require 3DS, the response is status: "approved".
  • If 3DS is required, the response includes a redirect_url and status: "processing" (HTTP 202). Navigate the customer to the challenge page.

The customer authenticates with their issuer (typically a CAPTCHA or one-time code on their banking app). On completion, Mastercard posts to /payments/card/3ds-callback on the gateway — not on your server.

The gateway processes the callback, updates the local transaction, and returns the customer to your success_redirect_url or fail_redirect_url.

Use the notify_url webhook (preferred) or poll /payments/check-status/{order_id}:

Terminal window
curl https://app.api.gtwy.pdirect.com/api/v1/payments/check-status/order_2026_05_05_8842 \
-H "Authorization: Bearer sess_..."

This refreshes the upstream Mastercard status and mirrors it back.

For Flutter SDK integrations, the hosted-checkout WebView polls /payments/mastercard/status/{transaction_id} internally to drive the post-3DS spinner.

In sandbox, Mastercard provides a set of test PANs. The most useful:

PANBehaviour
5123 4500 0000 0008Approves immediately, no 3DS
5111 1111 1111 1118Approves after 3DS challenge
5333 3333 3333 3300Declined

Any future expiry and any 3-digit CVV. Use a real-looking holder name; some processor edges fail empty strings.

error_codeMeaningFix
1106 INVALID_CARD_NUMBERLuhn check failed (in non-hosted flows)Validate client-side
1107 INVALID_CARD_EXPIRYBad format or past dateValidate client-side
1108 INVALID_CARD_CVVNot 3–4 digitsValidate client-side
1304 INVALID_SIGNATURE3DS callback HMAC mismatchMisconfigured MASTERCARD_WEBHOOK_SECRET on gateway side; not your problem
1502 PAYMENT_DECLINEDIssuer declinedDon’t auto-retry; surface to customer

Using HOSTED_SESSION keeps your PCI DSS scope at SAQ A — the lightest of the self-assessment questionnaires. Card data passes between the customer and Mastercard’s hosted form; you and Pdirect both sit outside the PAN data path.

DIRECT_CAPTURE (when it ships) will require SAQ D from any integrator using it. Plan accordingly.