Skip to content
Last updated

Tutorial: Build a checkout page

What you will learn

  1. Collect payment details in a form
  2. Tokenize the card to reduce PCI scope
  3. Process a sale using the token
  4. Display the result to the customer

Prerequisites

Architecture overview

image

PCI scope

By tokenizing on your server and never logging raw card data, you minimize your PCI-DSS scope. For the lowest scope (SAQ A), consider a hosted payment form. See PCI-DSS overview.

Step 1: Build the checkout form

Create a simple HTML form that collects the card number, expiry, CVV, and cardholder name.

<form id="checkout-form">
  <label>Cardholder name
    <input name="name" placeholder="John Doe" required />
  </label>
  <label>Card number
    <input name="pan" placeholder="4111 1111 1111 1111" maxlength="19" required />
  </label>
  <label>Expiry
    <input name="expiry_month" placeholder="MM" maxlength="2" required />
    <input name="expiry_year" placeholder="YYYY" maxlength="4" required />
  </label>
  <label>CVV
    <input name="cvv" placeholder="123" maxlength="4" required />
  </label>
  <label>Amount ($)
    <input name="amount" type="number" step="0.01" min="0.50" value="19.99" required />
  </label>
  <button type="submit">Pay now</button>
</form>

Step 2: Submit to your server

When the form submits, send the data to your backend over HTTPS. Never call the Paradise Gateway API directly from the browser — your API key must stay server-side.

document.getElementById("checkout-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const form = new FormData(e.target);
  const resp = await fetch("/api/checkout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: form.get("name"),
      pan: form.get("pan").replace(/\s/g, ""),
      expiry_month: form.get("expiry_month"),
      expiry_year: form.get("expiry_year"),
      cvv: form.get("cvv"),
      amount: Math.round(parseFloat(form.get("amount")) * 100),
    }),
  });
  const result = await resp.json();
  if (result.response_status === "approved") {
    document.body.innerHTML = "<h1>Payment successful!</h1><p>Transaction: " + result.id + "</p>";
  } else {
    document.body.innerHTML = "<h1>Payment failed</h1><p>" + result.response_message + "</p>";
  }
});

Step 3: Tokenize the card (server-side)

On your server, first tokenize the card. This stores it securely and returns a reusable payment_token.

import os, requests

API_KEY = os.environ["PARADISE_API_KEY"]
BASE = "https://api.dev.paradisegateway.net"

def tokenize_card(name, pan, expiry_month, expiry_year):
    resp = requests.post(
        f"{BASE}/v1/payment_tokens",
        headers={"Content-Type": "application/json", "APIKEY": API_KEY},
        json={
            "customer_id": "CUSTOMER-01KFDKXMQ637EKEAY410MSQSXB",
            "nickname": name,
            "card": {
                "pan": pan,
                "expiry_month": expiry_month,
                "expiry_year": expiry_year,
                "cardholder_name": name,
            },
        },
    )
    resp.raise_for_status()
    return resp.json()["payment_token"]

Step 4: Process the sale (server-side)

Use the token to charge the customer. This keeps raw card data out of the transaction request.

def process_sale(payment_token, amount):
    resp = requests.post(
        f"{BASE}/v1/transactions",
        headers={"Content-Type": "application/json", "APIKEY": API_KEY},
        json={
            "type": "sale",
            "amount": amount,
            "payment_method": {
                "type": "payment_token",
                "payment_token": payment_token,
            },
        },
    )
    resp.raise_for_status()
    return resp.json()

Step 5: Handle the result

Check response_status to determine what to show the customer.

# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/api/checkout", methods=["POST"])
def checkout():
    data = request.json
    token = tokenize_card(
        data["name"], data["pan"],
        data["expiry_month"], data["expiry_year"],
    )
    result = process_sale(token, data["amount"])

    if result["response_status"] == "approved":
        return jsonify({"response_status": "approved", "id": result["id"]})
    else:
        return jsonify({
            "response_status": result["response_status"],
            "response_message": result.get("response_message", "Payment failed"),
        }), 400

Complete flow diagram

image

Security checklist

  • API key stored in environment variable, not in client-side code
  • HTTPS on your checkout page
  • Raw PAN never logged or stored on your server
  • Token used for payment instead of raw card data
  • Error messages do not expose internal details to the customer

Next steps