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

TahapString yang ditandatanganiKunci atau secretHeader
Access tokenclientId|timestampPartner private keyX-SIGNATURE
Request APIMETHOD:path:accessToken:sha256(body):timestampClient secretX-SIGNATURE

Yang perlu disiapkan

  • client_id dari dashboard partner.
  • client_secret dari 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-ID dari dashboard partner. Saat ini nilainya WEB.

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.pem

Unggah 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 + "|" + timestamp

Contoh nilai:

BagianContoh
clientId01a1b2c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5
timestamp2026-11-04T14:30:00+07:00
String to sign01a1b2c3-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:

NilaiAturanContoh
METHODHuruf besarGET
pathPath dan query, tanpa skema dan host/openapi/v1.0/products
accessTokenToken saja, tanpa awalan Bearer<accessToken>
bodyHashSHA-256 dari body, hex lowercasee3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
timestampTimestamp request2026-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:timestamp

Contoh string lengkap untuk GET:

GET:/openapi/v1.0/products:<accessToken>:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:2026-11-04T14:30:00+07:00

Hitung 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.

NilaiContoh
client_secretcontoh-client-secret
accessTokentoken-contoh
methodGET
path/openapi/v1.0/products
bodyHashe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
timestamp2026-11-04T14:30:00+07:00

String yang ditandatangani:

GET:/openapi/v1.0/products:token-contoh:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:2026-11-04T14:30:00+07:00

Hasil HMAC-SHA512 yang diharapkan:

1e343cec039c43ece0058a04db425f7ae411b81635e1418cf6052f7d3fd58f26210a9798166f6d2d62e0431986f6a2eb1373e292d6a6f6b69454491879303f78

Jika hasil Anda berbeda, periksa urutan bagian, jumlah titik dua, encoding secret, dan pastikan tidak ada spasi tambahan.

5. Mengirim request

HeaderNilaiCatatan
AuthorizationBearer <accessToken>Token yang sama dengan yang dipakai di signature
X-PARTNER-ID<client_id>Harus sama dengan client pada access token
X-EXTERNAL-IDString unik maksimal 36 karakterAcak, unik per request. Karakter: huruf, angka, titik, garis bawah, tanda hubung
CHANNEL-IDWEBHarus 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 codeArtinyaYang perlu dicek
4000100Body access token bukan JSON validBody harus JSON valid
4000101Format field access token tidak validTimestamp +07:00, selisih 5 menit, atau grantType
4000102Header access token tidak lengkapX-CLIENT-KEY, X-TIMESTAMP, X-SIGNATURE
4010100Signature access token gagalPrivate key, client_id, dan encoding signature
4030100IP tidak diizinkanWhitelist IP partner
4000201Format field request tidak validX-EXTERNAL-ID, X-TIMESTAMP, CHANNEL-ID
4000202Header request tidak lengkapSemua header wajib sudah dikirim
4010200Signature request gagalClient secret, path, body hash, dan timestamp
4010201Access token tidak validToken sudah kedaluwarsa atau bukan milik partner
4010203Access token tidak adaHeader Authorization Bearer
4030200Akses katalog ditolakWhitelist 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.