Akses pesan pribadi
member JKT48, secara aman.
PM Partner API memberi aplikasi kamu akses baca ke pesan yang dibagikan member JKT48 lewat JKT48Connect โ dengan identitas partner sendiri, kontrol akses per-member, dan token yang cuma bisa dipakai oleh perangkat yang menerbitkannya.
01 Overview
Semua permintaan partner masuk lewat dua base URL berikut. Simpan keduanya โ kamu akan memakainya di setiap request.
| Layanan | Base URL | Kegunaan |
|---|---|---|
| Gateway | https://v5.jkt48connect.com/api/pm |
Registrasi, klaim akses, generate token |
| Messages | https://pmv.jkt48connect.com |
Ambil isi pesan member |
Daftar sekali โ klaim akses ke member yang kamu perlukan โ generate token setiap mau ambil data โ panggil endpoint messages dengan token itu. Token berumur pendek, jadi langkah generate perlu diulang secara berkala.
02 Konsep dasar
Empat istilah yang akan sering kamu temui di dokumentasi ini.
| Istilah | Artinya | Didapat dari |
|---|---|---|
| kid | ID unik partner kamu, didapat sekali saat registrasi. | Tim JKT48Connect |
| secret | Kunci rahasia untuk menandatangani request ke Gateway. | Tim JKT48Connect |
| apikey | Kunci statis yang dipakai di parameter ?apikey= saat generate token. | Tim JKT48Connect |
| DPoP key | Sepasang kunci yang kamu buat sendiri untuk membuktikan setiap permintaan ke Messages API benar dari perangkatmu. | Kamu generate sendiri |
| token | Izin baca sementara ke satu member tertentu, dikeluarkan lewat /generate. | Otomatis, hasil generate |
Kalau nilainya unik untuk identitas partner kamu (kid, secret, apikey) โ itu dikeluarkan oleh tim JKT48Connect lewat proses admin, kamu nggak bisa bikin sendiri.
Kalau nilainya berupa key pair kriptografi punya perangkatmu (DPoP private/public key) โ itu kamu generate dan simpan sendiri, tidak pernah diminta dari admin.
03 Quickstart
Dari nol sampai membaca pesan pertama, dalam empat langkah.
Buat DPoP key pair Kamu generate sendiri
Generate keypair ECDSA P-256 di sisi kamu sendiri. Private key tidak pernah dikirim ke mana pun โ bukan sesuatu yang diminta atau dikeluarkan oleh admin.
Node.jsconst { generateKeyPairSync } = require("crypto"); const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); // simpan ini di tempat aman, JANGAN pernah dibagikan console.log(privateKey.export({ type: "pkcs8", format: "pem" })); // kirim ini saat registrasi console.log(JSON.stringify(publicKey.export({ format: "jwk" })));
Daftar sebagai partner ADMIN
Kirim public key kamu ke /register. Simpan kid dan secret yang dikembalikan โ keduanya hanya muncul sekali. (Langkah ini dilakukan oleh tim JKT48Connect.)
Klaim akses ke member ADMIN
Aktifkan akses baca untuk member yang kamu perlukan lewat /access/claim. Berlaku 30 hari, tinggal klaim ulang untuk perpanjang. (Langkah ini dilakukan oleh tim JKT48Connect.)
Generate token & ambil pesan
Tiap kali mau ambil data, generate token baru lewat /generate, lalu pakai untuk memanggil /messages/:identifier di Messages API. Langkah ini butuh kid, secret, dan apikey yang kamu simpan dari proses admin sebelumnya Dari tim JKT48Connect โ kamu sendiri hanya perlu memakainya, tidak perlu meminta ulang tiap kali.
04 Registrasi partner ADMIN
Registrasi butuh API key yang diberikan oleh tim JKT48Connect. Hubungi tim kami kalau belum punya.
{
"label": "Nama Aplikasi Kamu",
"dpop_public_jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
Response
{
"status": true,
"data": {
"kid": "p_xxxxxxxxxxxx",
"secret": "..."
}
}
secret hanya ditampilkan sekali di response ini. Kalau hilang, kamu perlu
melakukan rotate secret untuk mendapat yang baru.
05 Membuat DPoP key
DPoP key adalah keypair ECDSA P-256 milikmu sendiri โ bukan diberikan oleh JKT48Connect. Private key-nya tetap di perangkat kamu selamanya; yang dikirim ke kami hanya public key-nya (saat registrasi atau saat rotate).
Kamu generate sendiri โ private & public key DPoP dibuat sepenuhnya di sisi kamu, kapan saja, tanpa perlu izin atau bantuan admin.
Yang perlu koordinasi dengan admin hanyalah mengirim public key-nya saat registrasi awal (bagian Registrasi partner) atau saat rotate.
Kalau kamu mengganti server, migrasi environment, atau curiga private key pernah ter-expose (misal ke-commit ke repo publik), segera rotate lewat /partner/rotate-key.
06 Menandatangani request
Tiga endpoint di Gateway (/access/claim, /access,
/generate) mewajibkan tiga header berikut di setiap panggilan.
| Header | Isi |
|---|---|
| x-kid | kid partner kamu |
| x-timestamp | Unix timestamp (milidetik) saat request dibuat |
| x-signature | Tanda tangan HMAC-SHA256 dari request ini |
import crypto from "crypto"; function signRequest(secret, method, path, extraSalt = "") { const timestamp = String(Date.now()); const derivedKey = crypto.hkdfSync( "sha256", Buffer.from(secret, "hex"), Buffer.from(timestamp, "utf8"), Buffer.from(`pm-gateway:${path}`, "utf8"), 32 ); const signingStr = `${method}:${path}:${KID}:${timestamp}${extraSalt}`; const signature = crypto .createHmac("sha256", Buffer.from(derivedKey)) .update(signingStr) .digest("hex"); return { timestamp, signature }; }
path yang dipakai untuk signing harus full path termasuk prefix,
misalnya /api/pm/generate โ bukan /generate saja.
Untuk endpoint /access/claim dan /generate, parameter
extraSalt wajib diisi dengan titik dua diikuti nilai identifier
member, contoh:
const { timestamp, signature } = signRequest(secret, "POST", "/api/pm/generate", `:${identifier}`); // signingStr akan jadi: POST:/api/pm/generate:KID:TIMESTAMP:Lily
Tanpa extraSalt ini, server akan menolak signature dengan pesan
"Signature tidak valid atau sudah dipakai (replay)" meskipun kid, secret,
dan timestamp semuanya benar โ errornya generic dan tidak menyebutkan extraSalt secara
eksplisit, jadi mudah membingungkan.
Endpoint /access (GET, melihat akses aktif) tidak butuh extraSalt
karena tidak menyasar identifier tertentu.
x-timestamp hanya diterima dalam rentang ยฑ60 detik dari waktu server.
Pastikan jam sistem kamu tersinkron (NTP) supaya tidak mendapat error timestamp.
KID dan SECRET yang dipakai buat menandatangani request ini
didapat dari tim JKT48Connect saat proses registrasi โ
bukan nilai yang kamu buat sendiri. Kalau belum punya, hubungi tim kami dulu.
07 Klaim akses ke member ADMIN
Aktifkan akses baca ke satu member JKT48 sebelum bisa generate token untuknya. Panggil ulang endpoint ini kapan saja untuk memperpanjang masa aktifnya.
x-kid: p_xxxxxxxxxxxx x-timestamp: 1751870000000 x-signature: ... Content-Type: application/jsonBody
{ "identifier": "Lily" }
Response
{
"status": true,
"message": "Akses ke \"Lily\" aktif selama 30 hari.",
"data": {
"identifier": "Lily",
"expires_at": 1754462000
}
}
Signature untuk endpoint ini wajib disign dengan
extraSalt = `:${identifier}`, sama seperti pada /generate.
Lihat contoh lengkap di bagian 06.
Melihat akses aktif
Pakai header signing yang sama untuk melihat daftar member yang sedang aktif untuk akun kamu, beserta tanggal kedaluwarsanya. Endpoint ini tidak butuh extraSalt.
08 Generate token
Token berumur pendek dan hanya berlaku untuk satu member. Generate token baru setiap kali kamu akan mengambil pesan โ jangan simpan token untuk dipakai berulang dalam jangka panjang.
x-kid: p_xxxxxxxxxxxx x-timestamp: 1751870005000 x-signature: ... x-identifier: Lily
Sama seperti /access/claim, signature endpoint ini wajib memakai
extraSalt = `:${identifier}`. Server akan merespons
{ status: false, message: "Signature tidak valid atau sudah dipakai (replay)" }
kalau extraSalt ini tidak disertakan, meskipun kredensial lain sudah benar.
Parameter ?apikey= di URL ini didapat dari tim JKT48Connect,
sama seperti kid dan secret โ bukan sesuatu yang kamu generate sendiri.
{
"status": true,
"data": {
"token": "eyJhbGciOi...",
"expiresIn": 600,
"identifier": "Lily"
}
}
09 Mengambil pesan
Endpoint ini ada di domain terpisah, pmv.jkt48connect.com, dan mewajibkan
dua header: token yang baru kamu generate, dan sebuah DPoP proof โ
bukti bahwa request ini benar berasal dari perangkat yang memegang DPoP key kamu.
Membuat DPoP proof
DPoP proof adalah JWT pendek yang kamu tanda tangani sendiri pakai DPoP private key, dibuat baru untuk setiap request.
import crypto from "crypto"; function b64url(buf) { return buf.toString("base64") .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function makeDpopProof(privateKeyPem, method, url) { const header = { alg: "ES256", typ: "dpop+jwt" }; const payload = { htm: method, htu: url, iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID(), }; const h = b64url(Buffer.from(JSON.stringify(header))); const p = b64url(Buffer.from(JSON.stringify(payload))); const signer = crypto.createSign("SHA256"); signer.update(`${h}.${p}`); signer.end(); const sig = signer.sign({ key: privateKeyPem, dsaEncoding: "ieee-p1363" }); return `${h}.${p}.${b64url(sig)}`; }
Memanggil endpoint
const urlNoQuery = `https://pmv.jkt48connect.com/messages/Lily`; const urlWithQuery = `${urlNoQuery}?page=1`; const dpop = makeDpopProof(privateKeyPem, "GET", urlNoQuery); const res = await fetch(urlWithQuery, { headers: { Authorization: `Bearer ${token}`, DPoP: dpop, }, }); const data = await res.json();
Saat membuat DPoP proof, field htu harus berisi URL tanpa
query string (misalnya https://pmv.jkt48connect.com/messages/Lily, bukan
...?page=1). Request fetch yang sebenarnya tetap dikirim ke URL lengkap
dengan ?page=1, tapi proof-nya sendiri disign dari base path saja.
{
"status": true,
"idol_name": "Lily",
"page": 1,
"encrypted": true,
"data": "base64-ciphertext..."
}
Isi pesan dienkripsi khusus untuk akun kamu. Lihat bagian dekripsi untuk cara membacanya.
10 Mengganti kredensial
Dua hal yang bisa kamu rotate sendiri, kapan pun, tanpa menunggu tim kami.
Rotate secret (HMAC)
Tanda tangani request ini dengan secret lama. Secret baru langsung aktif; token yang sudah terlanjur ada tetap valid sampai kedaluwarsa.
Rotate DPoP key
{ "dpop_public_jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } }
Setelah rotate, generate token baru sebelum memanggil /messages lagi โ
token yang diterbitkan sebelum rotate tidak akan lagi cocok dengan key baru.
11 Membaca response terenkripsi
Field data dari /messages berisi base64 dari
iv (12 byte) + authTag (16 byte) + ciphertext, dienkripsi AES-256-GCM
memakai output secret yang diberikan tim kami saat onboarding.
import crypto from "crypto"; function decryptMessageData(encryptedBase64, outputSecret) { const key = crypto.createHash("sha256").update(outputSecret).digest(); const combined = Buffer.from(encryptedBase64, "base64"); const iv = combined.subarray(0, 12); const authTag = combined.subarray(12, 28); const ciphertext = combined.subarray(28); const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return JSON.parse(decrypted.toString("utf8")); }
Belum punya output secret kamu? Hubungi tim JKT48Connect saat onboarding untuk mendapatkannya.
12 Kode error
Semua endpoint mengembalikan bentuk yang sama: { status: false, message: "..." }.
| Status | Arti | Yang perlu dilakukan |
|---|---|---|
| 400 | Header wajib hilang | Pastikan x-kid, x-timestamp, x-signature terkirim |
| 401 | Timestamp di luar jendela waktu | Sinkronkan jam sistem (NTP) |
| 401 | Signature tidak valid | Cek ulang cara penandatanganan di bagian 06 โ penyebab paling umum: lupa
menyertakan extraSalt (:identifier) untuk endpoint
/access/claim dan /generate, atau path yang disign
bukan full path (/api/pm/...) |
| 401 | DPoP proof invalid | Pastikan proof dibuat baru per request dan URL-nya (tanpa query string) sama persis dengan path yang dipanggil |
| 401 | Token binding tidak cocok | DPoP key baru saja dirotasi โ generate token baru |
| 403 | Belum punya akses ke member ini | Klaim akses dulu lewat /access/claim (ingat: sertakan extraSalt
:identifier saat signing) |
| 429 | Terlalu banyak permintaan | Kurangi frekuensi request, coba lagi sebentar lagi |
| 500 | Gangguan di sisi kami | Coba lagi; hubungi tim kami kalau berulang |
13 Rate limit
Setiap partner memiliki kuota permintaan wajar untuk penggunaan normal. Kalau aplikasi kamu perlu volume lebih tinggi dari kuota default, hubungi tim JKT48Connect untuk penyesuaian.
Cache hasil /messages di sisi kamu selama beberapa detik alih-alih
polling terus-menerus, dan generate token hanya saat benar-benar akan dipakai.
14 FAQ
Kenapa harus ada dua langkah, secret dan DPoP key?
Secret membuktikan identitas akun kamu ke Gateway. DPoP key membuktikan bahwa permintaan ke Messages API benar berasal dari perangkat kamu sendiri โ jadi token tetap aman dipakai meskipun secret dan DPoP key kamu simpan terpisah.
Berapa lama token berlaku?
10 menit. Generate token baru setiap kali kamu akan mengambil data terbaru.
Bagaimana kalau DPoP private key saya bocor?
Segera panggil /partner/rotate-key dengan public key baru. Key lama langsung tidak berlaku setelah itu.
Bisa akses banyak member sekaligus?
Bisa. Klaim akses satu per satu lewat /access/claim, lalu generate token
terpisah untuk masing-masing member saat dibutuhkan.
Kenapa muncul "Signature tidak valid atau sudah dipakai (replay)" padahal kid, secret, dan jam sudah benar?
Penyebab paling umum: lupa menyertakan extraSalt (:identifier)
saat signing untuk endpoint /access/claim dan /generate, atau
path yang disign tidak memakai full path (/api/pm/...). Lihat contoh lengkap
di bagian 06.
Butuh bantuan lebih lanjut?
Hubungi tim JKT48Connect melalui kanal partner yang sudah kamu gunakan saat onboarding.