Callback Notification
Sistem kami akan mengirimkan notifikasi callback ke endpoint yang Anda daftarkan setiap kali terjadi perubahan status transaksi (success atau failed).
Cara Kerja Callback
Request Transaksi
Anda melakukan request transaksi ke API kami
Proses Transaksi
Sistem kami memproses transaksi
Kirim Callback
Kami mengirim notifikasi hasil transaksi ke URL callback Anda
Konfigurasi Callback URL
Sebelum dapat menerima callback, Anda perlu mengatur callback URL di dashboard akun KokinPay Anda:
- Login ke dashboard kokinpay.com
- Masuk ke menu Pengaturan API atau Koneksi
- Masukkan URL endpoint callback Anda (contoh:
https://yourdomain.com/api/callback) - Simpan pengaturan
HTTP Headers
Setiap request callback yang kami kirim akan menyertakan HTTP headers berikut:
| Header | Value | Deskripsi |
|---|---|---|
| Content-Type | application/json |
Format payload adalah JSON |
| X-Kokinpay-Event | transaction.update |
Identifier untuk jenis event callback |
Callback Payload
Sistem kami akan mengirimkan payload dalam format JSON dengan struktur sebagai berikut:
Callback - Transaksi Berhasil (Success)
{
"message": "Pesanan berhasil diproses",
"data": {
"client_trx_id": "TRX20250129001",
"kokinpay_trx_id": "KPTRX2025012900001",
"product_code": "TSEL10",
"product_name": "Telkomsel 10.000",
"price": 10500,
"target": "081234567890",
"sn": "1234567890123456",
"status": "success"
}
}
Callback - Transaksi Gagal (Failed)
{
"message": "Pesanan gagal diproses. Saldo telah dikembalikan",
"data": {
"client_trx_id": "TRX20250129001",
"kokinpay_trx_id": "KPTRX2025012900001",
"product_code": "TSEL10",
"product_name": "Telkomsel 10.000",
"price": 10500,
"target": "081234567890",
"sn": "-",
"status": "failed"
}
}
Struktur Data Callback
| Field | Type | Deskripsi |
|---|---|---|
| message | string | Pesan informasi hasil transaksi |
| client_trx_id | string | ID transaksi dari sistem client |
| kokinpay_trx_id | string | ID transaksi dari sistem KokinPay |
| product_code | string | Kode produk yang dibeli |
| product_name | string | Nama produk yang dibeli |
| price | integer | Harga produk dalam Rupiah (IDR) |
| target | string | Tujuan transaksi |
| sn | string | Serial Number / kode voucher. Berisi - jika transaksi gagal atau tidak ada SN |
| status | string | Status transaksi: success atau failed |
Response yang Diharapkan
Endpoint callback Anda wajib mengembalikan HTTP status code 200 (OK) untuk mengindikasikan bahwa callback telah diterima dengan baik.
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "received"
}
Contoh Implementasi Endpoint Callback
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class CallbackController extends Controller
{
public function handleKokinpayCallback(Request $request)
{
if ($request->header('X-Kokinpay-Event') !== 'transaction.update') {
return response()->json(['status' => 'invalid event'], 400);
}
$allowedIp = '103.147.154.187';
if ($request->ip() !== $allowedIp) {
Log::warning('Callback from unauthorized IP', ['ip' => $request->ip()]);
return response()->json(['status' => 'unauthorized'], 403);
}
$message = $request->input('message');
$data = $request->input('data');
Log::info('Callback received from KokinPay', [
'client_trx_id' => $data['client_trx_id'],
'kokinpay_trx_id' => $data['kokinpay_trx_id'],
'status' => $data['status']
]);
try {
$transaction = Transaction::where('trx_id', $data['client_trx_id'])->first();
if ($transaction) {
$transaction->update([
'status' => $data['status'],
'kokinpay_trx_id' => $data['kokinpay_trx_id'],
'sn' => $data['sn'],
'updated_at' => now()
]);
if ($data['status'] === 'success') {
$this->sendSuccessNotification($transaction);
} else {
$this->sendFailedNotification($transaction);
}
}
return response()->json([
'status' => 'received',
'client_trx_id' => $data['client_trx_id']
], 200);
} catch (\Exception $e) {
Log::error('Error processing callback', [
'error' => $e->getMessage(),
'client_trx_id' => $data['client_trx_id'] ?? null
]);
return response()->json([
'status' => 'received_with_error',
'message' => 'Callback received but processing failed'
], 200);
}
}
}
?>
const express = require('express');
const app = express();
app.use(express.json());
const validateIp = (req, res, next) => {
const allowedIp = '103.147.154.187';
const clientIp = req.ip || req.connection.remoteAddress;
if (clientIp !== allowedIp) {
console.warn(`Callback from unauthorized IP: ${clientIp}`);
return res.status(403).json({ status: 'unauthorized' });
}
next();
};
app.post('/api/callback/kokinpay', validateIp, async (req, res) => {
try {
const eventHeader = req.headers['x-kokinpay-event'];
if (eventHeader !== 'transaction.update') {
return res.status(400).json({ status: 'invalid event' });
}
const { message, data } = req.body;
console.log('Callback received:', {
client_trx_id: data.client_trx_id,
kokinpay_trx_id: data.kokinpay_trx_id,
status: data.status
});
await updateTransaction({
trxId: data.client_trx_id,
kokinpayTrxId: data.kokinpay_trx_id,
status: data.status,
sn: data.sn
});
if (data.status === 'success') {
await sendSuccessNotification(data);
} else {
await sendFailedNotification(data);
}
return res.status(200).json({
status: 'received',
client_trx_id: data.client_trx_id
});
} catch (error) {
console.error('Error processing callback:', error);
return res.status(200).json({
status: 'received_with_error',
message: 'Callback received but processing failed'
});
}
});
app.listen(3000, () => {
console.log('Callback server running on port 3000');
});
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
ALLOWED_IP = '103.147.154.187'
@app.route('/api/callback/kokinpay', methods=['POST'])
def handle_callback():
try:
client_ip = request.remote_addr
if client_ip != ALLOWED_IP:
logging.warning(f'Callback from unauthorized IP: {client_ip}')
return jsonify({'status': 'unauthorized'}), 403
event_header = request.headers.get('X-Kokinpay-Event')
if event_header != 'transaction.update':
return jsonify({'status': 'invalid event'}), 400
payload = request.get_json()
message = payload.get('message')
data = payload.get('data')
logging.info(f"Callback received: {data.get('client_trx_id')} - {data.get('status')}")
update_transaction(
trx_id=data.get('client_trx_id'),
kokinpay_trx_id=data.get('kokinpay_trx_id'),
status=data.get('status'),
sn=data.get('sn')
)
if data.get('status') == 'success':
send_success_notification(data)
else:
send_failed_notification(data)
return jsonify({
'status': 'received',
'client_trx_id': data.get('client_trx_id')
}), 200
except Exception as e:
logging.error(f'Error processing callback: {str(e)}')
return jsonify({
'status': 'received_with_error',
'message': 'Callback received but processing failed'
}), 200
if __name__ == '__main__':
app.run(port=3000)
Testing Callback
Untuk testing callback di development environment, Anda dapat menggunakan tools berikut:
- ngrok - Untuk expose localhost ke internet
- webhook.site - Untuk melihat payload callback yang dikirim
- Postman / Insomnia - Untuk simulasi callback secara manual
- Install dan jalankan ngrok:
ngrok http 3000 - Copy URL yang diberikan (contoh: https://abc123.ngrok.io)
- Daftarkan URL callback di dashboard:
https://abc123.ngrok.io/api/callback/kokinpay - Lakukan transaksi test dan pantau callback yang masuk
Best Practices
- Validasi IP: Selalu validasi bahwa request berasal dari IP 103.147.154.187
- Validasi Header: Cek header
X-Kokinpay-Eventuntuk memastikan ini callback yang valid - Idempotent: Pastikan callback yang sama tidak memproses transaksi berulang kali
- Response Cepat: Proses callback dengan cepat (max 10 detik), gunakan queue untuk proses berat
- Logging: Simpan log semua callback untuk debugging dan audit trail
- Error Handling: Tetap return 200 meskipun terjadi error internal
- Security: Gunakan HTTPS untuk endpoint callback Anda
- Monitoring: Setup monitoring untuk memastikan callback endpoint selalu up
Troubleshooting
| Masalah | Solusi |
|---|---|
| Callback tidak pernah diterima |
|
| Callback diterima berkali-kali |
|
| Data callback tidak sesuai |
|