# 

## What you will build

An end-to-end payment flow for an e-commerce store that:

1. Tokenizes card data at checkout
2. Authorizes payment when the order is placed
3. Captures payment when the order ships
4. Handles cancellations and refunds


## Architecture

![image](https://files.modern-mermaid.live/images/1776277780602-mermaid-diagram-1776277780541.png)

## Prerequisites

* A sandbox API key — see [Obtain API credentials](/docs/how-tos/setup/obtain-credentials)
* A server-side application (Python, Node.js, or equivalent)
* A database for order and payment state


## Step 1: Design your payment data model

Track the relationship between orders and payments in your database.

| Field | Type | Purpose |
|  --- | --- | --- |
| `order_id` | string | Your internal order ID |
| `transaction_id` | string | Paradise Gateway transaction ID |
| `payment_token` | string | Tokenized payment method |
| `amount` | integer | Amount in cents |
| `state` | enum | `pending`, `authorized`, `captured`, `canceled`, `refunded` |
| `correlation_id` | string | For support requests |


## Step 2: Tokenize at checkout

When the customer submits payment details, tokenize the card before doing anything else. This keeps raw card data off your systems.

Python
```python
import requests, os

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}

def tokenize_card(card_data: dict, customer_id: str) -> str:
    resp = requests.post(f"{BASE}/v1/payment_tokens", headers=HEADERS, json={
        "customer_id": customer_id,
        "card": {
            "pan": card_data["pan"],
            "expiry_month": card_data["expiry_month"],
            "expiry_year": card_data["expiry_year"],
            "cvv": card_data["cvv"],
            "cardholder_name": card_data["name"],
        },
    })
    resp.raise_for_status()
    return resp.json()["token"]
```

JavaScript
```javascript
const API_KEY = process.env.PARADISE_API_KEY;
const BASE = process.env.PARADISE_BASE_URL || "https://api.dev.paradisegateway.net";
const headers = { "Content-Type": "application/json", "APIKEY": API_KEY };

async function tokenizeCard(cardData, customerId) {
  const resp = await fetch(`${BASE}/v1/payment_tokens`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      customer_id: customerId,
      card: {
        pan: cardData.pan,
        expiry_month: cardData.expiryMonth,
        expiry_year: cardData.expiryYear,
        cvv: cardData.cvv,
        cardholder_name: cardData.name,
      },
    }),
  });
  if (!resp.ok) throw new Error(`Tokenization failed: ${resp.status}`);
  const body = await resp.json();
  return body.token;
}
```

## Step 3: Authorize on order placement

Place a hold on the customer's card for the order amount. Do not capture yet — you'll do that when the order ships.

Python
```python
def authorize_payment(token: str, amount: int, order_id: str) -> dict:
    resp = requests.post(f"{BASE}/v1/transactions", headers=HEADERS, json={
        "type": "card_auth",
        "amount": amount,
        "payment_token": token,
        "order_id": order_id,
    })
    resp.raise_for_status()
    result = resp.json()

    if result["response_status"] != "approved":
        raise ValueError(f"Auth declined: {result['response_code']}")

    return {
        "transaction_id": result["id"],
        "correlation_id": result["correlation_id"],
        "state": "authorized",
    }
```

JavaScript
```javascript
async function authorizePayment(token, amount, orderId) {
  const resp = await fetch(`${BASE}/v1/transactions`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      type: "card_auth",
      amount,
      payment_token: token,
      order_id: orderId,
    }),
  });
  if (!resp.ok) throw new Error(`Auth request failed: ${resp.status}`);
  const result = await resp.json();

  if (result.response_status !== "approved") {
    throw new Error(`Auth declined: ${result.response_code}`);
  }

  return {
    transactionId: result.id,
    correlationId: result.correlation_id,
    state: "authorized",
  };
}
```

## Step 4: Capture on shipment

When the warehouse marks the order as shipped, capture the authorized amount.

Python
```python
def capture_payment(transaction_id: str, amount: int | None = None) -> dict:
    payload = {}
    if amount is not None:
        payload["amount"] = amount

    resp = requests.post(
        f"{BASE}/v1/transactions/{transaction_id}/capture",
        headers=HEADERS,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json()
```

JavaScript
```javascript
async function capturePayment(transactionId, amount = null) {
  const payload = amount !== null ? { amount } : {};
  const resp = await fetch(`${BASE}/v1/transactions/${transactionId}/capture`, {
    method: "POST",
    headers,
    body: JSON.stringify(payload),
  });
  if (!resp.ok) throw new Error(`Capture failed: ${resp.status}`);
  return resp.json();
}
```

Partial shipments
If you ship part of the order, capture only the shipped amount. See [Handle partial captures](/docs/how-tos/process-payments/handle-partial-capture).

## Step 5: Handle cancellations and refunds

![image](https://files.modern-mermaid.live/images/1776277818109-mermaid-diagram-1776277818051.png)

Python
```python
def cancel_or_refund(transaction_id: str, is_settled: bool, amount: int | None = None) -> dict:
    payload = {}
    if is_settled:
        payload["type"] = "card_refund"
        if amount:
            payload["amount"] = amount
    resp = requests.post(
        f"{BASE}/v1/transactions/{transaction_id}/cancel",
        headers=HEADERS,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json()
```

JavaScript
```javascript
async function cancelOrRefund(transactionId, isSettled, amount = null) {
  const payload = {};
  if (isSettled) {
    payload.type = "card_refund";
    if (amount) payload.amount = amount;
  }
  const resp = await fetch(`${BASE}/v1/transactions/${transactionId}/cancel`, {
    method: "POST",
    headers,
    body: JSON.stringify(payload),
  });
  if (!resp.ok) throw new Error(`Cancel/refund failed: ${resp.status}`);
  return resp.json();
}
```

## Step 6: Synchronize payment state

Keep your order database in sync with Paradise Gateway by checking transaction state.

```python
def sync_payment_state(transaction_id: str) -> str:
    resp = requests.get(f"{BASE}/v1/transactions/{transaction_id}", headers=HEADERS)
    resp.raise_for_status()
    return resp.json()["transaction_state"]
```

### State mapping

| Paradise Gateway state | Your order state | Next action |
|  --- | --- | --- |
| `authorized` | Order confirmed | Ship or cancel |
| `captured` | Payment pending settlement | Wait for settlement |
| `settled` | Payment complete | Fulfill order |
| `canceled` | Canceled | Restock inventory |
| `declined` | Payment failed | Prompt for new card |
| `refunded` | Refunded | Process return |


## Best practices

* **Always auth first, capture on ship** — avoids charging for out-of-stock items.
* **Store `transaction_id` and `correlation_id`** — you'll need them for refunds and support.
* **Use tokens for returning customers** — skip tokenization on repeat purchases.
* **Implement idempotency keys** — prevent duplicate charges on checkout retries.
* **Log every API interaction** — see [Log and monitor API errors](/docs/tutorials/error-handling/log-monitor-api-errors).
* **Handle declines gracefully** — see [Handle declined transactions](/docs/tutorials/payment-cookbook/handle-declined-transactions).


## Next steps

* [Authorize and capture separately](/docs/how-tos/process-payments/authorize-capture-separately)
* [Handle partial captures](/docs/how-tos/process-payments/handle-partial-capture)
* [Refund a settled transaction](/docs/how-tos/manage-transactions/refund-settled-auth)
* [Build a checkout page](/docs/tutorials/build-checkout-page)