Bagaimana autentikasi API SekaliPay bekerja?
Setiap permintaan membawa API key di header X-API-Key. Selebihnya bergantung mode akun: IP Whitelist (bawaan) menerima permintaan dari IP terdaftar dan hanya menandatangani POST, sedangkan Enhanced HMAC menandatangani setiap permintaan beserta X-Timestamp dan cocok untuk server tanpa IP tetap.
Isi halaman ini dicocokkan dengan kode API per 16 Oktober 2026.
Apa beda API key dan secret key?#
| Kunci | Dipakai untuk | Dikirim di permintaan? |
|---|---|---|
API key (mk_...) | Mengenali akun merchant Anda. | Ya, di header X-API-Key setiap permintaan. |
Secret key (sk_...) | Menandatangani permintaan dan memverifikasi webhook. | Tidak pernah. Hanya dipakai menghitung HMAC di server Anda. |
Secret key hanya untuk server
Secret key tampil di dasbor setelah verifikasi PIN. Simpan di variabel lingkungan atau secret manager, jangan di aplikasi seluler, JavaScript browser, atau repositori. Bila bocor, buat kunci baru di Pengaturan API, API Keys: API key dan secret key diganti bersamaan dan kunci lama langsung tidak berlaku.
Mode mana yang sebaiknya dipakai?#
| IP Whitelist (bawaan) | Enhanced HMAC | |
|---|---|---|
| Cocok untuk | Server dengan IP publik tetap: VPS, server kantor. | IP berubah-ubah: Cloudflare Workers, AWS Lambda, Vercel. |
| Yang diperiksa | IP pengirim harus terdaftar. | Tanda tangan dan X-Timestamp. IP tidak diperiksa. |
| Permintaan GET | Cukup X-API-Key. | X-API-Key, X-Signature, X-Timestamp. |
| Permintaan POST | X-API-Key dan X-Signature. | X-API-Key, X-Signature, X-Timestamp. |
| Yang ditandatangani | Body JSON mentah. | METHOD|PATH|PARAMS|TIMESTAMP. |
| Cara mengaktifkan | Isi minimal satu IP di Pengaturan API, Mode & akses. | Matikan IP whitelist di halaman yang sama. |
Mode berlaku untuk seluruh akun dan tidak bisa dicampur per permintaan. Batas permintaan sama di kedua mode.
Bagaimana menandatangani permintaan di mode IP Whitelist?#
Hitung HMAC-SHA256 atas body JSON persis seperti yang dikirim, dengan secret key sebagai kunci, lalu kirim hasil hex huruf kecilnya di X-Signature. Permintaan GET tidak perlu tanda tangan.
X-Signature = hex( HMAC-SHA256( secret_key, raw_request_body ) )$body = json_encode([
'merchant_ref_id' => 'ORDER-2026-00184',
'amount' => 150000,
'payment_code' => 'QRIS',
'customer_name' => 'Budi Santoso',
'customer_email' => '[email protected]',
'customer_phone' => '081234567890',
'callback_url' => 'https://toko-anda.id/webhook/sekalipay',
'return_url' => 'https://toko-anda.id/checkout/selesai',
]);
$signature = hash_hmac('sha256', $body, getenv('SEKALIPAY_SECRET_KEY'));
$ch = curl_init('https://sekalipay.com/api/v1/gateway/payment');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
'X-API-Key: ' . getenv('SEKALIPAY_API_KEY'),
'X-Signature: ' . $signature,
],
]);
$response = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);import { createHmac } from "node:crypto";
const body = JSON.stringify({
"merchant_ref_id": "ORDER-2026-00184",
"amount": 150000,
"payment_code": "QRIS",
"customer_name": "Budi Santoso",
"customer_email": "[email protected]",
"customer_phone": "081234567890",
"callback_url": "https://toko-anda.id/webhook/sekalipay",
"return_url": "https://toko-anda.id/checkout/selesai"
});
const signature = createHmac("sha256", process.env.SEKALIPAY_SECRET_KEY)
.update(body)
.digest("hex");
const res = await fetch("https://sekalipay.com/api/v1/gateway/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-API-Key": process.env.SEKALIPAY_API_KEY,
"X-Signature": signature,
},
body,
});
const result = await res.json();BODY='{"merchant_ref_id":"ORDER-2026-00184","amount":150000,"payment_code":"QRIS","customer_name":"Budi Santoso","customer_email":"[email protected]","customer_phone":"081234567890","callback_url":"https://toko-anda.id/webhook/sekalipay","return_url":"https://toko-anda.id/checkout/selesai"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SEKALIPAY_SECRET_KEY" | sed 's/^.* //')
curl -X POST https://sekalipay.com/api/v1/gateway/payment \
-H "X-API-Key: $SEKALIPAY_API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data-raw "$BODY"Kesalahan paling umum
Menandatangani satu string lalu mengirim string lain, misalnya menandatangani hasil json_encode lalu membiarkan HTTP client meng-encode ulang array yang sama. Urutan kunci, spasi, atau escape \/ yang berbeda membuat tanda tangan tidak cocok. Simpan body sebagai string, tanda tangani string itu, kirim string itu.
Permintaan dari IP yang tidak terdaftar ditolak 403 IP_NOT_WHITELISTED, dan respons galatnya memuat your_ip: IP yang kami lihat, siap didaftarkan.
Bagaimana menandatangani permintaan di mode Enhanced HMAC?#
Setiap permintaan, termasuk GET, membawa X-Signature dan X-Timestamp. Yang ditandatangani adalah empat bagian yang digabung dengan |:
string_to_sign = METHOD + "|" + PATH + "|" + PARAMS + "|" + TIMESTAMP
X-Signature = hex( HMAC-SHA256( secret_key, string_to_sign ) )
POST|api/v1/gateway/payment|amount=50000&merchant_ref_id=ORDER-001&payment_code=QRIS|1791446400
GET|api/v1/gateway/payment/ORDER-001||1791446400
GET|api/v1/gateway/payment-methods||1791446400| Bagian | Isi |
|---|---|
METHOD | Huruf besar: GET, POST. |
PATH | Tanpa garis miring di depan dan tanpa query string: api/v1/gateway/payment. |
PARAMS | Semua parameter (field body JSON dan query string), kunci tingkat atas diurutkan A sampai Z, lalu di-encode persis seperti http_build_query() PHP. Kosong untuk GET tanpa parameter. |
TIMESTAMP | Unix time dalam detik, sama dengan header X-Timestamp. Selisih dengan jam server maksimal 300 detik. |
Aturan encode PARAMS (penting untuk selain PHP)
Spasi menjadi +; hanya A-Z a-z 0-9 - _ . yang tetap, selebihnya %XX (hex huruf besar, byte UTF-8). Nilai null dan array kosong dihilangkan; true/false menjadi 1/0. Hanya kunci tingkat atas yang diurutkan; objek bertingkat seperti metadata mengikuti urutan kirim dan menjadi metadata%5Bkunci%5D=nilai. Server memangkas spasi di awal dan akhir setiap string dan membuang string kosong sebelum memverifikasi, jadi lakukan hal yang sama. Kedua contoh di bawah sudah menanganinya.
function phpUrlencode(value) {
return encodeURIComponent(value)
.replace(/[!'()*~]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase())
.replace(/%20/g, "+");
}
function buildParams(params) {
const parts = [];
const add = (prefix, value) => {
if (typeof value === "string") value = value.trim();
if (value === null || value === undefined || value === "") return;
if (typeof value === "object") {
for (const key of Object.keys(value)) add(prefix + "%5B" + phpUrlencode(key) + "%5D", value[key]);
return;
}
if (value === true) value = "1";
else if (value === false) value = "0";
parts.push(prefix + "=" + phpUrlencode(String(value)));
};
for (const key of Object.keys(params).sort()) add(phpUrlencode(key), params[key]);
return parts.join("&");
}
async function hmacSha256Hex(secret, message) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
export async function sekalipay(method, path, payload, { apiKey, secretKey }) {
const timestamp = Math.floor(Date.now() / 1000);
const stringToSign = method + "|" + path + "|" + buildParams(payload ?? {}) + "|" + timestamp;
const signature = await hmacSha256Hex(secretKey, stringToSign);
const res = await fetch("https://sekalipay.com/" + path, {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-API-Key": apiKey,
"X-Signature": signature,
"X-Timestamp": String(timestamp),
},
body: method === "GET" ? undefined : JSON.stringify(payload),
});
return { status: res.status, body: await res.json() };
}
const keys = { apiKey: SEKALIPAY_API_KEY, secretKey: SEKALIPAY_SECRET_KEY };
await sekalipay("POST", "api/v1/gateway/payment", {
merchant_ref_id: "ORDER-2026-00184",
amount: 150000,
payment_code: "QRIS",
customer_name: "Budi Santoso",
customer_email: "[email protected]",
customer_phone: "081234567890",
}, keys);
await sekalipay("GET", "api/v1/gateway/payment/ORDER-2026-00184", null, keys);function sekalipay(string $method, string $path, array $payload = []): array
{
$timestamp = time();
$params = $payload;
array_walk_recursive($params, function (&$value) {
if (is_string($value)) {
$value = trim($value) === '' ? null : trim($value);
}
});
ksort($params);
$stringToSign = $method . '|' . $path . '|' . http_build_query($params) . '|' . $timestamp;
$signature = hash_hmac('sha256', $stringToSign, getenv('SEKALIPAY_SECRET_KEY'));
$ch = curl_init('https://sekalipay.com/' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
'X-API-Key: ' . getenv('SEKALIPAY_API_KEY'),
'X-Signature: ' . $signature,
'X-Timestamp: ' . $timestamp,
],
]);
if ($method !== 'GET') {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$body = curl_exec($ch);
return [curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($body, true)];
}
sekalipay('POST', 'api/v1/gateway/payment', [
'merchant_ref_id' => 'ORDER-2026-00184',
'amount' => 150000,
'payment_code' => 'QRIS',
'customer_name' => 'Budi Santoso',
'customer_email' => '[email protected]',
'customer_phone' => '081234567890',
]);
sekalipay('GET', 'api/v1/gateway/payment/ORDER-2026-00184');Jam server yang meleset lebih dari 300 detik membuat semua permintaan ditolak 401 TIMESTAMP_EXPIRED. Respons galatnya memuat server_time untuk menyelaraskan jam.
{
"success": false,
"message": "Request timestamp expired or invalid",
"error_code": "TIMESTAMP_EXPIRED",
"data": {
"server_time": 1791446812,
"your_timestamp": 1791446400,
"max_difference": 300
}
}Bagaimana memastikan tanda tangan saya sudah benar?#
Jalankan kode Anda dengan masukan di bawah. Hasilnya harus sama persis sebelum Anda mencoba API. Kunci ini contoh saja dan tidak berlaku di API.
secret_key : sk_test_contoh_saja
body : {"merchant_ref_id":"ORDER-001","amount":50000,"payment_code":"QRIS"}
X-Signature : f8fbd182f2c14ae0b8a231f0687e4070f095fe1881374f09f6a4634cf021fbfasecret_key : sk_test_contoh_saja
X-Timestamp : 1791446400
string_to_sign : POST|api/v1/gateway/payment|amount=50000&merchant_ref_id=ORDER-001&payment_code=QRIS|1791446400
X-Signature : 46e51144b78aefcf73342111a465ddd6aede25abdcfbfdbb4c6cf1b792116402Galat autentikasi apa yang mungkin muncul?#
| HTTP | Kode | Arti |
|---|---|---|
| 401 | MISSING_API_KEY | Header X-API-Key tidak dikirim. |
| 401 | INVALID_API_KEY | API key salah, sudah diganti, atau akun tidak aktif. |
| 403 | IP_WHITELIST_NOT_CONFIGURED | Mode IP Whitelist aktif tetapi belum ada IP terdaftar. Tambahkan IP, atau matikan whitelist untuk memakai Enhanced HMAC. |
| 403 | IP_NOT_WHITELISTED | IP pengirim tidak terdaftar. Daftarkan IP yang tertera di your_ip. |
| 401 | MISSING_SIGNATURE | Permintaan POST di mode IP Whitelist tanpa header X-Signature. |
| 401 | INVALID_SIGNATURE | Tanda tangan body tidak cocok. Tanda tangani byte yang persis sama dengan yang dikirim. |
| 401 | MISSING_HMAC_SIGNATURE | Mode Enhanced HMAC aktif tetapi X-Signature atau X-Timestamp tidak dikirim. |
| 401 | TIMESTAMP_EXPIRED | X-Timestamp berselisih lebih dari 300 detik dari jam server. Bandingkan dengan data.server_time. |
| 401 | INVALID_HMAC_SIGNATURE | Tanda tangan Enhanced HMAC tidak cocok. Periksa PATH, urutan dan encode PARAMS, serta timestamp. |
Siap memakai kunci Anda sendiri?
Masuk ke dasbor merchant untuk mengambil API key dan secret key, mengatur mode akses dan URL webhook, serta mengunduh koleksi Postman berisi kunci akun Anda.
Masuk ke dasbor
