API v1 JSON HTTPS only

KiosMitu API

Dokumentasi teknis untuk mendapatkan data user, melakukan transfer bank/e-wallet, dan membuat custom order. Semua request dan response menggunakan JSON—kecuali QR image yang dapat berupa data URI.

Base URL

https://kiosmitu.com/api/v1

Auth

Bearer token

Dari admin

Mutasi tertentu

Bearer + signature

Nonce 60 detik

Format

application/json

UTF-8

Kredensial API — hubungi admin

API ini tidak menyediakan endpoint login. Untuk mendapatkan access_token dan public_key, hubungi admin KiosMitu. Jangan membuat atau mengirim kredensial sendiri.

  • access_token = Bearer token; rahasia.
  • public_key = dipakai untuk membuat X-Nonce-Hash; diberikan admin.
  • Jangan commit token ke repository atau menaruhnya di frontend publik.

Signature request

Endpoint dengan badge Bearer + Signature wajib mengirim dua header tambahan.

Rumus

string_to_hash = X-Nonce + ":" + public_key
X-Nonce-Hash  = SHA-256(string_to_hash)  // lowercase hex

X-Nonce adalah Unix timestamp dalam detik. Server menolak nonce yang lebih tua dari 60 detik. Buat nonce baru untuk setiap request.

Headers minimum

Authorization: Bearer <access_token>
X-Nonce: 1760000000
X-Nonce-Hash: 8f14e45fceea167a5a36dedd4bea2543...

Contoh PHP

<?php
$publicKey = 'PUBLIC_KEY_DARI_ADMIN';
$token      = 'ACCESS_TOKEN_DARI_ADMIN';
$nonce      = (string) time();
$hash       = hash('sha256', $nonce.':'.$publicKey);

$headers = [
    'Authorization: Bearer '.$token,
    'X-Nonce: '.$nonce,
    'X-Nonce-Hash: '.$hash,
    'Content-Type: application/json',
];

Contoh JavaScript

const publicKey = 'PUBLIC_KEY_DARI_ADMIN';
const token = 'ACCESS_TOKEN_DARI_ADMIN';
const nonce = Math.floor(Date.now() / 1000).toString();
const data = new TextEncoder().encode(nonce + ':' + publicKey);
const digest = await crypto.subtle.digest('SHA-256', data);
const hash = Array.from(new Uint8Array(digest))
  .map(b => b.toString(16).padStart(2, '0')).join('');

const headers = {
  Authorization: 'Bearer ' + token,
  'X-Nonce': nonce,
  'X-Nonce-Hash': hash,
  'Content-Type': 'application/json',
};

Contoh cURL lengkap

NONCE=$(date +%s)
PUBLIC_KEY='PUBLIC_KEY_DARI_ADMIN'
TOKEN='ACCESS_TOKEN_DARI_ADMIN'
HASH=$(printf '%s:%s' "$NONCE" "$PUBLIC_KEY" | shasum -a 256 | awk '{print $1}')

curl -X POST 'https://kiosmitu.com/api/v1/transfer/inquiry' \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Nonce: $NONCE" \
  -H "X-Nonce-Hash: $HASH" \
  -H 'Content-Type: application/json' \
  -d '{"type":"bank","bankcode":"BCA","accountnumber":"1234567890","amount":50000}'

Response jika signature gagal

400 — header tidak lengkap

{"error":"Missing nonce or nonce hash"}

400 — hash salah

{"error":"Invalid nonce"}

400 — nonce kedaluwarsa

{"error":"Nonce expired61","message":"Nonce expired61","data":"..."}

Format response

Envelope standar

{"status":"success","data":{}}

Dipakai transfer dan custom order.

User resource

{"email":"...","balance":150000}

GET /user mengembalikan object langsung, tanpa data.

Validation 422

{"message":"...","errors":{"field":["..."]}}

Signature 400

{"error":"Invalid nonce"}

Tidak memakai envelope status.

GET /api/v1/user Bearer

Data user & saldo

Mengembalikan profil dan saldo user yang sedang login. Tidak butuh signature.

Request

GET /api/v1/user
Authorization: Bearer <access_token>

200 — success

{
  "email": "[email protected]",
  "name": "Nama User",
  "balance": 150000,
  "phone": "081234567890",
  "level": 0,
  "pin_exist": true,
  "verified": true,
  "referral": "REF123",
  "is_updated": 1,
  "last_changed": "2026-11-01 10:00:00",
  "trx_count": 25,
  "ai_kredit": 0
}

401 — unauthenticated

{"message":"Unauthenticated."}

Catatan: di codebase ini endpoint saldo adalah /user, bukan /user/me.

GET/api/v1/transfer/banksBearer

Daftar bank

Daftar bank tujuan transfer. Jika penyedia layanan sedang tidak dapat dihubungi, endpoint fail-soft mengembalikan array kosong.

200 — success

{"status":"success","data":[{"kodeBank":"BCA","namaBank":"Bank Central Asia"}]}

200 — sumber bank kosong

{"status":"success","data":[]}

401 — unauthenticated

{"message":"Unauthenticated."}
GET/api/v1/transfer/ewalletsBearer

Daftar e-wallet

Daftar e-wallet tujuan transfer. Fail-soft seperti daftar bank.

200 — success

{"status":"success","data":[{"kodeBank":"DANA","namaBank":"DANA"}]}

200 — kosong

{"status":"success","data":[]}

401 — unauthenticated

{"message":"Unauthenticated."}
POST/api/v1/transfer/inquiryBearer + Signature

Inquiry transfer

Validasi rekening/ewallet dan kunci nominal. Inquiry hanya berlaku 60 detik.

Headers

Authorization: Bearer <access_token>
X-Nonce: 1760000000
X-Nonce-Hash: <sha256(nonce + ":" + public_key)>
Content-Type: application/json

Body parameter

FieldTipeWajibAturan
typestringyabank atau ewallet
bankcodestringyamax 20, kode dari daftar bank/e-wallet
accountnumberstringyamax 50, spasi dihapus server
amountintegeryaminimal 30.000

Request

{
  "type": "bank",
  "bankcode": "BCA",
  "accountnumber": "1234567890",
  "amount": 50000
}

200 — inquiry berhasil

{
  "status": "success",
  "data": {
    "public_id": "TF-1232026110712000001",
    "accountname": "NAMA PENERIMA",
    "fee": 3500,
    "total": 53500,
    "expires_at": "2026-11-07 12:01:00"
  }
}

Gunakan nilai fee, total, dan expires_at langsung dari response. Saldo harus menutup total.

400 — saldo kurang dari amount

{"status":"fail","message":"Saldo tidak cukup"}

400 — batas 15 transfer processing

{"status":"fail","message":"Maksimal 15 transfer sedang diproses."}

400 — saldo kurang untuk total termasuk fee

{"status":"fail","message":"Saldo tidak cukup untuk total transfer (termasuk fee)."}

400 — pengecekan rekening gagal

{"status":"fail","message":"Inquiry gagal: <response_desc|rd|unknown>"}

400 — signature gagal

{"error":"Missing nonce or nonce hash"}

422 — validasi

{
  "message": "Tipe tujuan wajib diisi. (and 3 more errors)",
  "errors": {
    "type": ["Tipe tujuan wajib diisi."],
    "bankcode": ["Kode bank/ewallet wajib dipilih."],
    "accountnumber": ["Nomor rekening/HP wajib diisi."],
    "amount": ["Nominal wajib diisi."]
  }
}

401 / 429

{"message":"Unauthenticated."}
{"message":"Too Many Attempts."}
POST/api/v1/transfer/payBearer + Signature

Eksekusi transfer

Memotong saldo dan memproses transfer. Gunakan PIN 6 digit yang sama dengan PIN akun.

Body

{"public_id":"TF-...","pin":"123456"}

200 — berhasil masuk processing

{"status":"success","message":"Dalam proses","data":{"public_id":"TF-...","status":"processing"}}

200 — idempotent, sudah pernah diproses

{"status":"success","message":"Sudah diproses","data":{"public_id":"TF-...","status":"success"}}

404 — public_id bukan milik user/tidak ada

{"status":"fail","message":"Transfer tidak ditemukan"}

400 — inquiry kedaluwarsa

{"status":"fail","message":"Inquiry kedaluwarsa (>60 detik). Silakan inquiry ulang."}

400 — saldo tidak cukup / batas processing

{"status":"fail","message":"Saldo tidak cukup"}
{"status":"fail","message":"Maksimal 15 transfer sedang diproses."}

400 — PIN salah

{"message":"Pin Salah. Sisa percobaan: 9."}

429 — PIN terkunci

{"message":"PIN terkunci selama 20 menit karena terlalu banyak kesalahan."}

422 — validasi

{
  "message": "ID transfer wajib diisi. (and 1 more error)",
  "errors": {
    "public_id": ["ID transfer wajib diisi."],
    "pin": ["PIN wajib diisi."]
  }
}

400 signature / 401 / 429 throttle

{"error":"Invalid nonce"}
{"message":"Unauthenticated."}
{"message":"Too Many Attempts."}

PIN salah 10 kali mengunci PIN selama 20 menit. Status akhir transfer bisa dicek lewat detail transfer; status inquiry tidak muncul di list.

GET/api/v1/transfersBearer

Riwayat transfer

Query

GET /api/v1/transfers?search=TF-&status=success&page=1
Authorization: Bearer <access_token>

`search` mencari public_id, bank, nomor rekening, nama rekening, serial, dan payment ref. `status` opsional. Response dipaginasi 20 item.

200 — success

{
  "status": "success",
  "data": [
    {
      "public_id": "TF-...",
      "type": "bank",
      "bankcode": "BCA",
      "bankname": "Bank Central Asia",
      "accountnumber": "1234567890",
      "accountname": "NAMA PENERIMA",
      "amount": 50000,
      "fee": 3500,
      "total": 53500,
      "status": "success",
      "fail_reason": null,
      "serial": "SN123",
      "is_refunded": false,
      "refund_amount": 0,
      "refunded_at": null,
      "saldo_before": 150000,
      "saldo_after": 96500,
      "created_at": "2026-11-07T12:00:00.000000Z"
    }
  ],
  "links": {},
  "meta": {}
}

401

{"message":"Unauthenticated."}
GET/api/v1/transfer/{public_id}Bearer

Detail transfer

200 — success

{"status":"success","data":{"public_id":"TF-...","status":"success","amount":50000,"fee":3500,"total":53500,"serial":"SN123","is_refunded":false}}

404 — tidak ditemukan / masih inquiry

{"status":"fail","message":"Transfer tidak ditemukan"}

401

{"message":"Unauthenticated."}
POST/api/v1/ordercustom/createBearer + Signature

Buat custom order

Membuat tagihan QRIS dan menambahkan saldo user setelah dibayar. QRIS berlaku 15 menit.

Body parameter

FieldTipeWajibAturan
trxidstringyamax 255, unik per user
nominalintegerya1 s.d. 10.000.000
callback_urlstring/nulltidakHTTPS port 443, host publik, tidak IP/private, resolve
detailstring/nulltidakmax 1000

Nominal tagihan

Gunakan nilai price, real_price, dan fee_total langsung dari response.

Request

{
  "trxid": "INV-20261107-001",
  "nominal": 50000,
  "callback_url": "https://example.com/webhook/ordercustom",
  "detail": "Top up saldo custom"
}

200 — QRIS berhasil dibuat

{
  "status": "success",
  "data": {
    "public_id": "OC-...",
    "trxid": "INV-20261107-001",
    "price": 50700,
    "real_price": 50000,
    "fee_total": 700,
    "commercial": false,
    "status": "pending",
    "qr_url": "data:image/png;base64,...",
    "detail": "Top up saldo custom",
    "description": null,
    "created_at": "2026-11-07T12:00:00.000000Z"
  }
}

200 — pembuatan QRIS gagal (HTTP tetap 200)

{
  "status": "fail",
  "message": "Gagal menghubungi payment gateway",
  "data": {
    "public_id": "OC-...",
    "trxid": "INV-20261107-001",
    "price": 50700,
    "real_price": 50000,
    "fee_total": 700,
    "commercial": false,
    "status": "pending",
    "qr_url": null,
    "detail": "Top up saldo custom",
    "description": "Gagal: Gagal menghubungi payment gateway",
    "created_at": "2026-11-07T12:00:00.000000Z"
  }
}

422 — validasi trxid duplikat

{"message":"The trxid has already been taken.","errors":{"trxid":["The trxid has already been taken."]}}

422 — nominal tidak valid

{"message":"The nominal field must be between 1 and 10000000.","errors":{"nominal":["The nominal field must be between 1 and 10000000."]}}

422 — callback URL

{"message":"Callback URL harus https port 443.","errors":{"callback_url":["Callback URL harus https port 443."]}}

Pesan lain: Callback URL terlalu panjang., Callback URL tidak boleh IP., Callback URL tidak bisa di-resolve., Callback URL tidak boleh private range.

400 signature / 401 / 429

{"error":"Nonce expired61","message":"Nonce expired61","data":"..."}
{"message":"Unauthenticated."}
{"message":"Too Many Attempts."}
GET/api/v1/ordercustom/{public_id}Bearer

Cek custom order

Status order dapat berubah menjadi completed setelah pembayaran terdeteksi.

200 — pending

{"status":"success","data":{"public_id":"OC-...","trxid":"INV-...","price":50700,"real_price":50000,"fee_total":700,"commercial":false,"status":"pending","qr_url":"data:image/png;base64,...","detail":"Top up saldo custom","description":null,"created_at":"..."}}

200 — completed

{"status":"success","data":{"public_id":"OC-...","trxid":"INV-...","price":50700,"real_price":50000,"fee_total":700,"commercial":false,"status":"completed","qr_url":"data:image/png;base64,...","detail":"Top up saldo custom","description":"Order Custom: ...","created_at":"..."}}

200 — expired (HTTP tetap 200)

{"status":"fail","message":"QRIS kedaluwarsa","data":{"public_id":"OC-...","status":"expired"}}

200 — underpaid (HTTP tetap 200)

{"status":"fail","message":"Kurang bayar — hubungi admin","data":{"public_id":"OC-...","status":"underpaid"}}

200 — penyedia pembayaran tidak dapat dihubungi

{"status":"success","data":{"public_id":"OC-...","status":"pending"}}

404 — tidak ditemukan / bukan milik user

{"status":"fail","message":"Order tidak ditemukan"}

401 / 429

{"message":"Unauthenticated."}
{"message":"Too Many Attempts."}
POSTcallback_url milik AndaWebhook

Webhook custom order lunas

Dikirim sekali setelah order dibayar. Tidak ada retry otomatis.

Headers

X-Signature: <hmac_sha256>
X-Timestamp: 1760000000
Content-Type: application/json

Payload

{
  "public_id": "OC-...",
  "trxid": "INV-20261107-001",
  "status": "paid",
  "price": 50700,
  "real_price": 50000,
  "fee_total": 700,
  "commercial": false,
  "paid_amount": 50700,
  "paid_at": "2026-11-07T12:00:00+07:00",
  "issued_at": 1760000000,
  "idempotency_key": "oc..."
}

Verifikasi signature

key = SHA-256("ordercustom-webhook-v1:" + public_key)
signature = HMAC-SHA256(raw_request_body, key)

Verifikasi memakai raw body byte-per-byte. Jangan decode JSON lalu encode ulang sebelum verifikasi. Balas 2xx untuk menandai webhook diterima.

Catatan keamanan

  • Callback URL wajib HTTPS, port 443, host publik, dan tidak boleh private range/IP.
  • Webhook tidak di-retry; simpan idempotency key untuk mencegah pemrosesan ganda di sisi penerima.

Error & rate limit

StatusKapanContoh body
400Signature, aturan bisnis, saldo, batas transfer{"status":"fail","message":"..."} atau {"error":"..."}
401Token tidak ada/invalid{"message":"Unauthenticated."}
404Resource tidak ada / bukan milik user{"status":"fail","message":"... tidak ditemukan"}
422Validasi request{"message":"...","errors":{...}}
429Rate limit, PIN lockout{"message":"Too Many Attempts."}

Rate limit

AksiBatas
Transfer inquiry10 / menit
Transfer pay10 / menit
Custom order create30 / menit
Custom order check60 / menit

Rate limit dapat berbeda di masa depan. Jika menerima 429, hormati header Retry-After atau tunggu sebelum mencoba lagi.