Autentikasi & Penandatanganan Permintaan
Tandatangani permintaan API dengan HMAC-SHA256 menggunakan project UUID dan API key Anda.
Setiap permintaan API (kecuali webhook masuk) harus membawa project UUID dan tanda tangan permintaan Anda. Tanda tangan membuktikan bahwa permintaan berasal dari Anda dan tidak ada yang mengubahnya selama pengiriman.
API key
2328.io menggunakan dua key yang berbagi algoritma penandatanganan yang sama tetapi mencakup endpoint yang berbeda:
| Key | Digunakan untuk |
|---|---|
| API key | Pembayaran, dompet statis, saldo, nilai tukar, dan verifikasi webhook pembayaran / dompet statis |
| Payout API key | Semua endpoint /v1/payout/* dan verifikasi webhook penarikan |
Kedua key tersedia di pengaturan project Anda di 2328.io. Contoh di bawah menyebut "API key" secara umum — gantikan dengan key yang sesuai untuk endpoint yang Anda panggil.
Jangan pernah mencampur kedua key: menandatangani permintaan penarikan dengan API key biasa (atau permintaan pembayaran dengan payout key) akan mengembalikan kesalahan tanda tangan.
Header yang diperlukan
| Header | Tipe | Wajib | Deskripsi |
|---|---|---|---|
Content-Type | string | ya | Selalu application/json |
project | string | ya | Project UUID Anda |
sign | string | ya | Tanda tangan HMAC-SHA256 dari permintaan, dihitung dengan API key Anda |
User-Agent | string | ya | Mengidentifikasi aplikasi Anda (mis. MyShop/1.4 (+https://myshop.example)). Permintaan tanpa User-Agent dapat diblokir. |
Cara kerja tanda tangan
Anggap tanda tangan sebagai sidik jari dari body permintaan. Dibuat dengan:
- Serialisasi body ke JSON (kompak — tanpa spasi tambahan).
- Encode JSON tersebut ke Base64. Langkah ini menormalkan input lintas bahasa — setelah menjadi ASCII murni, setiap bahasa menghasilkan byte yang sama untuk HMAC.
- Hitung HMAC-SHA256 dari string Base64 menggunakan API key Anda, lalu konversi hasilnya ke huruf kecil hex.
Untuk GET dan tipe permintaan lain tanpa body, tandatangani string kosong sebagai pengganti JSON.
Tanda tangan string kosong bersifat konstan untuk API key tertentu. Anda dapat menyimpannya di cache jika melakukan banyak panggilan GET.
Implementasi
<?php
function apiSign(array $data, string $apiKey): string {
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$base64 = base64_encode($json);
return hash_hmac('sha256', $base64, $apiKey);
}import crypto from "crypto";
export function apiSign(data, apiKey) {
const json = JSON.stringify(data);
const base64 = Buffer.from(json).toString("base64");
return crypto.createHmac("sha256", apiKey).update(base64).digest("hex");
}import { createHmac } from "crypto";
export function apiSign(data: object, apiKey: string): string {
const json = JSON.stringify(data);
const base64 = Buffer.from(json).toString("base64");
return createHmac("sha256", apiKey).update(base64).digest("hex");
}import json
import hmac
import hashlib
import base64
def api_sign(data: dict, api_key: str) -> str:
# ensure_ascii=False keeps non-ASCII characters (Cyrillic, Chinese, …)
# as-is. Without it, Python escapes them to \uXXXX and the signature
# diverges from PHP / Node / Go.
body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
b64 = base64.b64encode(body.encode("utf-8")).decode()
return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()package sign
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
)
func ApiSign(data any, apiKey string) (string, error) {
// json.Encoder with SetEscapeHTML(false) — without it, Go escapes <, >, &
// to \u003c etc., which breaks compatibility with PHP / Node / Python.
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(data); err != nil {
return "", err
}
// Encoder appends a trailing newline — drop it.
body := bytes.TrimRight(buf.Bytes(), "\n")
b64 := base64.StdEncoding.EncodeToString(body)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
return hex.EncodeToString(h.Sum(nil)), nil
}Permintaan tanpa body (GET)
Untuk permintaan tanpa body (mis. GET /v1/payout/status/{uuid}), tandatangani string kosong:
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$API_KEY" -hex | awk '{print $NF}')$sign = hash_hmac('sha256', base64_encode(''), $apiKey);import { createHmac } from "crypto";
const sign = createHmac("sha256", apiKey)
.update(Buffer.from("").toString("base64"))
.digest("hex");import { createHmac } from "crypto";
const sign: string = createHmac("sha256", apiKey)
.update(Buffer.from("").toString("base64"))
.digest("hex");import hmac
import hashlib
import base64
sign = hmac.new(
api_key.encode(),
base64.b64encode(b"").decode().encode(),
hashlib.sha256,
).hexdigest()package sign
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
)
func EmptyBodySign(apiKey string) string {
b64 := base64.StdEncoding.EncodeToString([]byte(""))
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
return hex.EncodeToString(h.Sum(nil))
}Contoh permintaan lengkap
curl -X POST https://api.2328.io/api/v1/payment \
-H "Content-Type: application/json" \
-H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
-H "project: YOUR_PROJECT_UUID" \
-H "sign: YOUR_HMAC_SIGNATURE" \
-d '{"amount":"100.00","currency":"USD","order_id":"ORDER-123"}'<?php
function apiSign(string $body, string $apiKey): string {
return hash_hmac('sha256', base64_encode($body), $apiKey);
}
$project = 'YOUR_PROJECT_UUID';
$apiKey = 'YOUR_API_KEY';
$data = [
'amount' => '100.00',
'currency' => 'USD',
'order_id' => 'ORDER-123',
];
$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);
$ch = curl_init('https://api.2328.io/api/v1/payment');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'User-Agent: MyShop/1.0 (+https://myshop.example)',
"project: $project",
"sign: $sign",
],
]);
$response = json_decode(curl_exec($ch), true);import { createHmac } from "crypto";
function apiSign(body, apiKey) {
const base64 = Buffer.from(body, "utf8").toString("base64");
return createHmac("sha256", apiKey).update(base64).digest("hex");
}
const data = {
amount: "100.00",
currency: "USD",
order_id: "ORDER-123",
};
const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY);
const res = await fetch("https://api.2328.io/api/v1/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
project: process.env.PROJECT_UUID,
sign,
},
body,
});
const json = await res.json();import { createHmac } from "crypto";
function apiSign(body: string, apiKey: string): string {
const base64 = Buffer.from(body, "utf8").toString("base64");
return createHmac("sha256", apiKey).update(base64).digest("hex");
}
type CreatePaymentBody = {
amount: string;
currency: string;
order_id: string;
};
type CreatePaymentResponse = { state: number; result: unknown };
const data: CreatePaymentBody = {
amount: "100.00",
currency: "USD",
order_id: "ORDER-123",
};
const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY!);
const res = await fetch("https://api.2328.io/api/v1/payment", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
project: process.env.PROJECT_UUID!,
sign,
},
body,
});
const json = (await res.json()) as CreatePaymentResponse;import json
import hmac
import hashlib
import base64
import httpx
def api_sign(body: str, api_key: str) -> str:
b64 = base64.b64encode(body.encode("utf-8")).decode()
return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
data = {
"amount": "100.00",
"currency": "USD",
"order_id": "ORDER-123",
}
body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, API_KEY)
r = httpx.post(
"https://api.2328.io/api/v1/payment",
headers={
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
"project": PROJECT_UUID,
"sign": sign,
},
content=body.encode("utf-8"),
)
response = r.json()package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
)
func ApiSign(body []byte, apiKey string) string {
b64 := base64.StdEncoding.EncodeToString(body)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
return hex.EncodeToString(h.Sum(nil))
}
func marshalCanonical(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
func main() {
data := struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
OrderID string `json:"order_id"`
}{
Amount: "100.00",
Currency: "USD",
OrderID: "ORDER-123",
}
body, err := marshalCanonical(data)
if err != nil {
panic(err)
}
sign := ApiSign(body, apiKey)
req, _ := http.NewRequest("POST",
"https://api.2328.io/api/v1/payment",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "MyShop/1.0 (+https://myshop.example)")
req.Header.Set("project", projectUUID)
req.Header.Set("sign", sign)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}Jangan pernah mengekspos API key Anda di kode sisi klien. Tandatangani permintaan di backend Anda. API key yang bocor memberi siapa pun akses penuh ke akun merchant Anda.
Memverifikasi tanda tangan webhook
Saat 2328.io mengirim webhook kepada Anda, algoritma yang sama berjalan secara terbalik:
- Tarik field
signdari payload. - Encode field yang tersisa ke JSON (kompak, tanpa spasi).
- Encode string tersebut ke Base64.
- Hitung
HMAC-SHA256dengan key yang sesuai. - Bandingkan dengan
signyang diterima menggunakan perbandingan constant-time (hash_equals,crypto.timingSafeEqual,hmac.compare_digest,subtle.ConstantTimeCompare,OpenSSL.fixed_length_secure_compare).
Key penandatanganan tergantung pada sumber webhook:
| Webhook | Key untuk verifikasi |
|---|---|
Webhook pembayaran / dompet statis (/v1/payment, /v1/static-wallet) | API key |
Webhook penarikan (/v1/payout) | Payout API key |
Kesalahan umum verifikasi. Encoder JSON Anda harus menghasilkan byte yang persis sama dengan yang dihasilkan pengirim — jika tidak, Base64 akan berbeda dan tanda tangan tidak akan cocok.
- Go: gunakan
json.NewEncoderdenganSetEscapeHTML(false).json.Marshaldefault melakukan escape<,>,&menjadi<dan merusak tanda tangan. - Python: berikan
ensure_ascii=Falsekejson.dumps. Tanpa itu, karakter non-ASCII (Sirilik, Tionghoa, …) di-escape menjadi\uXXXX. - JSON ringkas: tanpa spasi antar field (
separators=(",", ":")di Python). - Urutan field (Go):
map[string]anybiasa mengacak kunci saat di-encode ulang. Gunakanjson.RawMessage, struct yang berurutan, atau hapussigndari byte mentah.
Jika verifikasi terus gagal, jalankan apiSign pada payload Anda sendiri — hasilnya harus berupa string heksadesimal yang sama dengan sign yang diterima.
Tanda tangan yang valid tidak mencegah replay. Tanda tangan hanya membuktikan bahwa webhook berasal dari 2328.io — bukan menghentikan penyerang untuk mengirim ulang webhook yang telah ditangkap belakangan. Selalu periksa idempotensi berdasarkan uuid (atau txid untuk dompet statis) sebelum mengkreditkan dana. Tolak dengan HTTP 401 jika tanda tangan hilang atau salah.
Contoh kode lengkap ada di Webhook Notifications. Penanganan retry dan aturan idempotensi ada di Praktik terbaik.