Skip to content
Last updated

Tutorial: PCI-compliant checkout

Choose your integration approach

Your PCI scope depends on how your system handles cardholder data. Paradise Gateway supports several approaches.

image

ApproachPCI scopeCard data touches your server?Complexity
Server-side tokenizationSAQ DYes — briefly, during tokenizationMedium
Client-side direct postSAQ A-EPNo — browser posts directly to APILow
Hosted payment pageSAQ ANo — card form hosted by gatewayLowest

Approach 1: Server-side tokenization (SAQ D)

Your server receives card data from the browser, immediately tokenizes it via the API, then uses the token for all subsequent operations. The raw card data never persists on your systems.

image

Implementation

from flask import Flask, request, jsonify
import requests, os

app = Flask(__name__)
API_KEY = os.environ["PARADISE_API_KEY"]
BASE = os.environ.get("PARADISE_BASE_URL", "https://api.dev.paradisegateway.net")
HEADERS = {"Content-Type": "application/json", "APIKEY": API_KEY}

@app.route("/checkout", methods=["POST"])
def checkout():
    data = request.json

    token_resp = requests.post(f"{BASE}/v1/payment_tokens", headers=HEADERS, json={
        "customer_id": data["customer_id"],
        "card": {
            "pan": data["pan"],
            "expiry_month": data["expiry_month"],
            "expiry_year": data["expiry_year"],
            "cvv": data["cvv"],
            "cardholder_name": data["cardholder_name"],
        },
    })
    if token_resp.status_code != 200:
        return jsonify({"error": "Tokenization failed"}), 400

    token = token_resp.json()["token"]

    sale_resp = requests.post(f"{BASE}/v1/transactions", headers=HEADERS, json={
        "type": "card_sale",
        "amount": data["amount"],
        "payment_token": token,
    })
    result = sale_resp.json()
    return jsonify({
        "status": result["response_status"],
        "transaction_id": result["id"],
    })

SAQ D requirements checklist

  • Encrypt all cardholder data in transit (TLS 1.2+)
  • Never store raw PAN, CVV, or track data
  • Implement access controls and logging
  • Regularly scan for vulnerabilities
  • Maintain a written security policy

Approach 2: Client-side direct post (SAQ A-EP)

The browser sends card data directly to Paradise Gateway. Your server never sees the raw card details — it only receives the token.

image

Implementation (JavaScript)

<form id="checkout-form">
  <input name="pan" placeholder="4111 1111 1111 1111" required />
  <input name="expiry_month" placeholder="MM" required />
  <input name="expiry_year" placeholder="YYYY" required />
  <input name="cvv" placeholder="123" required />
  <input name="cardholder_name" placeholder="John Doe" required />
  <button type="submit">Pay $19.99</button>
</form>

<script>
document.getElementById("checkout-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const form = new FormData(e.target);

  // Step 1: Tokenize directly with Paradise Gateway
  const tokenResp = await fetch("https://api.dev.paradisegateway.net/v1/payment_tokens", {
    method: "POST",
    headers: { "Content-Type": "application/json", "APIKEY": "PUBLIC_TOKENIZATION_KEY" },
    body: JSON.stringify({
      card: {
        pan: form.get("pan"),
        expiry_month: form.get("expiry_month"),
        expiry_year: form.get("expiry_year"),
        cvv: form.get("cvv"),
        cardholder_name: form.get("cardholder_name"),
      },
    }),
  });
  const { token } = await tokenResp.json();

  // Step 2: Send token to your server
  const orderResp = await fetch("/checkout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token, amount: 1999 }),
  });
  const result = await orderResp.json();
  alert(result.status === "approved" ? "Payment successful!" : "Payment failed.");
});
</script>
CORS and key scope

Client-side tokenization requires a tokenization-only API key that can be safely exposed in the browser. Verify that CORS headers allow requests from your domain.

Security checklist

Regardless of which approach you choose:

RequirementDetails
TLS everywhereAll pages that collect or display payment data must use HTTPS
No PAN in logsNever log full card numbers — mask to last four digits
No CVV storageNever store CVV anywhere, for any duration
Content Security PolicyRestrict script sources to prevent XSS that could steal card data
Subresource IntegrityPin hashes on third-party scripts loaded on payment pages
Input validationLuhn-check card numbers, validate expiry dates client-side before submission

Decision guide

image

FactorServer-sideClient-sideHosted
PCI scopeSAQ D (full)SAQ A-EP (reduced)SAQ A (minimal)
UX controlFullFullLimited
Implementation effortMediumLowLowest
Card data on your serverBrieflyNeverNever

Best practices

  • Tokenize immediately — if card data touches your server, tokenize in the same request and discard the raw data.
  • Use separate keys — if client-side tokenization is available, use a restricted key that can only create tokens.
  • Test with sandbox — verify your PCI-compliant flow works end-to-end before going to production.
  • Audit annually — complete your SAQ annually and maintain documentation of your data flow.
  • Review CSP headers — ensure your Content Security Policy allows connections to api.dev.paradisegateway.net (sandbox) and the production endpoint.

Further reading