Pembuatan Signature
Setiap request partner memakai dua signature yang berbeda. Signature pertama untuk meminta access token dan memakai partner private key. Signature kedua untuk memanggil endpoint produk dan memakai client secret. Keduanya memakai encoding hex, dan semua string ditandatangani sebagai UTF-8.
Ringkasan
| Tahap | String yang ditandatangani | Kunci atau secret | Header |
|---|---|---|---|
Access token | clientId|timestamp | Partner private key | X-SIGNATURE |
Request API | METHOD:path:accessToken:sha256(body):timestamp | Client secret | X-SIGNATURE |
Yang perlu disiapkan
client_iddari dashboard partner.client_secretdari dashboard partner. Pakai nilainya persis, jangan di-decode.- Partner private key yang Anda buat sendiri. Jangan pernah mengirimkannya ke platform.
- Partner public key yang diunggah ke dashboard.
CHANNEL-IDdari dashboard partner. Saat ini nilainyaWEB.
Membuat partner key pair
Buat key pair RSA 2048 bit di sisi Anda. Private key disimpan sendiri, public key diunggah ke dashboard partner.
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out partner-private-key.pem
openssl rsa -pubout -in partner-private-key.pem -out partner-public-key.pemUnggah isi partner-public-key.pem. File private key tidak boleh diunggah. Platform memakai public key tersebut untuk memverifikasi signature access token.
1. Membuat timestamp
Timestamp dibuat saat request, bukan saat aplikasi mulai. Formatnyayyyy-MM-ddTHH:mm:ss+07:00, tanpa milidetik. Nilai harus berada dalam toleransi 5 menit dari waktu server.
function timestampNow() {
return new Date(Date.now() + 7 * 60 * 60 * 1000)
.toISOString()
.replace(/\.\d{3}Z$/, "+07:00");
}
// Contoh: 2026-11-04T14:30:00+07:00
const timestamp = timestampNow();Timestamp untuk access token dan timestamp untuk request boleh berbeda. Selama masing-masing masih dalam toleransi 5 menit, keduanya valid.
Pastikan jam server memakai sinkronisasi waktu (NTP). Selisih jam lebih dari 5 menit akan membuat timestamp ditolak.
2. Membuat signature access token
Bentuk string berikut tanpa spasi tambahan:
clientId + "|" + timestampContoh nilai:
| Bagian | Contoh |
|---|---|
clientId | 01a1b2c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5 |
timestamp | 2026-11-04T14:30:00+07:00 |
String to sign | 01a1b2c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5|2026-11-04T14:30:00+07:00 |
Tanda tangani string tersebut dengan partner private key memakai RSA-SHA256 dengan padding PKCS#1 v1.5. Hasilnya berupa hex lowercase. Untuk RSA 2048, panjang signature 512 karakter hex.
import { createSign } from "node:crypto";
const stringToSign = `${clientId}|${timestamp}`;
const tokenSignature = createSign("RSA-SHA256")
.update(stringToSign, "utf8")
.sign(privateKeyPem, "hex");Body access token tidak masuk ke signature. Signature access token hanya memakaiclientId dan timestamp.
Kirim request berikut. Body harus valid JSON.
POST /openapi/v1.0/access-token/b2b
Content-Type: application/json
X-CLIENT-KEY: <client_id>
X-TIMESTAMP: <timestamp>
X-SIGNATURE: <tokenSignature>
{ "grantType": "client_credentials" }Response sukses berisi access token dan masa berlakunya:
{
"responseCode": "2000100",
"responseMessage": "Successful",
"accessToken": "<accessToken>",
"tokenType": "Bearer",
"expiresIn": "900",
"additionalInfo": {}
}Simpan accessToken. Token berlaku maksimal 15 menit. Jangan memakai token yang sudah kedaluwarsa dan jangan mengirim private key ke platform.
3. Menyiapkan bahan signature request
Signature request dibentuk dari lima nilai berikut:
| Nilai | Aturan | Contoh |
|---|---|---|
METHOD | Huruf besar | GET |
path | Path dan query, tanpa skema dan host | /openapi/v1.0/products |
accessToken | Token saja, tanpa awalan Bearer | <accessToken> |
bodyHash | SHA-256 dari body, hex lowercase | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
timestamp | Timestamp request | 2026-11-04T14:30:00+07:00 |
Untuk GET, body adalah string kosong. Hash SHA-256 dari string kosong selalue3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Untuk POST, pakai body mentah yang benar-benar dikirim. Jangan mengubah urutan key, spasi, atau formatting setelah signature dibuat. Jika ragu, hitung hash dari buffer yang sama dengan yang dikirim ke HTTP client.
import { createHash } from "node:crypto";
const method = "GET";
const path = "/openapi/v1.0/products";
const body = "";
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");4. Membuat signature request
Gabungkan nilai dengan titik dua, tanpa spasi:
METHOD:path:accessToken:bodyHash:timestampContoh string lengkap untuk GET:
GET:/openapi/v1.0/products:<accessToken>:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:2026-11-04T14:30:00+07:00Hitung HMAC-SHA512 memakai client_secret persis seperti yang ditampilkan di dashboard. Jangan melakukan base64 decode pada client secret. Hasilnya hex lowercase.
import { createHmac } from "node:crypto";
const stringToSign = `${method}:${path}:${accessToken}:${bodyHash}:${timestamp}`;
const requestSignature = createHmac("sha512", clientSecret)
.update(stringToSign, "utf8")
.digest("hex");Jika path memiliki query string, sertakan query tersebut. Contoh:/openapi/v1.0/products?contoh=1. Jangan memakai URL lengkap.
Contoh uji HMAC
Pakai nilai berikut untuk memastikan implementasi HMAC Anda benar. Client secret ini hanya contoh dan tidak berlaku di platform.
| Nilai | Contoh |
|---|---|
client_secret | contoh-client-secret |
accessToken | token-contoh |
method | GET |
path | /openapi/v1.0/products |
bodyHash | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
timestamp | 2026-11-04T14:30:00+07:00 |
String yang ditandatangani:
GET:/openapi/v1.0/products:token-contoh:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:2026-11-04T14:30:00+07:00Hasil HMAC-SHA512 yang diharapkan:
1e343cec039c43ece0058a04db425f7ae411b81635e1418cf6052f7d3fd58f26210a9798166f6d2d62e0431986f6a2eb1373e292d6a6f6b69454491879303f78Jika hasil Anda berbeda, periksa urutan bagian, jumlah titik dua, encoding secret, dan pastikan tidak ada spasi tambahan.
5. Mengirim request
| Header | Nilai | Catatan |
|---|---|---|
Authorization | Bearer <accessToken> | Token yang sama dengan yang dipakai di signature |
X-PARTNER-ID | <client_id> | Harus sama dengan client pada access token |
X-EXTERNAL-ID | String unik maksimal 36 karakter | Acak, unik per request. Karakter: huruf, angka, titik, garis bawah, tanda hubung |
CHANNEL-ID | WEB | Harus sama dengan channel partner |
X-TIMESTAMP | <timestamp> | Timestamp yang dipakai di signature |
X-SIGNATURE | <requestSignature> | Hex lowercase |
GET /openapi/v1.0/products
Authorization: Bearer <accessToken>
X-PARTNER-ID: <client_id>
X-EXTERNAL-ID: 7f3c2a1b-9d4e-4c6a-8b2f-1e5d7c9a0b3f
CHANNEL-ID: WEB
X-TIMESTAMP: 2026-11-04T14:30:00+07:00
X-SIGNATURE: <requestSignature>6. Contoh lengkap
Contoh Node.js berikut meminta access token, membuat signature request, lalu mengambil katalog produk.
import { createHash, createHmac, createSign, randomUUID } from "node:crypto";
function timestampNow() {
return new Date(Date.now() + 7 * 60 * 60 * 1000)
.toISOString()
.replace(/\.\d{3}Z$/, "+07:00");
}
const baseUrl = "https://api.example.com";
const clientId = "<client_id>";
const clientSecret = "<client_secret>";
const channelId = "WEB";
const privateKeyPem = process.env.PARTNER_PRIVATE_KEY;
if (!privateKeyPem) {
throw new Error("PARTNER_PRIVATE_KEY is not set");
}
async function fetchProducts() {
const tokenTimestamp = timestampNow();
const tokenString = `${clientId}|${tokenTimestamp}`;
const tokenSignature = createSign("RSA-SHA256")
.update(tokenString, "utf8")
.sign(privateKeyPem, "hex");
const tokenResponse = await fetch(`${baseUrl}/openapi/v1.0/access-token/b2b`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CLIENT-KEY": clientId,
"X-TIMESTAMP": tokenTimestamp,
"X-SIGNATURE": tokenSignature,
},
body: JSON.stringify({ grantType: "client_credentials" }),
});
const tokenBody = await tokenResponse.json();
if (!tokenResponse.ok) {
throw new Error(
"Token request failed: " + tokenResponse.status + " " + JSON.stringify(tokenBody),
);
}
const accessToken = tokenBody.accessToken;
const method = "GET";
const path = "/openapi/v1.0/products";
const body = "";
const requestTimestamp = timestampNow();
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
const requestString = `${method}:${path}:${accessToken}:${bodyHash}:${requestTimestamp}`;
const requestSignature = createHmac("sha512", clientSecret)
.update(requestString, "utf8")
.digest("hex");
const response = await fetch(`${baseUrl}${path}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"X-PARTNER-ID": clientId,
"X-EXTERNAL-ID": randomUUID(),
"CHANNEL-ID": channelId,
"X-TIMESTAMP": requestTimestamp,
"X-SIGNATURE": requestSignature,
},
});
const responseBody = await response.json();
if (!response.ok) {
throw new Error(
"Product request failed: " + response.status + " " + JSON.stringify(responseBody),
);
}
return responseBody;
}7. Token kedaluwarsa
Access token berlaku paling lama 15 menit. Jika endpoint mengembalikan4010201, minta access token baru, lalu buat signature request baru dengan token dan timestamp baru. Jangan memakai ulang signature lama.
Cara sederhana: simpan expiresIn, perbarui token sebelum kedaluwarsa, dan siapkan satu kali retry saat menerima 4010201.
Jangan meminta access token baru untuk setiap request. Simpan token dan pakai ulang sampai mendekati kedaluwarsa.
Bagaimana signature diverifikasi
Platform membentuk ulang string yang sama dari nilai yang diterima: client ID dan timestamp untuk access token, lalu method, path, access token, body hash, dan timestamp untuk request. Hasil hitung platform dibandingkan dengan X-SIGNATURE. Karena itu, satu spasi tambahan, urutan bagian yang berbeda, atau body yang berubah setelah signature dibuat akan membuat signature tidak cocok.
Kode error yang sering muncul
| Response code | Artinya | Yang perlu dicek |
|---|---|---|
4000100 | Body access token bukan JSON valid | Body harus JSON valid |
4000101 | Format field access token tidak valid | Timestamp +07:00, selisih 5 menit, atau grantType |
4000102 | Header access token tidak lengkap | X-CLIENT-KEY, X-TIMESTAMP, X-SIGNATURE |
4010100 | Signature access token gagal | Private key, client_id, dan encoding signature |
4030100 | IP tidak diizinkan | Whitelist IP partner |
4000201 | Format field request tidak valid | X-EXTERNAL-ID, X-TIMESTAMP, CHANNEL-ID |
4000202 | Header request tidak lengkap | Semua header wajib sudah dikirim |
4010200 | Signature request gagal | Client secret, path, body hash, dan timestamp |
4010201 | Access token tidak valid | Token sudah kedaluwarsa atau bukan milik partner |
4010203 | Access token tidak ada | Header Authorization Bearer |
4030200 | Akses katalog ditolak | Whitelist IP dan status akun partner |
Contoh response error
{
"responseCode": "4010200",
"responseMessage": "Unauthorized. Verify signature failed"
}Checklist sebelum menghubungi support
- Timestamp memakai format yang benar dan masih dalam 5 menit.
- String to sign tidak memiliki spasi tambahan.
- Signature access token memakai private key, bukan client secret.
- Signature request memakai client secret, bukan private key.
- Client secret dipakai persis, tanpa base64 decode.
- Path tidak memuat host dan menyertakan query jika ada.
- Body hash dihitung dari body yang benar-benar dikirim.
- Token yang dipakai di signature sama dengan token di header Authorization.
- CHANNEL-ID sama dengan channel partner.
- X-EXTERNAL-ID unik dan maksimal 36 karakter.