Sample Integration
A minimal Node.js + Express merchant that creates invoices, redirects to hosted checkout, and handles IPN webhooks — everything you need to integrate Oxin Pay in under 100 lines.
Payment flow
Customer clicks Pay→POST /pay (your server)→POST /v1/invoices (Oxin Pay)→Redirect to checkoutUrl→Customer pays→Webhook → POST /webhook→Verify HMAC → fulfil order
1
Install and configure
Clone the sample from examples/sample-merchant/, install deps, and set your credentials.
cd examples/sample-merchant npm install cp .env.example .env # Edit .env with your API key and webhook secret
OXIN_PAY_API_KEY=oxp_live_YOUR_KEY_HERE OXIN_PAY_BASE_URL=https://pay.oxinchain.io OXIN_PAY_WEBHOOK_SECRET=your_webhook_secret
2
Create an invoice
Call POST /v1/invoices with your Bearer key. The response contains a checkoutUrl — redirect the customer there.
POST /v1/invoices
Authorization: Bearer oxp_live_...
{
"tokenId": 2,
"amountFiat": 5.00,
"fiatCurrency": "USD",
"description": "Digital Access Pass",
"merchantReference": "order-1234",
"redirectUrl": "https://yourstore.com/thank-you",
"cancelUrl": "https://yourstore.com/cart"
}3
Verify webhook signatures
Every IPN POST carries an X-Oxinpay-Signature: sha256=<hex> header. Compute HMAC-SHA256(endpoint_secret, raw_body) and compare with constant-time equality.
Headers
X-Oxinpay-Event invoice.paid X-Oxinpay-Signature sha256=<hmac-sha256-hex> X-Oxinpay-Timestamp 1718000000 X-Oxinpay-Delivery 01J123456789ABCDEFGHJKMNPQ
Body (invoice.paid)
{
"publicId": "01J123456789ABCDEFGHJKMNPQ",
"merchantReference":"order-1718000000",
"status": "paid",
"amountFiat": "5.00",
"fiatCurrency": "USD",
"tokenId": 2,
"symbol": "USDT",
"chainCode": "BSC",
"amountCrypto": "5.012345678901234567",
"amountReceived": "5.012345678901234567",
"depositAddress": "0xabc...",
"paidAt": "2024-06-10T12:00:00.000Z"
}4
Full server code
The complete sample server — invoice creation, status polling, and webhook verification.
import 'dotenv/config';
import express from 'express';
import crypto from 'node:crypto';
const { OXIN_PAY_API_KEY, OXIN_PAY_BASE_URL, OXIN_PAY_WEBHOOK_SECRET } = process.env;
const app = express();
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
// Helper: call Oxin Pay API
async function oxinPayRequest(method, path, body) {
const res = await fetch(`${OXIN_PAY_BASE_URL}/v1${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OXIN_PAY_API_KEY}`,
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!json.ok) throw new Error(json.error?.message ?? 'API error');
return json.data;
}
// POST /pay — create invoice and redirect to hosted checkout
app.post('/pay', async (req, res) => {
const invoice = await oxinPayRequest('POST', '/invoices', {
tokenId: 2, // USDT-BSC; use /v1/invoices/currencies to list options
amountFiat: 5.00,
fiatCurrency: 'USD',
merchantReference: `order-${Date.now()}`,
redirectUrl: 'https://yourstore.com/thank-you',
cancelUrl: 'https://yourstore.com/cart',
});
res.redirect(302, invoice.checkoutUrl);
});
// GET /status/:publicId — poll invoice status
app.get('/status/:publicId', async (req, res) => {
const invoice = await oxinPayRequest('GET', `/invoices/${req.params.publicId}`);
res.json({ status: invoice.status, amountReceived: invoice.amountReceived });
});
// POST /webhook — receive and verify IPN events
app.post('/webhook', (req, res) => {
const rawBody = req.body; // Buffer
const sigHeader = req.headers['x-oxinpay-signature']; // "sha256=<hex>"
const event = req.headers['x-oxinpay-event'];
if (!verifySignature(rawBody, sigHeader)) {
return res.status(400).json({ error: 'invalid_signature' });
}
const payload = JSON.parse(rawBody.toString());
if (event === 'invoice.paid') {
// Fulfil order for payload.publicId
}
res.status(200).json({ ok: true });
});
function verifySignature(rawBody, sigHeader) {
const [algo, provided] = sigHeader?.split('=') ?? [];
if (algo !== 'sha256' || !provided) return false;
const expected = crypto
.createHmac('sha256', OXIN_PAY_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
const a = Buffer.from(provided, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.listen(3000);5
Run the integration test
Use the provided script to verify every API step against a live backend.
export OXIN_PAY_API_KEY="oxp_live_..." export OXIN_PAY_BASE_URL="https://pay.oxinchain.io" bash infra/scripts/test-live-integration.sh
Webhook events
| Event | When | Action |
|---|---|---|
| invoice.created | Invoice created by merchant | Optional: send receipt |
| invoice.paid | Full amount received on-chain | Fulfil order immediately |
| invoice.completed | Funds swept to merchant hot wallet | Optional: update accounting |
| invoice.expired | TTL elapsed with no payment | Cancel or re-create order |
| invoice.underpaid | Partial payment received | Contact customer or refund |
Security checklist
- Always verify the webhook HMAC before fulfilling an order
- Use
crypto.timingSafeEqualfor signature comparison - Process webhooks idempotently — use
X-Oxinpay-Deliveryas a dedup key - Never trust
invoice.paidif the amounts do not match your expected amount - Store your API key in an environment variable, never hard-coded
- Set an IP whitelist in the dashboard to limit API key usage to your server IPs