Skip to main content
This endpoint receives orders when a merchant scans your user’s QR code. The sessionId identifies which user will pay.

Flow

  1. User opens your app and requests to pay
  2. You generate a sessionId and display a QR code
  3. Merchant scans the QR code
  4. Merchant POSTs their signed order to this endpoint
  5. You validate the order and prompt user for confirmation
  6. User confirms, you process payment
  7. You send proof to merchant’s webhook

Implementation

Session Management

When your mobile app user requests to display a qrcode for the merchant to scan generate secure, single-use sessions and generate a qrCode.
// This is your internal app endpoint and it is not part of api the spec.
// Lets assume you mobile app calls this endpoint to get teh QrCode.

app.post('/app/pay', authenticateUser, async (req, res) => {
  const user = req.user;

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

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

  // Build QR payload -> url points to endpoint /orders/create/:sessionId below
  const qrPayload = {
    ocid: YOUR_OCID,
    order: `https://api.yourwallet.com/opencharge/orders/create/${sessionId}`,
    expiresAt: Math.floor((Date.now() + 5 * 60 * 1000) / 1000)
  };

  res.json({
    sessionId,
    qrPayload,
    qrCode: await generateQRCode(JSON.stringify(qrPayload))
  });
});
The merchant app scans the qrCode, and posts the order and signature to the session order url you set in the qrcode.
app.post('/orders/create/:sessionId', signatureAuth, async (req, res) => {
  const { sessionId } = req.params;
  const { order, signature, urls } = req.body;
  const merchantOcid = parseInt(req.headers['x-oc-id']);

  // 1. Find session
  const session = await db.getSession(sessionId);
  if (!session) {
    return res.status(404).json({
      error: { code: 'SESSION_NOT_FOUND', message: 'Session not found' }
    });
  }

  // 2. Check expiration
  if (session.expiresAt < Date.now()) {
    return res.status(404).json({
      error: { code: 'SESSION_EXPIRED', message: 'Session has expired' }
    });
  }

  // 3. Check not already used
  if (session.status !== 'pending') {
    return res.status(410).json({
      error: { code: 'SESSION_USED', message: 'Session already used' }
    });
  }

  // 4. Verify merchant signature on order
  const merchantPublicKey = await getMerchantPublicKey(merchantOcid);
  if (!verifyOrderSignature(order, signature, merchantPublicKey)) {
    return res.status(400).json({
      error: { code: 'INVALID_SIGNATURE', message: 'Invalid order signature' }
    });
  }

  // 5. Verify order not expired
  if (order.expiresAt && order.expiresAt < Date.now() / 1000) {
    return res.status(400).json({
      error: { code: 'ORDER_EXPIRED', message: 'Order has expired' }
    });
  }

  // 6. Check we can settle
  if (!canSettle(order.accepts)) {
    return res.status(400).json({
      error: { code: 'SETTLEMENT_NOT_SUPPORTED', message: 'Cannot settle with merchant' }
    });
  }

  // 7. Invalidate session and store order
  await db.updateSession(sessionId, {
    status: 'order_received',
    order,
    signature,
    merchantOcid,
    urls
  });

  // 8. If your user needs to confirm orders, Notify your user's app to load the order. (websocket/push)
  await notifyUser(session.userId, 'payment_request', {
    merchantOcid,
    order,
    sessionId
  });

  // 9. Return status URL
  res.json({
    urls: {
      status: `https://api.yourwallet.com/orders/${order.id}/status`
    }
  });

});

Processing Payment After User Confirms

If your user needs to confirm orders, implement an internal endpoint to process confirmations.
app.post('/app/confirm/:sessionId', authenticateUser, async (req, res) => {
  const { sessionId } = req.params;
  const user = req.user;

  const session = await db.getSession(sessionId);

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

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

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

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

Settlement

After collecting payment from the user, you must settle with the merchant through a trusted gateway. See the Settlement Guide for the complete details.
async function processPayment(session, user) {
  // Debit user
  await db.debitUser(user.id, session.order.amount);

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

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

Settlement Guide

Learn about direct settlement, partner reserves, and third-party settlement