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:
| Method | Scenario | You Provide |
|---|
| Online Checkout | E-commerce, web apps | Redirect to gateway |
| Merchant QR Code | POS, retail, kiosks | QR code for customer to scan |
| Inventory Display | Vending machines, catalogs | Product 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
- Create and sign the order with your private key
- POST to the gateway’s
/orders/checkout endpoint
- Redirect the customer to the returned
redirect_url
- Receive payment proof via your
/transfer/webhook endpoint
- 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.
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
- Customer scans your inventory QR code
- Payment app fetches your
/inventory endpoint
- Customer selects items in their app
- Payment app calls your
/orders/create endpoint
- You sign and return the order
- Payment app processes payment
- 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.
- To get the gateway OCID, register with the merchant gateway and add their OCID to your accepts lists.
- Only add OCIDs of gateways you have an account / service with.
- You can customize the merchant gateways in your accepts list for each order.