> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oneclickdz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Step 2: Payment Flow

> Simple guide to implementing payments in your application

## Overview

The payment flow is simple:

1. Customer creates order → Save as `PENDING`
2. Create payment link → Get `paymentRef`
3. **Save `paymentRef` with order** (important!)
4. Redirect customer to payment page
5. Customer pays and returns
6. Check payment status and update order

## Complete Implementation

### Step 1: Order Creation

When a customer places an order, save it with a `PENDING` status:

```javascript theme={null}
async function createOrder(customerData, items, total) {
  // Generate unique order ID
  const orderId = generateUniqueId(); // e.g., "ORD-2025-001234"

  // Save order to database
  const order = await db.orders.create({
    id: orderId,
    customerId: customerData.id,
    items: items,
    totalAmount: total,
    status: "PENDING", // Important: Start as PENDING
    paymentRef: null, // Will be set after creating payment link
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  return order;
}
```

<Warning>
  **Critical**: Every order must have a unique ID and must start with status
  `PENDING`
</Warning>

### Step 2: Create Payment Link

<CodeGroup>
  ```javascript Node.js / Express theme={null}
  const express = require("express");
  const fetch = require("node-fetch");
  const app = express();

  app.use(express.json());

  // 1. Create order and redirect to payment
  app.post("/checkout", async (req, res) => {
    const { items, total, customerId } = req.body;

    try {
      // Create order in database
      const orderId = generateOrderId();
      await db.orders.create({
        id: orderId,
        customerId: customerId,
        items: items,
        total: total,
        status: "PENDING",
      });

      // Create payment link
      const response = await fetch(
        "https://api.oneclickdz.com/v3/ocpay/createLink",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Access-Token": process.env.ONECLICK_API_KEY,
          },
          body: JSON.stringify({
            productInfo: {
              title: `Order ${orderId}`,
              amount: total,
            },
            redirectUrl: `https://yoursite.com/orders/${orderId}`,
          }),
        }
      );

      const data = await response.json();

      // IMPORTANT: Save paymentRef with order
      await db.orders.update(orderId, {
        paymentRef: data.data.paymentRef,
      });

      // Redirect to payment page
      res.redirect(data.data.paymentUrl);
    } catch (error) {
      console.error("Checkout error:", error);
      res.status(500).json({ error: "Checkout failed" });
    }
  });

  // 2. Order status page
  app.get("/orders/:orderId", async (req, res) => {
    const { orderId } = req.params;

    try {
      const order = await db.orders.findOne({ id: orderId });

      if (!order) {
        return res.status(404).send("Order not found");
      }

      // Check payment if still pending
      if (order.status === "PENDING" && order.paymentRef) {
        const statusResponse = await fetch(
          `https://api.oneclickdz.com/v3/ocpay/checkPayment/${order.paymentRef}`,
          {
            headers: { "X-Access-Token": process.env.ONECLICK_API_KEY },
          }
        );

        const statusData = await statusResponse.json();

        if (statusData.data.status === "CONFIRMED") {
          await db.orders.update(orderId, { status: "PAID" });
          order.status = "PAID";
        } else if (statusData.data.status === "FAILED") {
          await db.orders.update(orderId, { status: "FAILED" });
          order.status = "FAILED";
        }
      }

      res.render("order-status", { order });
    } catch (error) {
      console.error("Error:", error);
      res.status(500).send("Error loading order");
    }
  });

  // 3. Manual payment check endpoint
  app.post("/orders/:orderId/check", async (req, res) => {
    const { orderId } = req.params;

    try {
      const order = await db.orders.findOne({ id: orderId });

      if (!order || !order.paymentRef) {
        return res.status(404).json({ error: "Order not found" });
      }

      const response = await fetch(
        `https://api.oneclickdz.com/v3/ocpay/checkPayment/${order.paymentRef}`,
        {
          headers: { "X-Access-Token": process.env.ONECLICK_API_KEY },
        }
      );

      const data = await response.json();

      // Update order status
      if (data.data.status === "CONFIRMED") {
        await db.orders.update(orderId, { status: "PAID" });
      } else if (data.data.status === "FAILED") {
        await db.orders.update(orderId, { status: "FAILED" });
      }

      res.json({ status: data.data.status });
    } catch (error) {
      console.error("Check error:", error);
      res.status(500).json({ error: "Check failed" });
    }
  });

  function generateOrderId() {
    return `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  app.listen(3000);
  ```

  ```python Python / Flask theme={null}
  from flask import Flask, request, redirect, render_template, jsonify
  import requests
  import os
  import time
  import random
  import string

  app = Flask(__name__)

  # 1. Create order and redirect to payment
  @app.route('/checkout', methods=['POST'])
  def checkout():
      data = request.json
      items = data['items']
      total = data['total']
      customer_id = data['customerId']

      try:
          # Create order in database
          order_id = generate_order_id()
          db.orders.create({
              'id': order_id,
              'customerId': customer_id,
              'items': items,
              'total': total,
              'status': 'PENDING'
          })

          # Create payment link
          response = requests.post(
              'https://api.oneclickdz.com/v3/ocpay/createLink',
              headers={
                  'Content-Type': 'application/json',
                  'X-Access-Token': os.getenv('ONECLICK_API_KEY')
              },
              json={
                  'productInfo': {
                      'title': f'Order {order_id}',
                      'amount': total
                  },
                  'redirectUrl': f'https://yoursite.com/orders/{order_id}'
              }
          )

          data = response.json()

          # IMPORTANT: Save paymentRef with order
          db.orders.update(order_id, {
              'paymentRef': data['data']['paymentRef']
          })

          # Redirect to payment page
          return redirect(data['data']['paymentUrl'])

      except Exception as e:
          print(f'Checkout error: {e}')
          return jsonify({'error': 'Checkout failed'}), 500

  # 2. Order status page
  @app.route('/orders/<order_id>')
  def order_status(order_id):
      try:
          order = db.orders.find_one({'id': order_id})

          if not order:
              return 'Order not found', 404

          # Check payment if still pending
          if order['status'] == 'PENDING' and order.get('paymentRef'):
              status_response = requests.get(
                  f"https://api.oneclickdz.com/v3/ocpay/checkPayment/{order['paymentRef']}",
                  headers={'X-Access-Token': os.getenv('ONECLICK_API_KEY')}
              )

              status_data = status_response.json()

              if status_data['data']['status'] == 'CONFIRMED':
                  db.orders.update(order_id, {'status': 'PAID'})
                  order['status'] = 'PAID'
              elif status_data['data']['status'] == 'FAILED':
                  db.orders.update(order_id, {'status': 'FAILED'})
                  order['status'] = 'FAILED'

          return render_template('order_status.html', order=order)

      except Exception as e:
          print(f'Error: {e}')
          return 'Error loading order', 500

  # 3. Manual payment check endpoint
  @app.route('/orders/<order_id>/check', methods=['POST'])
  def check_order(order_id):
      try:
          order = db.orders.find_one({'id': order_id})

          if not order or not order.get('paymentRef'):
              return jsonify({'error': 'Order not found'}), 404

          response = requests.get(
              f"https://api.oneclickdz.com/v3/ocpay/checkPayment/{order['paymentRef']}",
              headers={'X-Access-Token': os.getenv('ONECLICK_API_KEY')}
          )

          data = response.json()

          # Update order status
          if data['data']['status'] == 'CONFIRMED':
              db.orders.update(order_id, {'status': 'PAID'})
          elif data['data']['status'] == 'FAILED':
              db.orders.update(order_id, {'status': 'FAILED'})

          return jsonify({'status': data['data']['status']})

      except Exception as e:
          print(f'Check error: {e}')
          return jsonify({'error': 'Check failed'}), 500

  def generate_order_id():
      timestamp = int(time.time())
      random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=9))
      return f'ORD-{timestamp}-{random_str}'

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```php PHP theme={null}
  <?php
  // checkout.php
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      $data = json_decode(file_get_contents('php://input'), true);
      $items = $data['items'];
      $total = $data['total'];
      $customerId = $data['customerId'];

      try {
          // Create order in database
          $orderId = generateOrderId();
          $db->orders->create([
              'id' => $orderId,
              'customerId' => $customerId,
              'items' => $items,
              'total' => $total,
              'status' => 'PENDING'
          ]);

          // Create payment link
          $ch = curl_init('https://api.oneclickdz.com/v3/ocpay/createLink');
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_POST, true);
          curl_setopt($ch, CURLOPT_HTTPHEADER, [
              'Content-Type: application/json',
              'X-Access-Token: ' . getenv('ONECLICK_API_KEY')
          ]);
          curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
              'productInfo' => [
                  'title' => "Order $orderId",
                  'amount' => $total
              ],
              'redirectUrl' => "https://yoursite.com/orders.php?id=$orderId"
          ]));

          $response = curl_exec($ch);
          $data = json_decode($response, true);

          // IMPORTANT: Save paymentRef with order
          $db->orders->update($orderId, [
              'paymentRef' => $data['data']['paymentRef']
          ]);

          // Redirect to payment page
          header('Location: ' . $data['data']['paymentUrl']);
          exit;

      } catch (Exception $e) {
          error_log('Checkout error: ' . $e->getMessage());
          http_response_code(500);
          echo json_encode(['error' => 'Checkout failed']);
      }
  }

  // order-status.php
  $orderId = $_GET['id'] ?? null;

  if (!$orderId) {
      http_response_code(404);
      echo 'Order not found';
      exit;
  }

  try {
      $order = $db->orders->findOne(['id' => $orderId]);

      if (!$order) {
          http_response_code(404);
          echo 'Order not found';
          exit;
      }

      // Check payment if still pending
      if ($order['status'] === 'PENDING' && !empty($order['paymentRef'])) {
          $ch = curl_init(
              "https://api.oneclickdz.com/v3/ocpay/checkPayment/{$order['paymentRef']}"
          );
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_HTTPHEADER, [
              'X-Access-Token: ' . getenv('ONECLICK_API_KEY')
          ]);

          $response = curl_exec($ch);
          $statusData = json_decode($response, true);

          if ($statusData['data']['status'] === 'CONFIRMED') {
              $db->orders->update($orderId, ['status' => 'PAID']);
              $order['status'] = 'PAID';
          } elseif ($statusData['data']['status'] === 'FAILED') {
              $db->orders->update($orderId, ['status' => 'FAILED']);
              $order['status'] = 'FAILED';
          }
      }

      // Display order status page
      include 'templates/order_status.php';

  } catch (Exception $e) {
      error_log('Error: ' . $e->getMessage());
      http_response_code(500);
      echo 'Error loading order';
  }

  // check-payment.php
  if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      $orderId = $_GET['id'] ?? null;

      try {
          $order = $db->orders->findOne(['id' => $orderId]);

          if (!$order || empty($order['paymentRef'])) {
              http_response_code(404);
              echo json_encode(['error' => 'Order not found']);
              exit;
          }

          $ch = curl_init(
              "https://api.oneclickdz.com/v3/ocpay/checkPayment/{$order['paymentRef']}"
          );
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_HTTPHEADER, [
              'X-Access-Token: ' . getenv('ONECLICK_API_KEY')
          ]);

          $response = curl_exec($ch);
          $data = json_decode($response, true);

          // Update order status
          if ($data['data']['status'] === 'CONFIRMED') {
              $db->orders->update($orderId, ['status' => 'PAID']);
          } elseif ($data['data']['status'] === 'FAILED') {
              $db->orders->update($orderId, ['status' => 'FAILED']);
          }

          echo json_encode(['status' => $data['data']['status']]);

      } catch (Exception $e) {
          error_log('Check error: ' . $e->getMessage());
          http_response_code(500);
          echo json_encode(['error' => 'Check failed']);
      }
  }

  function generateOrderId() {
      $timestamp = time();
      $random = bin2hex(random_bytes(5));
      return "ORD-{$timestamp}-{$random}";
  }
  ?>
  ```
</CodeGroup>

## Frontend: Order Status Page

Simple HTML page with manual check button:

```html theme={null}
<!DOCTYPE html>
<html>
  <head>
    <title>Order Status</title>
    <style>
      .status-pending {
        color: #f59e0b;
      }
      .status-paid {
        color: #10b981;
      }
      .status-failed {
        color: #ef4444;
      }
    </style>
  </head>
  <body>
    <h1>Order #<span id="orderId"></span></h1>
    <h2>Status: <span id="status" class="status-pending">PENDING</span></h2>

    <button id="checkBtn" onclick="checkPayment()">Check Payment Status</button>

    <p id="message"></p>

    <script>
      const orderId = window.location.pathname.split("/").pop();
      document.getElementById("orderId").textContent = orderId;

      async function checkPayment() {
        const btn = document.getElementById("checkBtn");
        const msg = document.getElementById("message");

        btn.disabled = true;
        msg.textContent = "Checking...";

        try {
          const response = await fetch(`/orders/${orderId}/check`, {
            method: "POST",
          });

          const data = await response.json();

          const statusEl = document.getElementById("status");
          statusEl.textContent = data.status;
          statusEl.className = `status-${data.status.toLowerCase()}`;

          if (data.status === "CONFIRMED") {
            msg.textContent = "✅ Payment confirmed! Refreshing...";
            setTimeout(() => location.reload(), 2000);
          } else if (data.status === "FAILED") {
            msg.textContent = "❌ Payment failed";
          } else {
            msg.textContent = "⏳ Payment still pending";
          }
        } catch (error) {
          msg.textContent = "Error checking payment";
        } finally {
          btn.disabled = false;
        }
      }

      // Auto-check when page loads
      if (document.getElementById("status").textContent === "PENDING") {
        checkPayment();
      }
    </script>
  </body>
</html>
```

## Database Schema

Simple schema for orders:

```sql theme={null}
CREATE TABLE orders (
  id VARCHAR(50) PRIMARY KEY,
  customer_id VARCHAR(50) NOT NULL,
  total INTEGER NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  payment_ref VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_payment_ref (payment_ref),
  INDEX idx_status (status)
);
```

## Key Points

<Warning>
  **Important:** - Always save `paymentRef` with your order - Check payment
  status when customer returns - Only fulfill orders with status `CONFIRMED` -
  Payment links expire after 20 minutes
</Warning>

## Next Steps

<Card title="Continue to Status Polling" icon="arrow-right" href="/en/ocpay-guides/3-status-polling">
  Learn how to automate status checking with cron jobs
</Card>
