ChaosPay Gateway API
Terima pembayaran QRIS dengan REST API JSON yang ditandatangani HMAC-SHA256. Bentuk request, respons, dan webhook-nya mengikuti integrasi gateway yang umum dipakai, jadi merchant yang sudah terintegrasi cukup mengganti base URL dan kredensial.
Base URL
https://api.labubu4d.lol
Semua request lewat HTTPS dengan Content-Type: application/json. Nominal berupa bilangan bulat Rupiah (IDR). Waktu dalam Asia/Jakarta (UTC+7).
Kredensial ada di dashboard merchant, menu 2FA & API: API Key dan Secret Key. Secret Key tidak pernah dikirim dalam request; ia hanya dipakai untuk menandatangani request dan memverifikasi callback.
Pindah dari integrasi lain
Yang perlu diganti hanya tiga hal:
- Base URL menjadi
https://api.labubu4d.lol. - API Key dan Secret Key menjadi milik akun ChaosPay Anda. Header
X-API-Keyberisi API Key, dan Secret Key dipakai untuk HMAC. - Callback URL diisi di dashboard, menu Callback Settings.
Hal yang perlu diketahui:
- Nominal pembayaran dibatasi Rp 10.000 sampai Rp 10.000.000 per transaksi (default; bisa berbeda per akun).
callback_urlpada request create diterima tetapi diabaikan: callback selalu dikirim ke URL yang tersimpan di dashboard.- Biaya (fee) ditanggung merchant (
fee_borne_by: "merchant"), jadiamount_paidsama dengangross_amount. - Status
cancelada di daftar nilai tetapi tidak pernah dipakai.
Rate Limit
300 request per menit untuk setiap API Key. Jika terlampaui, API menjawab HTTP 429 dengan header Retry-After (detik). Sebaiknya andalkan callback, bukan polling.
Header & Signature
Semua endpoint kecuali status publik dan health check wajib ditandatangani.
| Header | Wajib | Keterangan |
|---|---|---|
X-API-Key | Ya | API Key Anda (atau API Key sandbox). |
X-Timestamp | Ya | Unix time dalam detik. Harus dalam rentang 5 menit dari jam server. |
X-Nonce | POST/PUT/PATCH/DELETE | String acak unik (maks. 128 karakter). Tiap nonce hanya diterima satu kali. |
X-Signature | Ya | HMAC-SHA256 dalam hex huruf kecil. |
GET:
X-Signature = hex(HMAC-SHA256(apiKey + timestamp + body, secretKey))
POST / PUT / PATCH / DELETE:
X-Signature = hex(HMAC-SHA256(apiKey + timestamp + nonce + body, secretKey))
bodyadalah isi request persis seperti yang dikirim, byte per byte. UntukGETbody kosong, jadi string yang ditandatangani hanyaapiKey + timestamp; query string tidak ikut ditandatangani.- Kirim string yang sama dengan yang Anda hash. Jangan biarkan HTTP client men-serialisasi ulang JSON-nya.
- Secret Key tidak boleh ada di kode sisi klien (browser atau aplikasi mobile).
<?php
$baseUrl = 'https://api.labubu4d.lol';
$apiKey = 'API_KEY_ANDA';
$secretKey = 'SECRET_KEY_ANDA';
function gatewayRequest(string $method, string $path, ?array $payload = null): array
{
global $baseUrl, $apiKey, $secretKey;
$body = $payload === null ? '' : json_encode($payload);
$timestamp = (string) time();
$headers = ['X-API-Key: '.$apiKey, 'X-Timestamp: '.$timestamp, 'Content-Type: application/json'];
if ($method === 'GET') {
$toSign = $apiKey.$timestamp.$body;
} else {
$nonce = bin2hex(random_bytes(16));
$toSign = $apiKey.$timestamp.$nonce.$body;
$headers[] = 'X-Nonce: '.$nonce;
}
$headers[] = 'X-Signature: '.hash_hmac('sha256', $toSign, $secretKey);
$ch = curl_init($baseUrl.$path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
if ($method !== 'GET') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
return json_decode((string) curl_exec($ch), true) ?? [];
}
$payment = gatewayRequest('POST', '/api/v1/payment/qris', [
'amount' => 50000,
'customer_id' => 'testnara',
'order_id' => 'INV-2024-001',
]);
// $payment['data']['qr_code'] untuk dirender sendiri,
// atau arahkan pelanggan ke $payment['data']['payment_url'].const crypto = require('crypto');
const baseUrl = 'https://api.labubu4d.lol';
const apiKey = 'API_KEY_ANDA';
const secretKey = 'SECRET_KEY_ANDA';
async function gatewayRequest(method, path, payload = null) {
const body = payload === null ? '' : JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000));
const headers = { 'X-API-Key': apiKey, 'X-Timestamp': timestamp, 'Content-Type': 'application/json' };
let toSign = apiKey + timestamp + body;
if (method !== 'GET') {
const nonce = crypto.randomBytes(16).toString('hex');
headers['X-Nonce'] = nonce;
toSign = apiKey + timestamp + nonce + body;
}
headers['X-Signature'] = crypto.createHmac('sha256', secretKey).update(toSign).digest('hex');
const res = await fetch(baseUrl + path, { method, headers, body: method === 'GET' ? undefined : body });
return res.json();
}
gatewayRequest('POST', '/api/v1/payment/qris', {
amount: 50000,
customer_id: 'testnara',
order_id: 'INV-2024-001',
}).then(console.log);import hashlib, hmac, json, os, time
import requests
BASE_URL = 'https://api.labubu4d.lol'
API_KEY = 'API_KEY_ANDA'
SECRET_KEY = 'SECRET_KEY_ANDA'
def gateway_request(method, path, payload=None):
body = '' if payload is None else json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time()))
headers = {'X-API-Key': API_KEY, 'X-Timestamp': timestamp, 'Content-Type': 'application/json'}
to_sign = API_KEY + timestamp + body
if method != 'GET':
nonce = os.urandom(16).hex()
headers['X-Nonce'] = nonce
to_sign = API_KEY + timestamp + nonce + body
headers['X-Signature'] = hmac.new(SECRET_KEY.encode(), to_sign.encode(), hashlib.sha256).hexdigest()
# Kirim persis string yang ditandatangani: data=body, bukan json=payload.
return requests.request(method, BASE_URL + path, headers=headers, data=body or None, timeout=30).json()
print(gateway_request('POST', '/api/v1/payment/qris', {
'amount': 50000,
'customer_id': 'testnara',
'order_id': 'INV-2024-001',
}))IP Whitelist
Di Callback Settings Anda dapat mendaftarkan IP server yang boleh memakai API Key live, dipisah dengan |. Jika kosong, semua IP diterima. IP lain ditolak dengan HTTP 403.
Sandbox
API Key dan Secret Key sandbox (menu 2FA & API) dipakai di base URL yang sama. Pembayaran sandbox tidak pernah dikirim ke bank atau provider, tidak menyentuh saldo asli, dan QR-nya tidak dapat dibayar. Hasilnya (berhasil atau gagal) ditentukan dari halaman Sandbox di dashboard, yang mengirim callback sungguhan bertanda tangan Secret Key sandbox. IP whitelist tidak berlaku untuk sandbox.
Format Respons
{
"success": true,
"message": "OK",
"data": { }
}
{
"success": false,
"error": "Invalid API credentials",
"code": 401
}
Kode Error
| HTTP | Arti |
|---|---|
| 200 | Sukses |
| 400 | Parameter tidak valid (pesan menyebut field-nya) |
| 401 | Header autentikasi kurang, signature salah, timestamp di luar rentang, nonce sudah dipakai, atau akun nonaktif |
| 403 | IP tidak ada di whitelist |
| 404 | Transaksi atau path tidak ditemukan |
| 429 | Rate limit terlampaui |
| 500 | Kesalahan internal |
| 502 / 503 | Provider pembayaran bermasalah (keduanya dikirim sebagai HTTP 503; field code menyebut yang sebenarnya). Cek status transaksi sebelum membuat ulang. |
Buat Pembayaran QRIS
/api/v1/payment/qris| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
amount | integer | Ya | Nominal dalam IDR. Min Rp 10.000, maks Rp 10.000.000. |
customer_id | string | Ya | Pengenal pelanggan (username, email, telepon, atau ID unik). Maks. 191 karakter. |
order_id | string | Tidak | Referensi order atau invoice Anda (A-Z a-z 0-9 . _ : -, maks. 64). Setiap request membuat pembayaran baru, termasuk dengan order_id yang sudah pernah dipakai. |
description | string | Tidak | Keterangan pembayaran. |
callback_url | string | Tidak | Diabaikan; lihat Pindah dari integrasi lain. |
{
"transaction_id": "TRX2610241234567",
"order_id": "INV-2024-001",
"customer_id": "testnara",
"transaction_status": "pending",
"transaction_time": "2026-10-24 10:00:00",
"payment_type": "qris",
"gross_amount": 50000,
"amount_paid": 50000,
"fee_borne_by": "merchant",
"fee_from_customer": 0,
"fee_amount": 350,
"net_amount": 49650,
"currency": "IDR",
"payment_url": "https://api.labubu4d.lol/pay/TRX2610241234567",
"payment_url_direct": "https://api.labubu4d.lol/pay/TRX2610241234567",
"qr_code": "00020101021226...",
"expiry_time": "2026-10-24 10:10:00",
"sandbox": false
}
gross_amount: nominal yang Anda minta, tidak berubah.amount_paid: yang dibayar pelanggan lewat QR.fee_amountdannet_amountpada pembayaran yang belum lunas adalah perkiraan dari tarif saat ini; angka final tercatat saat pembayaran lunas.qr_code: string QRIS untuk dirender menjadi gambar QR di aplikasi Anda.payment_url: halaman pembayaran kami; arahkan pelanggan ke sana jika tidak ingin merender sendiri.- QRIS berlaku 10 menit (
expiry_time).
Cek Status Pembayaran
/api/v1/payment/{transaction_id}{transaction_id} dapat berupa ID TRX… atau order_id Anda. Jika order_id dipakai untuk lebih dari satu pembayaran, yang dikembalikan adalah yang terbaru. Respons berisi field yang sama dengan pembuatan, ditambah settlement_time setelah lunas. Untuk pembayaran yang masih pending, kami memeriksa provider lebih dulu, sehingga callback yang terlewat tetap terlihat di sini.
Status Publik
/api/v1/payment/status?id={transaction_id}Tanpa autentikasi, untuk halaman pembayaran milik pelanggan. Hanya menerima ID TRX… dan hanya mengembalikan status dan waktu (tanpa nominal atau data akun).
{
"status": "pending",
"transaction_time": "2026-10-24 10:00:00",
"expiry_time": "2026-10-24 10:10:00",
"settlement_time": null
}
Daftar Transaksi
/api/v1/transactions| Query | Keterangan |
|---|---|
page | Nomor halaman, default 1 |
per_page (alias limit) | Item per halaman, default 20, maks. 100 |
status | settlement, pending, expire, deny, cancel |
order_id | Filter berdasarkan order Anda |
date_from, date_to | YYYY-MM-DD, berdasarkan waktu pembuatan |
Urutan terbaru lebih dulu. data.transactions berisi objek transaksi, dan data.pagination berisi page, per_page, total, total_pages. Query string tidak ikut ditandatangani.
Nilai Status
| Status | Arti |
|---|---|
pending | Menunggu pembayaran pelanggan |
settlement | Pembayaran diterima |
deny | Pembayaran ditolak atau gagal |
expire | Waktu pembayaran habis |
cancel | Dibatalkan (tidak dipakai saat ini) |
Halaman Pembayaran
/pay/{transaction_id}Halaman milik kami yang menampilkan nominal, QR, hitung mundur, dan status yang diperbarui otomatis sampai lunas. Inilah payment_url. Tidak perlu autentikasi.
Saldo
/api/v1/balance{
"available": 15000000,
"pending": 250000,
"available_formatted": "Rp 15.000.000",
"pending_formatted": "Rp 250.000"
}
available: saldo yang dapat ditarik. pending: dana yang sedang ditahan untuk penarikan yang berjalan. Kunci sandbox selalu mendapat 0.
Health Check
/api/v1/healthTanpa autentikasi. Mengembalikan {"status": "ok", "timestamp": …} di dalam data.
Callback Pembayaran
Saat sebuah pembayaran mencapai status akhir (settlement, deny, atau expire), kami mengirim POST JSON ke Callback URL Anda.
| Header | Keterangan |
|---|---|
Content-Type | application/json |
X-Callback-Signature | HMAC-SHA256 hex untuk verifikasi |
X-Callback-Timestamp | Unix time saat callback dikirim |
{
"transaction_id": "TRX2610241234567",
"order_id": "INV-2024-001",
"customer_id": "testnara",
"transaction_status": "settlement",
"transaction_time": "2026-10-24 10:00:00",
"settlement_time": "2026-10-24 10:02:11",
"payment_type": "qris",
"gross_amount": 50000,
"amount_paid": 50000,
"fee_borne_by": "merchant",
"fee_from_customer": 0,
"fee_amount": 350,
"net_amount": 49650,
"currency": "IDR"
}
Body juga memuat field tambahan format native kami (status, amount, idrpayment_id, signature, signature_v2, sandbox). Abaikan saja jika tidak dipakai. Semua field di body ikut ditandatangani.
Verifikasi Signature
- Baca body mentah dan header
X-Callback-Signature. - Decode JSON, urutkan key tingkat atas (
ksort). - Encode ulang tanpa spasi dengan
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES. - Hitung
HMAC-SHA256(hasil, secretKey)dalam hex dan bandingkan denganhash_equals. - Jika tidak sama, balas
401. Jika sama, prosestransaction_statuslalu balas200.
Secret Key yang dipakai adalah milik akun Anda (Secret Key sandbox untuk pembayaran sandbox). X-Callback-Timestamp tidak ikut ditandatangani. Cocokkan juga gross_amount dengan pesanan Anda sebelum menandai lunas.
<?php
$secretKey = 'SECRET_KEY_ANDA';
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CALLBACK_SIGNATURE'] ?? '';
$payload = json_decode($raw, true);
if (! is_array($payload)) {
http_response_code(400);
exit;
}
ksort($payload);
$expected = hash_hmac(
'sha256',
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
$secretKey
);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
if ($payload['transaction_status'] === 'settlement') {
// tandai $payload['order_id'] lunas, cocokkan gross_amount dengan pesanan Anda
}
http_response_code(200);
echo 'OK';const crypto = require('crypto');
const express = require('express');
const secretKey = 'SECRET_KEY_ANDA';
const app = express();
app.post('/callback', express.json(), (req, res) => {
const payload = req.body;
const sorted = Object.fromEntries(Object.keys(payload).sort().map((k) => [k, payload[k]]));
const expected = crypto.createHmac('sha256', secretKey).update(JSON.stringify(sorted)).digest('hex');
const given = String(req.get('X-Callback-Signature') || '');
const ok = given.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
if (payload.transaction_status === 'settlement') {
// tandai payload.order_id lunas
}
res.status(200).send('OK');
});import hashlib, hmac, json
from flask import Flask, request
SECRET_KEY = 'SECRET_KEY_ANDA'
app = Flask(__name__)
@app.post('/callback')
def callback():
payload = request.get_json(force=True)
signed = json.dumps(dict(sorted(payload.items())), separators=(',', ':'), ensure_ascii=False)
expected = hmac.new(SECRET_KEY.encode(), signed.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-Callback-Signature', '')):
return '', 401
if payload['transaction_status'] == 'settlement':
pass # tandai payload['order_id'] lunas
return 'OK', 200Pengiriman & Retry
- Endpoint Anda harus membalas HTTP
2xxdalam beberapa detik (batas waktu default 5 detik; dapat diatur per akun). - Jika tidak, kami mengulang sesuai pengaturan akun (default 3 percobaan, dengan jeda 10 detik, 1 menit, lalu 5 menit).
- Callback yang gagal permanen tampak di dashboard, menu Callbacks, dan dapat dikirim ulang dari sana.
- Callback yang sama bisa terkirim lebih dari sekali. Buat penanganannya idempoten berdasarkan
transaction_id. - URL callback harus HTTPS dan mengarah ke alamat publik; pengalihan (redirect) tidak diikuti.