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.
What’s protected, and how
Section titled “What’s protected, and how”| Threat | Defence |
|---|---|
| Stolen mobile-app credential | Per-checkout session_token, single-use, 30-min default TTL. The long-lived merchant_secret never leaves your backend. |
| Body tampering after session-mint | Server-side rebinding on every payment call: amount, currency, and customer_reference must match the values declared at session-mint. |
| Replay of intercepted requests | Idempotency-Key cache in the merchant boundary; device-fingerprint binding inside the SDK boundary. |
| OTP brute force | 5 / 5 min rate limit on verify and resend. |
| Card-data theft | Mastercard Hosted Session — card data never touches your servers or ours; only the hosted-session ID passes through. |
| Open-redirect / phishing of customer | Per-merchant allow-list on notify_url, success_redirect_url, fail_redirect_url (fail-closed: empty list rejects all). |
| Rogue webhook origin | HMAC-SHA256 signature with replay window — see caveat below. |
Body binding
Section titled “Body binding”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.
Device fingerprint binding
Section titled “Device fingerprint binding”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... → lockedSecond request: X-Device-Fingerprint: 9f8e7d6c5b4a... → okThird request: X-Device-Fingerprint: aabbccddeeff... → 401This 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 with401.
The Angular SDK does not yet attach fingerprints. This will land with Auth-v3 in Angular v2.0.0.
Key types and rotation
Section titled “Key types and rotation”Three keys (Authentication covers the model in more depth):
| Key | Storage at rest | Lifetime |
|---|---|---|
merchant_secret | Argon2id hash | Long-lived, manually rotated |
webhook_secret | Fernet-encrypted ciphertext | Long-lived, manually rotated |
session_token | SHA-256 hash | 30-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.
Rotation
Section titled “Rotation”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.
IP allow-listing
Section titled “IP allow-listing”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.
Redirect URL allow-listing
Section titled “Redirect URL allow-listing”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.
PCI scope
Section titled “PCI scope”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.
Webhook authenticity
Section titled “Webhook authenticity”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.
Audit logging
Section titled “Audit logging”Every authenticated request leaves an audit record:
merchant_secret/session_tokenkey_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.
See also
Section titled “See also”- Authentication — the three keys in detail
- Idempotency — replay protection
- Webhooks overview — signature status and delivery