Partner Auth API — Dapatkan JWT Context dengan HMAC Signature
Integrasi dengan Partner API TuplePay terdiri dari dua langkah. Langkah pertama adalah
menandatangani request menggunakan HMAC signature dengan Partner Credential yang disediakan oleh
TuplePay, kemudian menggunakan signature tersebut untuk mendapatkan JWT context dengan masa
berlaku singkat untuk tipe pengguna MERCHANT. Body request boleh kosong karena seluruh proses
autentikasi dilakukan melalui enam header yang dijelaskan di bawah.
Langkah kedua dilakukan saat mengakses layanan transaksi: gunakan JWT yang diperoleh sebagai
header Authorization: Bearer <token> untuk memanggil endpoint pada
Partner Transactions API, seperti pembuatan QRIS dan
disbursement.
Simpan dan gunakan kembali JWT tersebut selama masih berlaku. Hindari memanggil endpoint
autentikasi ini untuk setiap request transaksi. Pola ini serupa dengan alur client_credentials
pada OAuth 2.0.
Header yang wajib dikirim
Keenam header ini wajib dikirim secara lengkap. Jika salah satu header tidak diisi, request akan
ditolak dengan 400 Bad Request (MISSING_HEADER) sebelum diproses lebih lanjut.
| Header | Deskripsi |
|---|---|
X-Partner-Id | UUID Partner (lihat dashboard Partner). |
X-Key-Version | Versi Partner Credential yang digunakan untuk menandatangani request ini. Satu Partner dapat memiliki beberapa versi credential yang aktif secara bersamaan, misalnya selama proses rotasi key. |
X-Environment | SANDBOX atau PRODUCTION — nilainya harus sesuai dengan environment Partner Credential yang digunakan. |
X-Timestamp | Epoch seconds saat request dibuat. Request akan ditolak jika timestamp berada di luar toleransi 300 detik (5 menit) dari waktu server. Mekanisme ini membantu mencegah replay attack menggunakan request lama. |
X-Nonce | String unik untuk setiap request, dengan UUID v4 sebagai format yang direkomendasikan. Digunakan bersama X-Timestamp untuk mencegah replay attack. Kombinasi nonce dan Partner ID yang sama akan ditolak. |
X-Signature | Base64 hasil HMAC-SHA256 atau RSA-SHA256, tergantung pada tipe kunci Partner Credential yang digunakan, atas string-to-sign yang dijelaskan di bawah. |
Membentuk String-to-Sign
Signature dihitung dari string yang terdiri dari lima bagian, dengan setiap bagian dipisahkan
oleh karakter baris baru (\n):
{method}
{path}
{timestamp}
{nonce}
{sha256hex(body)}
method-- method HTTP dalam huruf besar, misalnyaPOST.path-- path request, misalnya/api/v1/auth/context(tanpa query string).timestamp-- nilai yang sama persis dengan headerX-Timestamp(epoch seconds).nonce-- nilai yang sama persis dengan headerX-Nonce.sha256hex(body)-- SHA-256 dari body request dalam bentuk hex string. Kalau body kosong, ini adalah SHA-256 dari string kosong.
Contoh request
- cURL
- Node.js
- Java
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen)
BODY_SHA256=$(printf '' | openssl dgst -sha256 | cut -d' ' -f2)
STRING_TO_SIGN="POST
/api/v1/auth/context
${TIMESTAMP}
${NONCE}
${BODY_SHA256}"
SIGNATURE=$(printf '%s' "$STRING_TO_SIGN" \
| openssl dgst -sha256 -hmac "$TUPLEPAY_HMAC_SECRET" -binary \
| base64)
curl -X POST '{{BASE_URL}}/api/v1/auth/context' \
-H "X-Partner-Id: $TUPLEPAY_PARTNER_ID" \
-H "X-Key-Version: 1" \
-H "X-Environment: SANDBOX" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"
import { randomUUID, createHash, createHmac } from "node:crypto";
const method = "POST";
const path = "/api/v1/auth/context";
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomUUID();
const body = ""; // body boleh kosong untuk endpoint ini
const bodySha256Hex = createHash("sha256").update(body).digest("hex");
const stringToSign = [method, path, timestamp, nonce, bodySha256Hex].join("\n");
const signature = createHmac("sha256", process.env.TUPLEPAY_HMAC_SECRET)
.update(stringToSign)
.digest("base64");
const response = await fetch("{{BASE_URL}}/api/v1/auth/context", {
method,
headers: {
"X-Partner-Id": process.env.TUPLEPAY_PARTNER_ID,
"X-Key-Version": "1",
"X-Environment": "SANDBOX",
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
},
});
const data = await response.json();
console.log(data);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.HexFormat;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
String method = "POST";
String path = "/api/v1/auth/context";
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String body = ""; // body boleh kosong untuk endpoint ini
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
String bodySha256Hex =
HexFormat.of().formatHex(sha256.digest(body.getBytes(StandardCharsets.UTF_8)));
String stringToSign = String.join("\n", method, path, timestamp, nonce, bodySha256Hex);
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(hmacSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = Base64.getEncoder().encodeToString(hmac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
HttpClient client = HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create("{{BASE_URL}}/api/v1/auth/context"))
.header("X-Partner-Id", partnerId)
.header("X-Key-Version", "1")
.header("X-Environment", "SANDBOX")
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", signature)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
{{BASE_URL}} — ganti dengan base URL environment TuplePay yang digunakan (SANDBOX atau
PRODUCTION) sesuai yang diberikan oleh tim integrasi partner. Contoh di atas menggunakan
HMAC-SHA256. Jika Partner Credential menggunakan tipe RSA, proses pembentukan stringToSign
tetap sama; yang berbeda hanya proses penandatanganannya, yaitu menggunakan private key RSA
alih-alih createHmac.
Response sukses
200 OK -- signature valid, JWT context berhasil diterbitkan.
{
"code": "00",
"message": "Success",
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtZXJjaGFudCJ9...",
"tokenType": "Bearer",
"partnerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"credentialId": "b2c3d4e5-6f78-90ab-cdef-1234567890ab"
},
"correlationId": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"requestId": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
"timestamp": "2026-08-13T09:15:30.123Z"
}
Pakai data.accessToken sebagai header Authorization: Bearer <accessToken> untuk memanggil
Partner Transactions API -- misalnya
buat transaksi QRIS.
Response ini tidak menyertakan field masa berlaku terpisah seperti expiresIn. accessToken
merupakan JWT standar; decode token dan gunakan klaim exp untuk mengetahui waktu
kedaluwarsanya.
Response error
Body error hanya berisi dua field: code dan message. Endpoint ini gagal sebelum request masuk
ke lapisan yang menambahkan correlationId, requestId, dan timestamp (berbeda dari response
endpoint lain di dokumentasi ini; lihat catatan di bawah tabel).
Untuk 9 kode kegagalan verifikasi (PARTNER_NOT_FOUND hingga IP_NOT_WHITELISTED pada tabel di
bawah), nilai message sengaja digeneralisasi menjadi satu kalimat yang sama. Tujuannya untuk
mencegah informasi mengenai bagian kredensial yang tidak valid terbuka kepada pemanggil yang
belum terverifikasi sebagai partner.
MISSING_HEADER merupakan pengecualian: pesannya menyebutkan header yang tidak diisi karena pada
tahap ini identitas pemanggil belum diverifikasi dan tidak ada informasi sensitif yang dapat
dibocorkan melalui nama header.
CONTEXT_TOKEN_MINT_FAILED juga merupakan pengecualian — lihat catatan di bawah tabel.
{
"code": "INVALID_SIGNATURE",
"message": "Otentikasi partner ditolak"
}
{
"code": "MISSING_HEADER",
"message": "Header 'X-Timestamp' wajib diisi"
}
| HTTP Status | code | Kapan terjadi |
|---|---|---|
| 400 | MISSING_HEADER | Salah satu dari 6 header wajib tidak dikirim atau formatnya tidak valid. |
| 401 | PARTNER_NOT_FOUND | X-Partner-Id tidak terdaftar. |
| 401 | PARTNER_NOT_ACTIVE | Partner ditemukan tapi statusnya bukan ACTIVE. |
| 401 | CREDENTIAL_NOT_FOUND | Kombinasi Partner + X-Key-Version + X-Environment tidak ditemukan. |
| 401 | CREDENTIAL_NOT_ACTIVE | Credential ditemukan tapi statusnya bukan ACTIVE. |
| 401 | CREDENTIAL_EXPIRED | Credential sudah lewat masa berlaku. |
| 401 | INVALID_SIGNATURE | X-Signature tidak cocok dengan string-to-sign yang dihitung server. |
| 401 | TIMESTAMP_EXPIRED | X-Timestamp di luar batas toleransi (default 300 detik). |
| 401 | REPLAY_DETECTED | Kombinasi X-Nonce + X-Partner-Id sudah pernah dipakai. |
| 403 | IP_NOT_WHITELISTED | Alamat IP pemanggil tidak ada di IP whitelist Partner. |
| 503 | CONTEXT_TOKEN_MINT_FAILED | Kredensial kamu valid, tapi backend TuplePay gagal menerbitkan JWT context (gangguan sistem sementara). Aman langsung di-retry -- tidak ada yang salah dengan signature/kredensial kamu. |
{
"code": "CONTEXT_TOKEN_MINT_FAILED",
"message": "Gagal memproses otentikasi karena gangguan sistem sementara, coba lagi."
}
CONTEXT_TOKEN_MINT_FAILED terjadi setelah kredensial berhasil melalui seluruh proses
verifikasi. Berbeda dari 9 kode di atas, pesan error ini sengaja tidak digeneralisasi karena
tidak ada risiko kebocoran informasi. Pesan yang spesifik juga membantu mengidentifikasi bahwa
kegagalan terjadi karena gangguan sementara pada sisi TuplePay, bukan karena kredensial yang
tidak valid.