Skip to content

Security

The gateway’s security model is layered. Each layer assumes the ones above it can be defeated; together they make a single stolen credential or a single compromised device materially uninteresting to an attacker.

ThreatDefence
Stolen mobile-app credentialPer-checkout session_token, single-use, 30-min default TTL. The long-lived merchant_secret never leaves your backend.
Body tampering after session-mintServer-side rebinding on every payment call: amount, currency, and customer_reference must match the values declared at session-mint.
Replay of intercepted requestsIdempotency-Key cache in the merchant boundary; device-fingerprint binding inside the SDK boundary.
OTP brute force5 / 5 min rate limit on verify and resend.
Card-data theftMastercard Hosted Session — card data never touches your servers or ours; only the hosted-session ID passes through.
Open-redirect / phishing of customerPer-merchant allow-list on notify_url, success_redirect_url, fail_redirect_url (fail-closed: empty list rejects all).
Rogue webhook originHMAC-SHA256 signature with replay window — see caveat below.

Your merchant backend declares (amount, currency, customer_reference) when it mints the session; those values are stored on the session row. The gateway reads them straight off the session on every /payments/collect call — the SDK does not need to re-send them in the request body.

Legacy callers that still send those three fields are soft-checked byte-for-byte against the session binding. Any drift returns a 400 with error_code: "1101" (amount), "1102" (currency), or "1103" (customer_reference).

Session minted with: amount="12.50" currency="usd" customer_reference="cust_abc123"
Legacy SDK sends /collect: amount="12.51" ...
Drift → 400 with error_code: "1101"

This means a compromised mobile app cannot:

  • Inflate the amount and pocket the difference
  • Charge a different customer’s reference
  • Switch currencies to a depreciated one to gain arbitrage

It also means you cannot apply a discount client-side after session-mint — discounts must be reflected in the session’s amount, which means re-minting if the customer changes their cart.

The Flutter SDK auto-attaches X-Device-Fingerprint (SHA-256 of stable device attributes) to every request. The first authenticated request on a session locks that fingerprint; subsequent requests must match.

First request: X-Device-Fingerprint: 9f8e7d6c5b4a... → locked
Second request: X-Device-Fingerprint: 9f8e7d6c5b4a... → ok
Third request: X-Device-Fingerprint: aabbccddeeff... → 401

This forces a session compromise to also include the original device — a stolen session_token alone is useless from a different device.

The mode is gradual:

  • off — no fingerprint enforcement.
  • warn (default for the first 14 days of a session’s life) — records mismatches in audit logs but does not reject.
  • enforce — rejects mismatches with 401.

The Angular SDK does not yet attach fingerprints. This will land with Auth-v3 in Angular v2.0.0.

Three keys (Authentication covers the model in more depth):

KeyStorage at restLifetime
merchant_secretArgon2id hashLong-lived, manually rotated
webhook_secretFernet-encrypted ciphertextLong-lived, manually rotated
session_tokenSHA-256 hash30-min default TTL, single-use

Cleartext for merchant_secret and webhook_secret is shown once at creation or rotation. The hashes / ciphertexts in the database are not reversible — losing the cleartext means rotating.

Rotate merchant_secret when:

  • A backend developer with prod access leaves
  • A backup or log file containing the cleartext is discovered
  • The default 90-day rotation cadence is reached

The dashboard supports overlapping active keys: the new key works while the old one is still being phased out of your deployment.

You can restrict each merchant_secret to a CIDR list of source IPs. A request from outside the list is 401 even with valid credentials. This is recommended for production secrets; sandbox secrets typically remain unrestricted for development convenience.

Every notify_url, success_redirect_url, and fail_redirect_url must be on a per-merchant host allow-list. Empty list is a fail-closed default — until you populate it, all redirects are rejected.

This prevents a compromised SDK from redirecting users to an attacker-controlled phishing site, and prevents a compromised merchant backend from forwarding webhooks somewhere unexpected.

Use Mastercard Hosted Session for card payments (Card payments with 3DS). The customer’s PAN, CVV, and expiry are entered into a Mastercard-hosted form; only an opaque session ID flows through your servers and ours.

Pdirect’s PCI DSS scope is bounded to the gateway’s outbound Mastercard connection. As an integrator, your scope under HOSTED_SESSION is SAQ A — substantially lighter than full direct capture.

If you have a future need for DIRECT_CAPTURE (where card data flows through the gateway), expect a separate compliance conversation; the field is reserved in the schema but not yet implemented.

Until signing ships, treat your notify_url as a soft secret — don’t share it publicly, scrutinise webhook payloads against the local transaction record, and don’t credit the customer until the gateway confirms via a status query.

Every authenticated request leaves an audit record:

  • merchant_secret / session_token key_id
  • Source IP and User-Agent
  • HTTP method, path, response status, duration
  • Body fingerprint (not the body itself — payment-sensitive bodies are never logged)
  • request_id (echoed in the error envelope)

Quote the request_id when contacting support. They can pull the audit record with that alone.