Autentikasi
Setiap permintaan membawa bearer token. Tidak ada kredensial lain dan tidak ada bentuk query string.
Authorization: Bearer sdk_live_ab12cd34.<secret>
- Autentikasi
- Key berbentuk sdk_live_ab12cd34.<rahasia>. Bagian sebelum titik mengidentifikasi key; semua setelahnya adalah rahasianya.
- Kami hanya menyimpan hash key. Kalau hilang, cabut dan buat yang baru.
- Key live memindahkan uang nyata. Key test memakai saldo sandbox dan tidak pernah menyentuh penyedia.
- Key tidak punya masa berlaku sendiri. Key bekerja selama langganan akun aktif atau dalam masa tenggang.
- Mencabut key bersifat langsung dan permanen.
Paket
Katalognya sama dengan yang dijual toko publik. Harga dikembalikan dalam satuan minor USD dan IDR; angka IDR adalah yang akan ditagih.
- Harga per paket tergantung tier kamu. Key Reseller melihat harga toko publik. Key Enterprise melihat harga pokok penyedia, tanpa markup.
- Paket DAILY adalah data tak terbatas per hari dengan throttle setelah kuota harian. duration_days-nya hanya placeholder 1; kirim validity_days saat memesan.
- country_count dan countries menjelaskan cakupan. Paket SINGLE mencakup satu negara; REGIONAL dan MULTI mencakup beberapa.
GET/v1/packages
Every purchasable package, priced for your tier.
Parameter
country ISO-2 code · opsionalOnly packages covering this country.coverage SINGLE,MULTI,REGIONAL · opsionalComma-separated coverage kinds.type FIXED | DAILY · opsionalFixed-data or unlimited-per-day plans.sort price_asc | price_desc | volume_desc · opsionalDefault is by name.page integer · opsional1-based. Clamped to the last page.limit integer 1-100 · opsionalDefault 24.
Respons
{
"data": [{
"package_code": string,
"slug": string,
"name": string,
"coverage": "SINGLE" | "MULTI" | "REGIONAL",
"plan_type": "FIXED" | "DAILY",
"duration_days": number,
"volume_bytes": number,
"speed": string | null,
"fup_policy": string | null,
"support_top_up": boolean,
"country_count": number,
"countries": [{ "code": string, "name": string }],
"price": { "usd_minor": number, "idr_minor": number, "currency": "USD" },
"wholesale": boolean
}],
"meta": { "page": number, "perPage": number, "total": number, "totalPages": number }
}
GET/v1/packages/{package_code}
One package by its SKU.
Parameter
Tanpa parameter.
Respons
The same object as one element of `GET /v1/packages`.
Pesanan
Satu pesanan memotong dompet dan menyediakan eSIM dalam satu panggilan. Tidak ada langkah pembayaran yang perlu dipoll: responsnya sudah membawa status pesanan dan eSIM-nya begitu fulfillment cukup cepat.
- Kirim header Idempotency-Key. Percobaan ulang dengan key yang sama mengembalikan pesanan yang sama, bukan membeli dua kali. Key unik per akun, jadi dua developer boleh sama-sama memakai 'order-1'.
- quantity menyediakan sebanyak itu eSIM dalam satu pesanan, dengan satu pemotongan dompet. Maksimal 10.
- Kalau saldo dompet tidak cukup, API menjawab 402 sebelum pesanan apa pun ada, jadi pembelian gagal tidak mengotori daftar pesanan kamu.
- Pembatalan mengikuti aturan toko: eSIM yang belum pernah diaktivasi, di dalam jendela pembatalan. Dompet dikembalikan.
POST/v1/orders
Buy a package. Debits the wallet and provisions an eSIM.
Parameter
Idempotency-Key header · opsionalA retry with the same key returns the same order.
Body permintaan
{
"package_code": "JP-5GB-30D",
"validity_days": 30,
"quantity": 1,
"idempotency_key": "9f2c-order-1"
}
Respons
{
"data": {
"id": string,
"order_number": "SUD-...",
"status": "PAID" | "FULFILLMENT" | "ACTIVE",
"package_code": string,
"quantity": number,
"validity_days": number | null,
"amount_idr_minor": number,
"pricing_mode": "STORE" | "WHOLESALE",
"created_at": string,
"paid_at": string | null,
"esims": [{ "id": string, "iccid": string, "lpa": string, "status": string }]
}
}
GET/v1/orders
Orders created by your integration, newest first.
Parameter
status order status · opsionalPENDING_PAYMENT, PAID, FULFILLMENT, ACTIVE, EXPIRED, CANCELLED, FAILED.page integer · opsional1-based.limit integer 1-100 · opsionalDefault 20.
Respons
A list of order objects, with `meta`.
GET/v1/orders/{id}
One order, by id or order_number.
Parameter
Tanpa parameter.
Respons
POST/v1/orders/{id}/cancel
Cancel an order whose eSIM was never activated.
Parameter
Tanpa parameter.
Respons
The order, now `CANCELLED`.
eSIM
Satu eSIM milik satu paket dan satu pesanan. Membacanya memberi detail pemasangan; aksi siklus hidup mengubah apa yang penyedia lakukan dengannya.
- lpa adalah string aktivasi yang dibaca ponsel dari kode QR, dan qr_code_url adalah gambar hosted dari hal yang sama. pin dan puk hanya dikembalikan saat kamu meminta ?include=secrets.
- status diturunkan dari field penyedia ditambah tanggal kedaluwarsa, jadi bisa saja EXPIRED walau penyedia masih melaporkan eSIM itu terpakai.
- Top up butuh paket yang penyedia tawarkan untuk eSIM ini. Picker di halaman akun dan endpoint ini memakai daftar yang sama, jadi kode dari sana selalu bisa dipakai di sini. Kirim Idempotency-Key dan panggilan ulang mengembalikan top-up yang sudah dibeli.
- suspend dan unsuspend hanya bekerja pada eSIM yang sudah aktif. revoke permanen dan tidak mengembalikan apa pun; pakai cancel di pesanan kalau eSIM belum pernah dipakai.
GET/v1/esims
eSIMs owned by the account.
Parameter
status provider status · opsionalIN_USE, USED_UP, GOT_RESOURCE, CANCEL.page integer · opsional1-based.limit integer 1-100 · opsionalDefault 20.
Respons
{
"data": [{
"id": string,
"iccid": string | null,
"package_code": string,
"status": "READY" | "ACTIVE" | "SUSPENDED" | "EXPIRED" | "USED_UP" | "CANCELLED",
"total_bytes": number,
"used_bytes": number,
"remaining_bytes": number,
"activated_at": string | null,
"expires_at": string | null,
"created_at": string
}],
"meta": { ... }
}
GET/v1/esims/{id}
One eSIM, with its install details.
Parameter
include secrets · opsionalAdds `pin` and `puk`.
Respons
The list shape plus `lpa`, `qr_code_url`, `smdp_status`, `esim_status`, `apn`.
GET/v1/esims/{id}/usage
Data used and remaining, as last measured.
Parameter
Tanpa parameter.
Respons
{
"esim_id": string,
"iccid": string | null,
"total_bytes": number,
"used_bytes": number,
"remaining_bytes": number,
"percent_used": number,
"measured_at": string | null
}
POST/v1/esims/{id}/topups
Buy more data for an eSIM. Debits the wallet.
Parameter
Tanpa parameter.
Body permintaan
{ "package_code": "JP-1GB-7D" }
Respons
{
"id": string,
"esim_id": string,
"package_code": string,
"amount_idr_minor": number,
"status": string,
"created_at": string
}
POST/v1/esims/{id}/suspend
Temporarily suspend an activated eSIM.
Parameter
Tanpa parameter.
Respons
{ "esim_id": string, "status": "SUSPEND" }
POST/v1/esims/{id}/unsuspend
Re-enable a suspended eSIM.
Parameter
Tanpa parameter.
Respons
{ "esim_id": string, "status": "UNSUSPEND" }
POST/v1/esims/{id}/revoke
Permanently revoke an eSIM. No refund.
Parameter
Tanpa parameter.
Respons
{ "esim_id": string, "status": "REVOKE" }
Dompet
Pembelian dipotong dari dompet Sentadu dalam Rupiah. Top up dari halaman akun, lalu belanja lewat API.
- Dompet hanya IDR. Tidak ada mata uang kedua dan tidak ada konversi di endpoint ini.
- Pembelian lewat API tanpa biaya layanan. Biaya yang kamu bayar saat top up sudah menutup transaksi gateway.
- Setiap kredit dan debit muncul di ledger, terbaru dulu, dengan referensi penyebabnya. Key sandbox mendapat daftar kosong, karena uang sandbox tidak masuk ledger.
GET/v1/wallet
Wallet balance in IDR, or the sandbox balance for a TEST key.
Parameter
Tanpa parameter.
Respons
{ "balance_idr_minor": number, "currency": "IDR", "sandbox": boolean }
GET/v1/wallet/transactions
Wallet ledger entries, newest first.
Parameter
limit integer 1-100 · opsionalDefault 50. Always empty for a TEST key.
Respons
{
"data": [{
"id": string,
"amount_idr_minor": number,
"balance_after_idr_minor": number,
"type": "TOPUP" | "PURCHASE" | "REFUND" | "REFERRAL",
"description": string | null,
"reference_type": string | null,
"reference_id": string | null,
"created_at": string
}]
}
Webhook
Kami mengirim POST untuk setiap event yang kamu langganan, dan mencoba ulang endpoint yang gagal selama sehari sebelum menyerah.
order.paidorder.activeorder.failedorder.cancelled
Setiap permintaan membawa X-Sentadu-Signature: t=<unix>,v1=<hex>. Isinya HMAC-SHA256 atas string '<t>.<raw body>', dengan rahasia endpoint kamu sebagai kunci.
X-Sentadu-Signature: t=1750000000,v1=6f1c...
Verifikasi tanda tangan terhadap body mentah, bukan objek yang diserialisasi ulang, dan tolak timestamp lebih tua dari lima menit.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=")),
);
const timestamp = Number(parts.t);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}
Respons non-2xx dicoba ulang setelah 1 menit, 5, 30, 2 jam, dan 12 jam. Setelah lima kegagalan, pengiriman ditandai FAILED dan tampil di dashboard kamu.
Endpoint harus https dan bisa dijangkau dari internet publik. Alamat privat, loopback, dan link-local ditolak.
Sandbox
Key Test menjalankan seluruh API terhadap akun palsu. Katalog dan harganya asli, jadi angka yang kamu lihat sama dengan produksi.
- Pesanan bernomor TST-, eSIM punya ICCID sintetis dan LPA yang menunjuk test.sentadu.com, dan pemasangannya tidak ke mana-mana. Tidak ada panggilan ke penyedia.
- Sandbox mulai dengan Rp 500.000 dan bisa direset dari konsol. Pembelian yang melebihi saldo mengembalikan insufficient_wallet yang sama dengan key live.
- Key test butuh langganan aktif, sama seperti key live.
Batas laju
Batasnya per key, dihitung per menit. Key Reseller mendapat 60 permintaan/menit; Enterprise mulai dari 300 dan bisa menambah.
- Setiap respons membawa X-RateLimit-Limit dan X-RateLimit-Remaining. Penolakan berupa 429 dengan Retry-After.
- Add-on Enterprise bertumpuk dan berakhir bersama tahun langganan. Dibeli dari konsol developer.
- Batasnya dihitung per key, bukan sebagai kumpulan bersama: setiap key dihitung sendiri.
| Tier | Requests/minute | Per year |
|---|
| Reseller | 60 | Rp 150.000 once |
| Enterprise | 300 | Rp 888.000 |
| +300 add-on | 300 | Rp 450.000 |
| +1200 add-on | 1200 | Rp 1.500.000 |
| +2800 add-on | 2800 | Rp 3.000.000 |
Maximum self-serve: 4600 requests/minute.
Error
Setiap kegagalan berupa satu objek dengan kode stabil. Cabangkan pada kode, jangan pada pesan.
Bentuk
{
"error": {
"code": "insufficient_wallet",
"message": "Wallet balance Rp 40.000 does not cover Rp 62.000.",
"details": [{ "path": "package_code", "message": "..." }]
}
}
| Kode | HTTP | |
|---|
| invalid_api_key | 401 | The Authorization bearer token is missing, malformed or revoked. |
| subscription_inactive | 403 | The account's API subscription has expired. Renew it to resume access. |
| rate_limited | 429 | Too many requests in the current window. Respect Retry-After. |
| invalid_request | 400 | The request body or query string failed validation. |
| not_found | 404 | The resource does not exist, or does not belong to this account. |
| insufficient_wallet | 402 | The Sentadu wallet balance does not cover this purchase. |
| package_unavailable | 409 | The package exists but is not available for purchase. |
| esim_not_toplup_able | 409 | This eSIM cannot be topped up (expired or unsupported). |
| esim_not_activatable | 409 | This eSIM is in a state that refuses the requested action. |
| idempotency_conflict | 409 | The same Idempotency-Key was used with a different request body. |
| internal_error | 500 | Something failed on our side. Retry with backoff. |
Siap membangun?
Buat kunci API, pilih paket, dan kirim permintaan pertama dalam beberapa menit.