Table of Contents
ToggleShared conversation
Assistant ·
Implementasi Backend Minimal Menggunakan Node.js + Express untuk Midtrans dan Xendit
1. Struktur Proyek
project-root/
├── server.js (Express server)
├── package.json
└── README.md
2. Environment Variables
Untuk Midtrans:
MIDTRANS_SERVER_KEY=your_midtrans_server_keyMIDTRANS_CLIENT_KEY=your_midtrans_client_keyNODE_ENV=development
Untuk Xendit:
XENDIT_SECRET_API_KEY=your_xendit_secret_api_key
Porta server default: 3000
3. Kode Server (server.js)
javascript
const express = require('express');
const fetch = require('node-fetch');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const ORDERS = {};
function genOrderId() {
return 'ORD-' + Date.now();
}
// MIDTRANS (Snap) - Server
const MIDTRANS_SERVER_KEY = process.env.MIDTRANS_SERVER_KEY || '';
const MIDTRANS_CLIENT_KEY = process.env.MIDTRANS_CLIENT_KEY || '';
const MIDTRANS_BASE = 'https://api.sandbox.midtrans.com/v2';
function midtransAuthHeader() {
return 'Basic ' + Buffer.from(MIDTRANS_SERVER_KEY + ':').toString('base64');
}
app.post('/midtrans/create-transaction', async (req, res) => {
try {
const { name, email, ticketType, price } = req.body;
if (!name || !email || !ticketType || !price) {
return res.status(400).json({ error: 'Missing fields' });
}
const orderId = genOrderId();
ORDERS[orderId] = {
orderId, name, email, ticketType, price, status: 'pending'
};
const payload = {
transaction_details: {
order_id: orderId,
gross_amount: Number(price)
},
credit_card: {
secure: true
},
customer_details: {
first_name: name,
email: email
}
};
const resp = await fetch(`${MIDTRANS_BASE}/transactions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': midtransAuthHeader()
},
body: JSON.stringify(payload)
});
if (!resp.ok) {
const text = await resp.text();
return res.status(502).json({ error: 'Midtrans API error', detail: text });
}
const data = await resp.json();
ORDERS[orderId].midtrans = data;
return res.json({ orderId, midtrans: data, midtransClientKey: MIDTRANS_CLIENT_KEY });
} catch (err) {
res.status(500).json({ error: 'server_error' });
}
});
// Midtrans webhook
app.post('/midtrans/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const rawBody = req.body.toString('utf8');
const payload = JSON.parse(rawBody);
const { order_id, status_code, gross_amount, signature_key } = payload;
const expected = crypto.createHash('sha512').update(order_id + status_code + gross_amount + MIDTRANS_SERVER_KEY).digest('hex');
if (expected !== signature_key) {
return res.status(400).send('invalid signature');
}
const order = ORDERS[order_id];
if (!order) {
return res.status(200).send('order not found');
}
if (payload.transaction_status === 'settlement') {
order.status = 'paid';
} else {
order.status = 'failed';
}
res.status(200).send('ok');
} catch (err) {
res.status(500).send('server error');
}
});
// XENDIT - Server
const XENDIT_SECRET_API_KEY = process.env.XENDIT_SECRET_API_KEY || '';
const XENDIT_BASE = 'https://api.xendit.co';
app.post('/xendit/create-invoice', async (req, res) => {
try {
const { name, email, ticketType, price } = req.body;
if (!name || !email || !ticketType || !price) {
return res.status(400).json({ error: 'Missing fields' });
}
const orderId = genOrderId();
ORDERS[orderId] = { orderId, name, email, ticketType, price, status: 'pending' };
const body = {
external_id: orderId,
amount: Number(price),
payer_email: email,
description: `Ticket ${ticketType} - ${orderId}`
};
const resp = await fetch(`${XENDIT_BASE}/v2/invoices`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + Buffer.from(XENDIT_SECRET_API_KEY + ':').toString('base64')
},
body: JSON.stringify(body)
});
if (!resp.ok) {
const t = await resp.text();
return res.status(502).json({ error: 'Xendit API error', detail: t });
}
const data = await resp.json();
ORDERS[orderId].xendit = data;
return res.json({ orderId, invoice: data });
} catch (err) {
res.status(500).json({ error: 'server_error' });
}
});
// Xendit webhook
app.post('/xendit/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const signature = req.headers['x-callback-signature'] || '';
const payload = req.body.toString('utf8');
const expected = crypto.createHmac('sha256', XENDIT_SECRET_API_KEY).update(payload).digest('hex');
if (signature !== expected) {
return res.status(400).send('invalid signature');
}
const data = JSON.parse(payload);
if (data.type === 'invoice' && data.data && data.data.status) {
const external_id = data.data.external_id;
const order = ORDERS[external_id];
if (order && data.data.status === 'PAID') {
order.status = 'paid';
}
}
res.status(200).send('ok');
} catch (err) {
res.status(500).send('server error');
}
});
// Utility endpoints
app.get('/order/:id', (req, res) => {
const order = ORDERS[req.params.id];
if (!order) return res.status(404).json({ error: 'not found' });
res.json(order);
});
app.get('/', (req, res) => res.send('Payment demo server running'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('Server listening on', PORT));
4. package.json
json
{
"name": "payment-demo",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"body-parser": "^1.20.0",
"express": "^4.18.2",
"node-fetch": "^2.6.7"
}
}
5. Contoh Panggilan dari Front-End
javascript
async function checkoutWithMidtrans(name, email, ticketType, price) {
const resp = await fetch('/midtrans/create-transaction', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, ticketType, price })
});
const data = await resp.json();
if (data.midtrans && data.midtrans.redirect_url) {
window.location = data.midtrans.redirect_url;
}
}
async function checkoutWithXendit(name, email, ticketType, price) {
const resp = await fetch('/xendit/create-invoice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, ticketType, price })
});
const data = await resp.json();
if (data.invoice && data.invoice.invoice_url) {
window.location = data.invoice.invoice_url;
}
}
6. Testing Lokal dengan Ngrok
- Jalankan server:
npm install && npm start - Jalankan ngrok:
ngrok http 3000 - Gunakan URL ngrok sebagai callback/webhook URL di dashboard Midtrans/Xendit.
7. Generasi Tiket & QR Code (Opsional)
- Setelah status order menjadi
paid, Anda bisa menggunakan library untuk menghasilkan QR code atau PDF.
8. Keamanan & Produksi
- Jangan simpan API keys di repo.
- Gunakan HTTPS.
- Validasi semua input dan simpan transaksi di database.
9. Permintaan Tambahan
Beritahu jika Anda ingin menambahkan contoh implementasi tertentu atau jika ada bagian lain yang ingin Anda kembangkan lebih lanjut.





