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_token2. 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 | declined7. webhook POST notify_url → final state OR client GET /payments/check-status/{order_id}Step 1 — Mint the session
Section titled “Step 1 — Mint the session”Same as for any other payment method. Bind
(amount, currency, customer_reference).
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" }'Step 2 — Collect
Section titled “Step 2 — Collect”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.
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.
Step 3 — Hosted form
Section titled “Step 3 — 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.
Step 4 — Submit
Section titled “Step 4 — Submit”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_urlandstatus: "processing"(HTTP 202). Navigate the customer to the challenge page.
Step 5 — 3DS challenge
Section titled “Step 5 — 3DS challenge”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.
Step 6 — Reconcile
Section titled “Step 6 — Reconcile”Use the notify_url webhook (preferred) or poll
/payments/check-status/{order_id}:
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.
Test cards
Section titled “Test cards”In sandbox, Mastercard provides a set of test PANs. The most useful:
| PAN | Behaviour |
|---|---|
5123 4500 0000 0008 | Approves immediately, no 3DS |
5111 1111 1111 1118 | Approves after 3DS challenge |
5333 3333 3333 3300 | Declined |
Any future expiry and any 3-digit CVV. Use a real-looking holder name; some processor edges fail empty strings.
Common failures
Section titled “Common failures”error_code | Meaning | Fix |
|---|---|---|
1106 INVALID_CARD_NUMBER | Luhn check failed (in non-hosted flows) | Validate client-side |
1107 INVALID_CARD_EXPIRY | Bad format or past date | Validate client-side |
1108 INVALID_CARD_CVV | Not 3–4 digits | Validate client-side |
1304 INVALID_SIGNATURE | 3DS callback HMAC mismatch | Misconfigured MASTERCARD_WEBHOOK_SECRET on gateway side; not your problem |
1502 PAYMENT_DECLINED | Issuer declined | Don’t auto-retry; surface to customer |
PCI scope
Section titled “PCI scope”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.
See also
Section titled “See also”- Payment methods — card field reference
- API reference: Cards
- Security — what protects card data end-to-end