# Memulai

Xmltronik API memberikan akses katalog produk melalui API partner. Akun partner Anda sudah siap digunakan setelah aktivasi.

## Base URL

| Environment | Base URL |
| --- | --- |
| Production | https://openapi.xmltronik.id |

## Prasyarat: pendaftaran OpenAPI

Sebelum akun partner dibuat, ajukan pendaftaran OpenAPI ke XMLTRONIK melalui tim kami. Pengajuan ini diperlukan agar akses API partner dapat disiapkan. Setelah pengajuan disetujui, XMLTRONIK membuat akun partner dan mengirim email aktivasi ke alamat email yang Anda daftarkan.

## 1. Aktivasi akun

Buka tautan aktivasi yang dikirim ke email partner Anda. Tautan berlaku 24 jam. Set password, lalu login di `/partner`.

## 2. Siapkan kredensial

Di dashboard partner, buat partner key pair. Simpan private key di sisi Anda dan unggah public key ke platform. Lengkapi callback URL dan whitelist IP sesuai kebutuhan.

Dashboard juga menampilkan platform public key untuk verifikasi callback. Client secret tersedia di halaman yang sama dan bisa dilihat ulang atau dirotasi.

Perintah pembuatan key pair dan detail formatnya ada di [Pembuatan Signature](/docs/signature).

## 3. Minta access token

```
POST /openapi/v1.0/access-token/b2b
X-CLIENT-KEY: <client_id>
X-TIMESTAMP: <ISO-8601>
X-SIGNATURE: <SHA256withRSA(clientId|X-TIMESTAMP)>

{ "grantType": "client_credentials" }
```

Signature dibuat dengan partner private key. Access token berlaku maksimal 15 menit. Langkah lengkap, contoh nilai, dan penanganan error ada di [Pembuatan Signature](/docs/signature).

## 4. Ambil katalog produk

```
GET /openapi/v1.0/products
Authorization: Bearer <accessToken>
X-PARTNER-ID: <client_id>
X-EXTERNAL-ID: <random string>
CHANNEL-ID: WEB
X-TIMESTAMP: <ISO-8601>
X-SIGNATURE: HMAC_SHA512(METHOD:path:token:sha256(body):timestamp)
```

Response berisi katalog produk yang tersedia untuk akun partner Anda, dikelompokkan per operator.

---

# 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_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. Formatnya`yyyy-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:

| 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 memakai`clientId` 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 selalu`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.

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.

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

| 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 mengembalikan`4010201`, 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.

---

# Access Token B2B

Endpoint ini menerbitkan access token yang dipakai untuk seluruh request partner.

## Service Info

| Item | Value |
| --- | --- |
| HTTP Method | POST |
| Service Code | 01 |
| Base URL | URL yang diberikan saat aktivasi |
| Path | /openapi/v1.0/access-token/b2b |

## Header

| Header | Tipe | Wajib | Deskripsi |
| --- | --- | --- | --- |
| Content-Type | String | Ya | application/json |
| X-CLIENT-KEY | String | Ya | client_id partner |
| X-TIMESTAMP | String | Ya | yyyy-MM-ddTHH:mm:ss+07:00 |
| X-SIGNATURE | String | Ya | RSA-SHA256(clientId\|X-TIMESTAMP) |

## Body

| Parameter | Tipe | Wajib | Deskripsi |
| --- | --- | --- | --- |
| grantType | String | Ya | Nilai: client_credentials |

## Request Sample

```
{
  "grantType": "client_credentials"
}
```

## Response Structure

| Parameter | Tipe | Deskripsi |
| --- | --- | --- |
| responseCode | String | Kode response |
| responseMessage | String | Pesan response |
| accessToken | String | Token untuk setiap request partner |
| tokenType | String | Bearer |
| expiresIn | String | 900 detik |
| additionalInfo | Object | Informasi tambahan, saat ini kosong |

## Response Sample

```
{
  "responseCode": "2000100",
  "responseMessage": "Successful",
  "accessToken": "<accessToken>",
  "tokenType": "Bearer",
  "expiresIn": "900",
  "additionalInfo": {}
}
```

## Response Codes

| Response Code | Message | Deskripsi |
| --- | --- | --- |
| 2000100 | Successful | Token berhasil dibuat |
| 4000100 | Bad Request | Request body tidak valid |
| 4000101 | Invalid Field Format | Format field tidak valid |
| 4000102 | Invalid Mandatory Field | Header wajib tidak ada |
| 4010100 | Unauthorized | Signature salah atau client tidak dikenal |
| 4030100 | Forbidden | Akses ditolak |
| 4290100 | Too Many Requests | Batas request terlampaui |
| 5000101 | Internal Server Error | Error server |

Cara membuat signature dijelaskan di [Signature Generation](/docs/signature).

---

# Katalog Produk

Endpoint ini mengembalikan katalog produk yang tersedia untuk akun partner Anda.

## Service Info

| Item | Value |
| --- | --- |
| HTTP Method | GET |
| Service Code | 02 |
| Base URL | URL yang diberikan saat aktivasi |
| Path | /openapi/v1.0/products |

## Header

| Header | Tipe | Wajib | Deskripsi |
| --- | --- | --- | --- |
| Authorization | String | Ya | Bearer <accessToken> |
| X-PARTNER-ID | String | Ya | client_id partner |
| X-EXTERNAL-ID | String | Ya | String random, maksimal 36 karakter |
| CHANNEL-ID | String | Ya | Nilai: WEB |
| X-TIMESTAMP | String | Ya | yyyy-MM-ddTHH:mm:ss+07:00 |
| X-SIGNATURE | String | Ya | HMAC-SHA512 request signature |

## Response Structure

| Parameter | Tipe | Deskripsi |
| --- | --- | --- |
| responseCode | String | Kode response |
| responseMessage | String | Pesan response |
| data | Array | Daftar kategori produk |
| data[].nama_kategori | String | Nama operator |
| data[].produk | Array | Daftar produk dalam kategori |
| data[].produk[].kode_produk | String | Kode produk |
| data[].produk[].nama_produk | String | Nama produk |
| data[].produk[].harga_jual | Number | Harga produk |
| data[].produk[].gangguan | Boolean | Status gangguan produk |

## Response Sample

```
{
  "responseCode": "2000200",
  "responseMessage": "Successful",
  "data": [
    {
      "nama_kategori": "TELKOMSEL",
      "produk": [
        {
          "kode_produk": "TSEL10",
          "nama_produk": "Telkomsel 10",
          "harga_jual": 10500,
          "gangguan": false
        }
      ]
    }
  ]
}
```

## Response Codes

| Response Code | Message | Deskripsi |
| --- | --- | --- |
| 2000200 | Successful | Katalog berhasil diambil |
| 4000201 | Invalid Field Format | Format field tidak valid |
| 4000202 | Invalid Mandatory Field | Header wajib tidak ada |
| 4010200 | Unauthorized | Signature request tidak cocok |
| 4010201 | Invalid Token | Access token tidak valid |
| 4010203 | Token Not Found | Access token tidak ada |
| 4030200 | Forbidden | Akses ditolak |
| 4290200 | Too Many Requests | Batas request terlampaui |
| 5000200 | General Error | Error server |

---

# Callback

Callback URL diatur per partner di dashboard partner, bukan global per akun.

Pengiriman callback belum aktif. Header yang akan digunakan: `X-CLIENT-ID`, `X-TIMESTAMP`, dan `X-SIGNATURE`.

---

# Referensi Error

| Status | Arti |
| --- | --- |
| 400 Bad Request | Format request atau signature tidak valid |
| 401 Unauthorized | Access token tidak ada, tidak valid, atau kedaluwarsa |
| 403 Forbidden | Kredensial revoked atau signature tidak cocok |
| 429 Too Many Requests | Rate limit partner terlampaui, lihat header Retry-After |
| 500 Internal Server Error | Error server yang tidak terduga |

## Rate limiting

Endpoint partner memiliki rate limit per partner. Melebihi batas akan mengembalikan `429` dengan header `Retry-After`.
