Skip to main content
As a merchant, you initiate payments by creating signed orders. How you deliver that order to a payment service depends on your integration scenario.

Overview

There are three ways to initiate a payment:
MethodScenarioYou Provide
Online CheckoutE-commerce, web appsRedirect to gateway
Merchant QR CodePOS, retail, kiosksQR code for customer to scan
Inventory DisplayVending machines, catalogsProduct list for browsing
In all cases, payment is confirmed via the transfer webhook.

Online Checkout

For web-based checkout, redirect customers to a merchant gateway’s hosted payment page.

Implementation

  1. Create and sign the order with your private key
  2. POST to the gateway’s /orders/checkout endpoint
  3. Redirect the customer to the returned redirect_url
  4. Receive payment proof via your /transfer/webhook endpoint
  5. Fulfill the order once you have verified the proof
// 1. Create the order
const order = {
  id: generateOrderId(),
  ocid: YOUR_OCID,
  amount: cart.total,
  currency: "USD",
  items: cart.items.map(item => ({
    id: item.sku,
    name: item.name,
    quantity: item.qty,
    price: item.price
  })),
  createdAt: Math.floor(Date.now() / 1000),
  expiresAt: Math.floor(Date.now() / 1000) + 3600,
  accepts: [100, 101, 102]  // Settlement providers you accept
};

// 2. Sign the order
const signature = signOrder(order, YOUR_PRIVATE_KEY);

// 3. Submit to merchant gateway
const response = await fetch(`${GATEWAY_ENDPOINT}/orders/checkout`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    ...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
  },
  body: JSON.stringify({
    order,
    signature,
    urls: {
      order: [`https://yourstore.com/orders/${order.id}`],
      completed: `https://yourstore.com/checkout/success?order=${order.id}`,
      cancelled: `https://yourstore.com/checkout/cancelled?order=${order.id}`
    }
  })
});

const { redirect_url } = await response.json();

// 4. Redirect customer
res.redirect(redirect_url);
Never rely on the customer returning to your site as payment confirmation. Always wait for the webhook proof before fulfilling orders.

Merchant QR Code

For in-person payments, display a QR code that payment apps scan. This works for retail POS, market stalls, or any face-to-face transaction.

QR Code Format

Create a QR code containing this JSON:
{
  "ocid": 500,
  "order": "https://api.yourstore.com/opencharge/orders/ord_abc123",
  "expiresAt": 1706400000
}
The order URL points to where the payment app can fetch your signed order.

Implementation

// 1. Create the order and store it
const order = {
  id: `ord_${crypto.randomUUID()}`,
  ocid: YOUR_OCID,
  reference: "POS-001",
  amount: "25.00",
  currency: "USD",
  items: posItems,
  createdAt: Math.floor(Date.now() / 1000),
  expiresAt: Math.floor(Date.now() / 1000) + 300,  // 5 minutes
  accepts: [100, 101, 102]
};

const signature = signOrder(order, YOUR_PRIVATE_KEY);
await db.storeOrder(order.id, { order, signature });

// 2. Generate QR code payload
const qrPayload = {
  ocid: YOUR_OCID,
  order: `https://api.yourstore.com/opencharge/orders/${order.id}`,
  expiresAt: order.expiresAt
};

// 3. Display QR code on POS screen
displayQRCode(JSON.stringify(qrPayload));

// 4. Wait for webhook confirmation
await waitForPayment(order.id);

Serving the Order

When a payment app fetches your order URL, return the signed order:
app.get('/orders/:orderId', async (req, res) => {
  const stored = await db.getOrder(req.params.orderId);

  if (!stored) {
    return res.status(404).json({
      error: { code: 'ORDER_NOT_FOUND', message: 'Order not found' }
    });
  }

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

  res.json({
    order: stored.order,
    signature: stored.signature,
    urls: {
      order: [`https://api.yourstore.com/opencharge/orders/${stored.order.id}`],
      completed: `https://yourstore.com/orders/${stored.order.id}/success`,
      cancelled: `https://yourstore.com/orders/${stored.order.id}/cancelled`
    }
  });
});

Scanning Customer QR Codes

Alternatively, if you have a barcode scanner (common in retail), you can scan a QR code displayed by the customer’s payment app. When you scan the customer’s QR, you receive a session URL:
{
  "ocid": 200,
  "order": "https://api.paymentapp.com/opencharge/orders/create/sess_xyz789",
  "expiresAt": 1706400000
}

Implementation

// 1. Parse scanned QR code
const qrData = JSON.parse(scannedQRContent);

// 2. Verify the payment gateway
const gatewayMetadata = await fetch(`${getEndpoint(qrData.ocid)}/metadata.json`);
if (!isAcceptedGateway(qrData.ocid)) {
  throw new Error('Payment gateway not accepted');
}

// 3. Check expiration
if (qrData.expiresAt < Date.now() / 1000) {
  throw new Error('QR code expired');
}

// 4. Create and sign the order
const order = {
  id: `ord_${crypto.randomUUID()}`,
  ocid: YOUR_OCID,
  amount: cart.total,
  currency: "USD",
  items: cart.items,
  createdAt: Math.floor(Date.now() / 1000),
  expiresAt: Math.floor(Date.now() / 1000) + 300,
  accepts: [100, 101, 102]
};

const signature = signOrder(order, YOUR_PRIVATE_KEY);

// 5. POST to the payment app's session URL
const response = await fetch(qrData.order, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    ...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
  },
  body: JSON.stringify({
    order,
    signature,
    urls: {
      order: [`https://api.yourstore.com/opencharge/orders/${order.id}`],
      completed: `https://yourstore.com/orders/${order.id}/success`,
      cancelled: `https://yourstore.com/orders/${order.id}/cancelled`
    }
  })
});

const { urls } = await response.json();

// 6. Optionally poll status URL while waiting for webhook
pollStatus(urls.status);

Inventory Display

For vending machines, kiosks, or any scenario where customers browse your catalog, expose your inventory and let payment apps create orders.

Static QR Code

Display a permanent QR code for your inventory:
{
  "ocid": 500,
  "inventory": "https://api.yourstore.com/opencharge/inventory",
  "expiresAt": null
}
expiresAt: null indicates this is a permanent QR code.

Flow

  1. Customer scans your inventory QR code
  2. Payment app fetches your /inventory endpoint
  3. Customer selects items in their app
  4. Payment app calls your /orders/create endpoint
  5. You sign and return the order
  6. Payment app processes payment
  7. You receive webhook confirmation
See Inventory endpoint and Create Order endpoint for implementation details.

Receiving Payment Confirmation

All payment flows end with a signed proof delivered to your /transfer/webhook endpoint.
app.post('/transfer/webhook', async (req, res) => {
  const { proof, signature } = req.body;

  // 1. Verify issuer is trusted
  if (!acceptedIssuers.includes(proof.issuer)) {
    return res.status(400).json({
      error: { code: 'ISSUER_NOT_ACCEPTED', message: 'Unknown issuer' }
    });
  }

  // 2. Verify signature
  const publicKey = await getPublicKey(proof.issuer);
  if (!verifySignature(proof, signature, publicKey)) {
    return res.status(400).json({
      error: { code: 'PROOF_SIGNATURE_INVALID', message: 'Invalid signature' }
    });
  }

  // 3. Verify recipient is you
  if (proof.to.ocid !== YOUR_OCID) {
    return res.status(400).json({
      error: { code: 'INVALID_PROOF', message: 'Wrong recipient' }
    });
  }

  // 4. Match to order and verify amount
  const order = await db.getOrderByReference(proof.to.reference);
  if (!order) {
    return res.status(400).json({
      error: { code: 'ORDER_NOT_FOUND', message: 'Unknown order reference' }
    });
  }

  if (proof.amount !== order.amount || proof.currency !== order.currency) {
    return res.status(400).json({
      error: { code: 'AMOUNT_MISMATCH', message: 'Payment amount mismatch' }
    });
  }

  // 5. Mark order as paid and fulfill
  await db.markOrderPaid(order.id, proof.txid, proof.timestamp);
  await fulfillOrder(order);

  res.json({ status: 'accepted', txid: proof.txid });
});
See Transfer Webhook for complete implementation guidance.

Merchant Gateways you accept.

The accepts field in your order specifies which merchant gateways [OCIDs] you accept payments from. A payment is only valid if the proof issuer is in your accepts list.
  1. To get the gateway OCID, register with the merchant gateway and add their OCID to your accepts lists.
  2. Only add OCIDs of gateways you have an account / service with.
  3. You can customize the merchant gateways in your accepts list for each order.