REST API v1

Dokumentasi API

Panduan lengkap integrasi QRIS deposit, withdraw, dan webhook SyahPay ke aplikasi kamu.

BASE URLhttps://syahpay.my.id
20 / 10s
Batas request per API Key
JSON
Format respon
HMAC
Webhook bertanda tangan

APersiapan

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:

KodeArti
200Berhasil
400Parameter tidak valid atau saldo kurang
401API Key tidak ada atau salah
403IP tidak ada di whitelist atau akun dinonaktifkan
404Data tidak ditemukan
429Terlalu banyak request
5xxKesalahan 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:

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:

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:

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:

Sukses
{
  "success": true,
  "invoice_id": "INV1a2b3c4d5e",
  "amount": 50000,
  "fee": 500,
  "total": 50500,
  "status": "cancelled"
}
Gagalstatus bukan pending
{
  "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:

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:

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:

Suksesmanual
{
  "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:

Suksesinstant
{
  "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:

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 background

Penting: 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.