Dokumentasi API
Panduan lengkap integrasi QRIS deposit, withdraw, dan webhook SyahPay ke aplikasi kamu.
https://syahpay.my.idAPersiapan
Langkah pertama yang harus kamu lakukan adalah mendaftar dan login ke dashboard. Setelah itu, dapatkan API Key kamu di menu Profil untuk mulai mengintegrasikan sistem pembayaran ke aplikasi kamu.
Autentikasi, Whitelist IP & Rate Limit
- Kirim API Key lewat header x-api-key (disarankan), header Authorization: Bearer KEY, atau parameter api_key / apikey. API Key tidak dibaca dari body.
- /api/invoice dan /api/withdraw menerima GET (query) maupun POST (JSON atau form). Kirim header Idempotency-Key agar retry tidak membuat invoice atau withdraw ganda. Lihat panduan integrasi bot.
- Whitelist IP diatur di menu Profil. Isi 1.1.1.1 untuk mengizinkan semua IP. Request dari IP lain mendapat HTTP 403.
- Batas 20 request / 10 detik per API Key. Melebihi batas mendapat HTTP 429, tunggu sebentar lalu ulangi.
- Perlindungan trafik (serangan, bot berbahaya, DDoS) ditangani oleh Cloudflare. Pantau batas request Anda agar tidak mendapat HTTP 429.
Kode status:
| Kode | Arti |
|---|---|
| 200 | Berhasil |
| 400 | Parameter tidak valid atau saldo kurang |
| 401 | API Key tidak ada atau salah |
| 403 | IP tidak ada di whitelist atau akun dinonaktifkan |
| 404 | Data tidak ditemukan |
| 429 | Terlalu banyak request |
| 5xx | Kesalahan server atau gateway, coba lagi |
- Method: GET
- URL: /api/balance
Parameter:
- apikey (string, wajib)
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/balance`, {
params: {
apikey: API_KEY,
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"username": "your_username",
"email": "[email protected]",
"balance": 150000
}
- Method: GET
- URL: /api/invoice
Parameter:
- apikey (string, wajib)
- amount (integer, wajib)
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/invoice`, {
params: {
apikey: API_KEY,
amount: 50000,
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"success": true,
"invoice_id": "64c8d9e...",
"amount": 50000,
"fee": 500,
"total": 50500,
"qris_image": "https://...",
"payment_link": "https://...",
"expired_at": "2025-01-01 12:00:00"
}
- Method: GET
- URL: /api/invoice/status
Parameter:
- apikey (string, wajib)
- invoice_id (string, wajib)
Nilai status: pending, paid, expired, cancelled.
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/invoice/status`, {
params: {
apikey: API_KEY,
invoice_id: 'YOUR_INVOICE_ID',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"invoice_id": "64c8d9e...",
"amount": 50000,
"fee": 500,
"total": 50500,
"status": "paid",
"qris_image": "https://...",
"payment_link": "https://...",
"expired_at": "2025-01-01 12:00:00",
"created_at": "2025-01-01 11:30:00"
}
- Method: POST (GET juga didukung)
- URL: /api/invoice/cancel
Parameter:
- apikey (string, wajib)
- invoice_id (string, wajib)
Catatan:
- Hanya invoice berstatus pending yang bisa dibatalkan. Invoice paid, expired, atau yang sudah cancelled ditolak dengan HTTP 400.
- Pembatalan hanya berlaku di sistem kami dan tidak menghubungi penyedia pembayaran. Pastikan pelanggan tidak membayar QR yang sudah dibatalkan.
- Setelah dibatalkan, status invoice menjadi cancelled dan halaman payment_link menampilkan transaksi dibatalkan.
- Jika callback_url diisi, event deposit.cancelled dikirim ke URL tersebut.
- Invoice yang tidak ditemukan atau bukan milik API key tersebut mengembalikan HTTP 404.
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.post(`${BASE_URL}/api/invoice/cancel`, null, {
params: {
apikey: API_KEY,
invoice_id: 'YOUR_INVOICE_ID',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"success": true,
"invoice_id": "INV1a2b3c4d5e",
"amount": 50000,
"fee": 500,
"total": 50500,
"status": "cancelled"
}
{
"success": false,
"error": "Invoice berstatus paid tidak bisa dibatalkan",
"status": "paid"
}
- Method: GET
- URL: /api/withdraw/methods
Parameter:
- apikey (string, wajib)
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/withdraw/methods`, {
params: {
apikey: API_KEY,
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"manual_methods": [
{
"name": "Dana",
"method": "dana",
"fee": 100,
"min": 10000,
"max": 1000000
}
],
"instant_methods": [
{
"name": "Dana",
"method": "dana",
"fee": 100,
"min": 10000,
"max": 1000000
}
]
}
- Method: GET
- URL: /api/withdraw/payments
Tampilkan daftar payment milik akun ini sebagai list. Pakai id sebagai payment_id saat withdraw. Payment hanya bisa ditambah atau diubah lewat halaman Profil di website.
Parameter:
- apikey (string, wajib)
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/withdraw/payments`, {
params: {
apikey: API_KEY,
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"success": true,
"total": 2,
"payments": [
{
"id": "PAYa1b2c3d4e5",
"provider": "Dana",
"account_name": "PIANTECH",
"account_number": "08123456789",
"manual": { "available": true, "fee": 1000, "min": 10000, "max": 1000000 },
"instant": { "available": true, "fee": 1000, "min": 10000, "max": 1000000 }
},
{
"id": "PAYf6e5d4c3b2",
"provider": "GoPay",
"account_name": "PIANTECH",
"account_number": "08129876543",
"manual": { "available": true, "fee": 2000, "min": 10000, "max": 1000000 },
"instant": { "available": false }
}
]
}
Withdraw hanya bisa ke payment yang sudah terdaftar dan disetujui admin lewat halaman Profil. Tujuan tidak bisa diisi manual, jadi meski API key bocor dana tidak bisa ditarik ke rekening lain.
- Method: GET
- URL: /api/withdraw
Parameter:
- apikey (string, wajib)
- amount (integer, wajib)
- payment_id (string, wajib) – ID payment dari /api/withdraw/payments
- instant (boolean, opsional, default false)
Contoh Request Manual:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/withdraw`, {
params: {
apikey: API_KEY,
amount: 50000,
payment_id: 'PAYa1b2c3d4e5',
instant: 'false',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"success": true,
"message": "Permintaan penarikan berhasil diajukan.",
"data": {
"id": "WDc4e3f2a1b2c3",
"amount": 50000,
"fee": 1000,
"payment_id": "PAYa1b2c3d4e5",
"method": "Dana",
"account_name": "PIANTECH",
"account_number": "08123456789",
"status": "pending",
"created_at": "2025-01-01T12:00:00.000Z"
}
}
Contoh Request Instan:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/withdraw`, {
params: {
apikey: API_KEY,
amount: 20000,
payment_id: 'PAYf6e5d4c3b2',
instant: 'true',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"success": true,
"message": "Permintaan withdraw instan sedang diproses.",
"data": {
"id": "WDf3a1b2c3d4e5",
"amount": 20000,
"fee": 2000,
"payment_id": "PAYf6e5d4c3b2",
"method": "GoPay",
"account_name": "PIANTECH",
"account_number": "08129876543",
"status": "pending",
"created_at": "2025-01-01T12:05:00.000Z"
}
}
- Method: GET
- URL: /api/withdraw/status
Parameter:
- apikey (string, wajib)
- id (string, wajib) – ID withdraw
Contoh Request:
const axios = require('axios');
const BASE_URL = 'BASE_URL';
const API_KEY = 'YOUR_API_KEY';
axios
.get(`${BASE_URL}/api/withdraw/status`, {
params: {
apikey: API_KEY,
id: 'WDc4e3f2a1b2c3',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.log(err.response ? err.response.data : err.message));
Contoh Response:
{
"id": "WDc4e3f2a1b2c3",
"amount": 50000,
"fee": 1000,
"method": "Dana",
"account_number": "08123456789",
"instant": false,
"status": "success",
"admin_note": null,
"created_at": "2025-01-01T12:00:00.000Z",
"completed_at": "2025-01-01T12:30:00.000Z"
}
Integrasi Bot, Backend & Mode Under Attack
Saat Mode Under Attack menyala, website tetap memeriksa pengunjung browser. Endpoint API di bawah ini tidak dikenai JavaScript Challenge, jadi bot Telegram, WhatsApp, Discord, backend Node.js, PHP, Python, Go, Java, aplikasi mobile, dan server-to-server tetap menerima JSON. Keamanan tetap dijaga API Key, whitelist IP, batas request, dan verifikasi di server.
- Endpoint bebas challenge: /api/ping, /api/balance, /api/invoice, /api/invoice/status, /api/invoice/cancel, /api/withdraw, /api/withdraw/methods, /api/withdraw/payments, /api/withdraw/status, /api/diagnostics.
- Pakai path persis huruf kecil tanpa slash di akhir, dengan method GET atau POST, dan kirim API Key lewat header x-api-key, Authorization: Bearer, atau query api_key. Key di body saja akan terkena challenge.
- Selalu kirim header User-Agent yang jelas (nama aplikasi kamu) dan Accept: application/json.
- Cek koneksi tanpa key: GET /api/ping harus membalas JSON {"success":true}. Jika kamu menerima HTML, berarti request tidak sampai ke API (hubungi admin).
- Cek key dan IP: GET /api/diagnostics dengan API Key menampilkan IP yang terbaca server dan status whitelist.
Error selalu JSON dengan field success, code, dan error. Jika retryable: true (HTTP 502, 503, 5xx), ulangi dengan jeda bertahap dan Idempotency-Key yang sama. Untuk HTTP 429, tunggu sesuai header Retry-After.
Node.js (axios) - buat invoice dengan retry aman:
const axios = require('axios');
const crypto = require('crypto');
const BASE_URL = 'BASE_URL';
async function createInvoice(amount) {
const idem = crypto.randomUUID(); // pakai ulang saat retry
for (let i = 0; i < 3; i++) {
try {
const { data } = await axios.post(`${BASE_URL}/api/invoice`, { amount }, {
headers: { 'x-api-key': process.env.SYAHPAY_KEY, 'Idempotency-Key': idem,
'User-Agent': 'my-bot/1.0', Accept: 'application/json' },
timeout: 15000
});
return data; // data.invoice_id, data.qris_image, data.payment_link
} catch (e) {
const d = e.response && e.response.data;
if (d && d.retryable === false) throw e;
if (e.response && e.response.status < 500 && e.response.status !== 429) throw e;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
throw new Error('Gagal membuat invoice setelah 3 percobaan');
}Fetch API (Node 18+, Bun, Deno, Cloudflare Workers):
const res = await fetch(`${BASE_URL}/api/invoice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': KEY,
'Idempotency-Key': crypto.randomUUID(), 'User-Agent': 'my-bot/1.0' },
body: JSON.stringify({ amount: 10000 })
});
const data = await res.json();Python (requests):
import requests, uuid
r = requests.post(BASE_URL + "/api/invoice", json={"amount": 10000}, timeout=15,
headers={"x-api-key": KEY, "Idempotency-Key": str(uuid.uuid4()), "User-Agent": "my-bot/1.0"})
print(r.status_code, r.json())PHP (cURL):
$ch = curl_init($baseUrl . '/api/invoice');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15,
CURLOPT_POSTFIELDS => json_encode(['amount' => 10000]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'x-api-key: ' . $key,
'Idempotency-Key: ' . bin2hex(random_bytes(16)), 'User-Agent: my-bot/1.0'],
]);
$data = json_decode(curl_exec($ch), true);Go:
req, _ := http.NewRequest("POST", baseURL+"/api/invoice", strings.NewReader(`{"amount":10000}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", key)
req.Header.Set("Idempotency-Key", uuid.NewString())
req.Header.Set("User-Agent", "my-bot/1.0")
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)Java (11+):
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/api/invoice"))
.timeout(Duration.ofSeconds(15))
.header("Content-Type", "application/json").header("x-api-key", key)
.header("Idempotency-Key", UUID.randomUUID().toString()).header("User-Agent", "my-bot/1.0")
.POST(HttpRequest.BodyPublishers.ofString("{\"amount\":10000}")).build();
HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());cURL (cek koneksi lalu buat invoice, cocok untuk bot Telegram / WhatsApp / Discord):
curl -s "$BASE_URL/api/ping"
curl -s -X POST "$BASE_URL/api/invoice" \
-H "x-api-key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{"amount":10000}'Webhook & Verifikasi Tanda Tangan
Kami mengirim POST JSON ke Webhook URL di Profil untuk event deposit.paid, deposit.cancelled, balance.released, withdraw.success, dan withdraw.rejected. Jika server kamu tidak membalas 2xx, kami mencoba ulang hingga 3 kali.
{
"event": "deposit.paid",
"data": {
"invoice_id": "...",
"amount": 10000,
"fee": 0,
"total": 10123,
"status": "paid",
"balance_after": 50000
},
"timestamp": "2026-01-01T10:00:00.000Z"
}Verifikasi header X-Webhook-Signature (HMAC-SHA256 dari body mentah memakai Webhook Secret di Profil):
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody).digest('hex');
if (sig !== req.headers['x-webhook-signature']) return res.sendStatus(401);Disarankan: verifikasi X-Webhook-Signature-V2 (berformat t=TIMESTAMP,v1=HEX, HMAC-SHA256 dari TIMESTAMP + "." + body mentah) dan tolak timestamp yang lebih tua dari 5 menit agar tahan replay. Header X-Webhook-Id unik per pengiriman, gunakan untuk mencegah pemrosesan ganda. Selalu bandingkan dengan perbandingan waktu-konstan:
const [t, v1] = req.headers['x-webhook-signature-v2'].split(',').map(p => p.split('=')[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401);
const exp = crypto.createHmac('sha256', WEBHOOK_SECRET).update(t + '.' + rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(v1))) return res.sendStatus(401);
res.sendStatus(200); // balas cepat, proses di backgroundPenting: jika website penerima webhook memakai Cloudflare Under Attack, request webhook dari kami akan terkena challenge dan gagal. Minta pemilik domain mengecualikan path webhook tersebut (Configuration Rule) atau gunakan domain/subdomain terpisah untuk webhook.