Skip to main content
This guide covers the different ways users can pay merchants through your payment gateway.

Overview

Your payment gateway can collect payments in several ways:
  1. Hosted checkout - Users redirected to your checkout page from Merchant Gateways
  2. Display QR code - User shows QR, merchant scans and submits order
  3. Scan merchant QR - User scans merchant QR, you fetch and process the order
  4. Scan inventory QR - User browses items, creates order, pays

Flow 1: Hosted Checkout

The most common flow for e-commerce. Merchant Gateways redirect end users to your hosted checkout page to complete payment.

Step 1: Receive Order from Merchant Gateway

// Merchant Gateway forwards an order to your checkout
app.post('/checkout/create', signatureAuth, async (req, res) => {
  const { order, signature, urls, merchantOcid } = req.body;
  const merchantGatewayOcid = req.headers['x-oc-id'];

  // Verify the order signature matches the merchant
  const merchant = await fetchMerchantMetadata(merchantOcid);
  if (!verifyOrderSignature(order, signature, merchant.config.publicKey)) {
    return res.status(400).json({
      error: { code: 'INVALID_SIGNATURE', message: 'Order signature invalid' }
    });
  }

  // Create checkout session
  const session = await db.createCheckoutSession({
    id: crypto.randomUUID(),
    merchantOcid,
    merchantGatewayOcid,
    order,
    signature,
    urls,
    status: 'pending',
    expiresAt: Date.now() + 30 * 60 * 1000
  });

  // Return checkout URL
  res.json({
    checkout_url: `https://checkout.yourgateway.com/${session.id}`
  });
});

Step 2: Display Checkout Page

Render your payment UI with the order details:
app.get('/checkout/:sessionId', async (req, res) => {
  const session = await db.getCheckoutSession(req.params.sessionId);

  if (!session || session.status !== 'pending') {
    return res.redirect('/error?code=invalid_session');
  }

  if (session.expiresAt < Date.now()) {
    return res.redirect(session.urls?.cancel || '/error?code=expired');
  }

  // Get merchant info for display
  const merchant = await fetchMerchantMetadata(session.merchantOcid);

  res.render('checkout', {
    session,
    merchant: {
      name: merchant.business?.name,
      logo: merchant.business?.logo
    },
    paymentMethods: await getAvailablePaymentMethods(session)
  });
});

Step 3: Process Payment

When user submits payment:
app.post('/checkout/:sessionId/pay', async (req, res) => {
  const { paymentMethod, paymentDetails } = req.body;
  const session = await db.getCheckoutSession(req.params.sessionId);

  // Validate session
  if (!session || session.status !== 'pending') {
    return res.status(400).json({ error: 'Invalid session' });
  }

  // Process payment based on method (card, bank, wallet balance, etc.)
  const paymentResult = await processPayment(paymentMethod, paymentDetails, session);

  if (!paymentResult.success) {
    return res.status(400).json({ error: paymentResult.error });
  }

  // Create proof
  const proof = {
    txid: generateTxid(),
    issuer: YOUR_OCID,
    from: { ocid: YOUR_OCID, reference: paymentResult.reference },
    to: { ocid: session.merchantOcid, reference: session.order.id },
    amount: session.order.amount,
    currency: session.order.currency,
    timestamp: Math.floor(Date.now() / 1000)
  };

  const signature = signProof(proof);

  // Notify Merchant Gateway
  await notifyMerchantGateway(session.merchantGatewayOcid, proof, signature);

  // Update session
  await db.updateCheckoutSession(session.id, {
    status: 'completed',
    proof,
    signature
  });

  // Redirect to success URL
  res.json({
    success: true,
    redirect: session.urls?.success || '/success'
  });
});

Step 4: Notify Merchant Gateway

Send payment proof to the Merchant Gateway:
async function notifyMerchantGateway(merchantGatewayOcid, proof, signature) {
  const gateway = await fetchMetadata(merchantGatewayOcid);
  const webhookUrl = `${gateway.config.endpoint}/transfer/webhook`;

  await fetch(webhookUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
    },
    body: JSON.stringify({ proof, signature })
  });
}

Flow 2: User Displays QR Code

Best for retail where merchants have scanners/cameras.

Step 1: Generate Session

When user wants to pay, create a session and QR code:
app.post('/app/pay', authenticateUser, async (req, res) => {
  const user = req.user;

  const sessionId = crypto.randomBytes(16).toString('hex');

  await db.createSession({
    id: sessionId,
    userId: user.id,
    status: 'pending',
    expiresAt: Date.now() + 5 * 60 * 1000
  });

  const qrPayload = {
    ocid: YOUR_OCID,
    order: `${YOUR_ENDPOINT}/orders/create/${sessionId}`,
    expiresAt: Math.floor((Date.now() + 5 * 60 * 1000) / 1000)
  };

  res.json({
    sessionId,
    qrPayload: JSON.stringify(qrPayload),
    qrCode: await generateQRCode(JSON.stringify(qrPayload))
  });
});

Step 2: Receive Order from Merchant

Merchant scans the QR and POSTs their order:
app.post('/orders/create/:sessionId', signatureAuth, async (req, res) => {
  const { sessionId } = req.params;
  const { order, signature, urls } = req.body;

  // Validate session
  const session = await validateSession(sessionId);

  // Verify merchant's order signature
  await verifyMerchantOrder(order, signature, req.headers['x-oc-id']);

  // Store order and notify user
  await db.updateSession(sessionId, {
    status: 'order_received',
    order,
    signature,
    urls
  });

  // Push notification to user's app
  await pushNotification(session.userId, {
    type: 'payment_request',
    amount: order.amount,
    currency: order.currency,
    merchant: order.ocid,
    sessionId
  });

  res.json({
    urls: { status: `${YOUR_ENDPOINT}/orders/${order.id}/status` }
  });
});

Step 3: User Confirms Payment

User reviews and confirms in your app:
app.post('/app/confirm/:sessionId', authenticateUser, async (req, res) => {
  const session = await db.getSession(req.params.sessionId);

  // Verify ownership
  if (session.userId !== req.user.id) {
    return res.status(403).json({ error: 'Not your session' });
  }

  // Check balance
  if (req.user.balance < parseFloat(session.order.amount)) {
    return res.status(402).json({ error: 'Insufficient funds' });
  }

  // Process payment
  await processPayment(session, req.user);

  res.json({ success: true });
});

Flow 3: User Scans Merchant QR

Best for kiosks, vending machines, or when merchant displays QR.

Step 1: Parse Scanned QR

app.post('/app/scan', authenticateUser, async (req, res) => {
  const { qrContent } = req.body;
  const user = req.user;

  const qrData = JSON.parse(qrContent);

  // Check expiration
  if (qrData.expiresAt && qrData.expiresAt < Date.now() / 1000) {
    return res.status(400).json({ error: 'QR code expired' });
  }

  // Determine type and fetch data
  if (qrData.order) {
    // Fetch order
    const orderData = await fetchMerchantOrder(qrData.order);
    return res.json({
      type: 'order',
      merchantOcid: qrData.ocid,
      order: orderData.order,
      signature: orderData.signature
    });
  }

  if (qrData.inventory) {
    // Fetch inventory
    const inventory = await fetchInventory(qrData.inventory);
    return res.json({
      type: 'inventory',
      merchantOcid: qrData.ocid,
      inventory
    });
  }

  res.status(400).json({ error: 'Unknown QR format' });
});

Step 2: User Confirms and Pays

app.post('/app/pay-order', authenticateUser, async (req, res) => {
  const { merchantOcid, order, signature } = req.body;
  const user = req.user;

  // 1. Verify the order signature
  const merchant = await fetchMerchantMetadata(merchantOcid);
  if (!verifyOrderSignature(order, signature, merchant.config.publicKey)) {
    return res.status(400).json({ error: 'Invalid order signature' });
  }

  // 2. Verify order not expired
  if (order.expiresAt && order.expiresAt < Date.now() / 1000) {
    return res.status(400).json({ error: 'Order expired' });
  }

  // 3. Check balance
  if (user.balance < parseFloat(order.amount)) {
    return res.status(402).json({ error: 'Insufficient funds' });
  }

  // 4. Debit user
  await db.debitUser(user.id, order.amount);

  // 5. Settle with merchant - see Settlement Guide
  const proof = await settlePayment(user, order, merchantOcid);

  res.json({ success: true, txid: proof.txid });
});

Flow 4: Inventory Browsing

For vending machines or merchants with small catalogs.

Fetch and Display Inventory

app.post('/app/browse', authenticateUser, async (req, res) => {
  const { inventoryUrl, merchantOcid } = req.body;

  const inventory = await fetch(inventoryUrl).then(r => r.json());

  res.json({
    merchantOcid,
    currency: inventory.currency,
    items: inventory.items.map(item => ({
      id: item.id,
      name: item.name,
      description: item.description,
      price: item.price,
      available: item.quantity > 0,
      image: item.images?.[0]
    }))
  });
});

Create Order and Pay

After user selects items:
app.post('/app/create-order', authenticateUser, async (req, res) => {
  const { merchantOcid, items } = req.body;
  const user = req.user;

  // 1. Get merchant endpoint
  const merchant = await fetchMerchantMetadata(merchantOcid);

  // 2. Request signed order from merchant
  const response = await fetch(`${merchant.config.endpoint}/orders/create`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
    },
    body: JSON.stringify({
      id: generateOrderId(),
      ocid: merchantOcid,
      items,
      currency: 'USD'
    })
  });

  const { order, signature } = await response.json();

  // 3. Verify signature
  if (!verifyOrderSignature(order, signature, merchant.config.publicKey)) {
    return res.status(400).json({ error: 'Invalid merchant signature' });
  }

  // 4. Check balance
  if (user.balance < parseFloat(order.amount)) {
    return res.status(402).json({ error: 'Insufficient funds' });
  }

  // 5. Store pending order for confirmation
  await db.createPendingOrder({
    userId: user.id,
    merchantOcid,
    order,
    signature
  });

  res.json({
    order,
    total: order.amount,
    currency: order.currency
  });
});

Settlement

After collecting payment from the user through any of the flows above, you must settle with the merchant. The settlement process is the same regardless of how you received the order.
async function processPayment(user, order, merchantOcid) {
  // Debit user
  await db.debitUser(user.id, order.amount);

  // Settle with merchant
  const proof = await settlePayment(user, order, merchantOcid);

  return proof;
}

Settlement Guide

Learn about the three settlement strategies: direct, partner reserve, and common third party