SentaduSentadu

eSIMs for people who wander. Buy before you fly, be online when you land.

PT. Digital One Global

Tomorowland III Tegal, Central Java ID 52462

VisaMastercardJCBAmerican ExpressDiners Club
PayPalUnionPayQRISE-walletBank VARetail

Product

  • Local
  • Bundles
  • Regional
  • Global
  • Destinations
  • Unlimited
  • Umrah and Hajj

Help

  • How it Works
  • eSIM Internet Routing
  • FAQ
  • Supported Devices
  • Share your story
  • Support

For Partners

  • Dashboard
  • Become a Partner
  • Business Solutions
  • Resellers
  • Docs
  • Agent Skills

Company

  • About
  • Investor (Private)Soon
  • Careers
  • Terms & Conditions
  • Privacy Policy
  • Refund Policy
VisaMastercardJCBAmerican ExpressDiners Club
PayPalUnionPayQRISE-walletBank VARetail

© 2026 PT. Digital One Global. All rights reserved.

Ask Many
Sentadu
Sentadu
LocalBundlesRegionalGlobalDestinationsUnlimited

API documentation

Buy and manage eSIMs programmatically. Fourteen endpoints, one bearer token, Rupiah settlement.

Documentation

  • Quickstart
  • Agent Skills
  • Authentication
  • Packages
  • Orders
  • eSIMs
  • Wallet
  • Webhooks
  • Sandbox
  • Rate limits
  • Errors

Quickstart

Buy API access from your wallet, create a key, then make your first call. The whole flow takes about a minute.

  1. 1Open the developer console and buy a plan. Both plans debit your Sentadu wallet immediately.
  2. 2Create a key. Choose Live for real purchases, or Test to use the sandbox. The full key is shown once.
  3. 3Call the API with the key as a bearer token. Every response is JSON with a data field, or an error object.

Base URL

https://api.sentadu.com/v1

Your first call

curl https://api.sentadu.com/v1/me \
  -H "Authorization: Bearer $SENTADU_API_KEY"
Agent SkillsHand this documentation to your coding agent. One bundle with SKILL.md, references for endpoints, errors, webhooks and sandbox, plus two scripts. Downloadable as .zip or .tar.gz.

Authentication

Every request carries a bearer token. There is no other credential and no query-string form.

Authorization: Bearer sdk_live_ab12cd34.<secret>
Authentication
Keys look like sdk_live_ab12cd34.<secret>. The part before the dot identifies the key; everything after it is the secret.
We store only a hash of the key. If you lose it, revoke it and create a new one.
A live key moves real money. A test key uses the sandbox balance and never touches the provider.
A key has no expiry of its own. It works while the account's subscription is active or in its grace period.
Revoking a key is immediate and permanent.

Packages

The catalogue is the same one the public store sells. Prices are returned in USD minor units and IDR minor units; the IDR figure is what will be charged.

  • What you pay per package depends on your tier. A Reseller key sees the public store price. An Enterprise key sees provider cost, with no markup.
  • A DAILY package is unlimited data per day with a throttle after the daily allowance. Its duration_days is a placeholder of 1; pass validity_days when ordering.
  • country_count and countries describe coverage. A SINGLE plan covers one country; REGIONAL and MULTI cover several.
GET/v1/packages

Every purchasable package, priced for your tier.

Parameters

  • country ISO-2 code · optionalOnly packages covering this country.
  • coverage SINGLE,MULTI,REGIONAL · optionalComma-separated coverage kinds.
  • type FIXED | DAILY · optionalFixed-data or unlimited-per-day plans.
  • sort price_asc | price_desc | volume_desc · optionalDefault is by name.
  • page integer · optional1-based. Clamped to the last page.
  • limit integer 1-100 · optionalDefault 24.

Response

{
  "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.

Parameters

No parameters.

Response

The same object as one element of `GET /v1/packages`.

Orders

An order debits your wallet and provisions an eSIM in one call. There is no payment step to poll: the response already carries the order's status and its eSIMs when fulfilment is fast enough.

  • Send an Idempotency-Key header. A retry with the same key returns the same order instead of buying a second one. Keys are unique per account, so two developers can both use 'order-1'.
  • quantity provisions that many eSIMs under one order, for one wallet debit. It is capped at 10.
  • If the wallet does not cover the price, the API answers 402 before any order exists, so a failed purchase does not clutter your order list.
  • Cancellation follows the same rules as the store: an eSIM that has never been activated, inside the cancellation window. The wallet is refunded.
POST/v1/orders

Buy a package. Debits the wallet and provisions an eSIM.

Parameters

  • Idempotency-Key header · optionalA retry with the same key returns the same order.

Request body

{
  "package_code": "JP-5GB-30D",
  "validity_days": 30,
  "quantity": 1,
  "idempotency_key": "9f2c-order-1"
}

Response

{
  "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.

Parameters

  • status order status · optionalPENDING_PAYMENT, PAID, FULFILLMENT, ACTIVE, EXPIRED, CANCELLED, FAILED.
  • page integer · optional1-based.
  • limit integer 1-100 · optionalDefault 20.

Response

A list of order objects, with `meta`.
GET/v1/orders/{id}

One order, by id or order_number.

Parameters

No parameters.

Response

One order object.
POST/v1/orders/{id}/cancel

Cancel an order whose eSIM was never activated.

Parameters

No parameters.

Response

The order, now `CANCELLED`.

eSIMs

An eSIM belongs to one package and one order. Reading it gives you the install details; the lifecycle actions change what the provider does with it.

  • lpa is the activation string a phone reads from a QR code, and qr_code_url is a hosted image of the same thing. pin and puk are returned only when you ask for ?include=secrets.
  • status is derived from the provider's own fields plus the expiry date, so it can say EXPIRED even while the provider still reports the eSIM as in use.
  • A top-up needs a package the provider offers for this eSIM. The account page's picker and this endpoint use the same list, so a code from there will always work here. Send an Idempotency-Key and a retried call returns the top-up it already bought.
  • suspend and unsuspend work only on an activated eSIM. revoke is permanent and refunds nothing; use cancel on the order instead when the eSIM was never used.
GET/v1/esims

eSIMs owned by the account.

Parameters

  • status provider status · optionalIN_USE, USED_UP, GOT_RESOURCE, CANCEL.
  • page integer · optional1-based.
  • limit integer 1-100 · optionalDefault 20.

Response

{
  "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.

Parameters

  • include secrets · optionalAdds `pin` and `puk`.

Response

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.

Parameters

No parameters.

Response

{
  "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.

Parameters

No parameters.

Request body

{ "package_code": "JP-1GB-7D" }

Response

{
  "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.

Parameters

No parameters.

Response

{ "esim_id": string, "status": "SUSPEND" }
POST/v1/esims/{id}/unsuspend

Re-enable a suspended eSIM.

Parameters

No parameters.

Response

{ "esim_id": string, "status": "UNSUSPEND" }
POST/v1/esims/{id}/revoke

Permanently revoke an eSIM. No refund.

Parameters

No parameters.

Response

{ "esim_id": string, "status": "REVOKE" }

Wallet

Purchases settle from your Sentadu wallet in Rupiah. Top up from the account page, then spend through the API.

  • The wallet is IDR only. There is no second currency and no conversion on this endpoint.
  • API purchases carry no service fee. The fee you paid when topping up already covered the gateway transaction.
  • Every credit and debit appears in the ledger, newest first, with the reference that caused it. A sandbox key gets an empty list, because sandbox money is not ledgered.
GET/v1/wallet

Wallet balance in IDR, or the sandbox balance for a TEST key.

Parameters

No parameters.

Response

{ "balance_idr_minor": number, "currency": "IDR", "sandbox": boolean }
GET/v1/wallet/transactions

Wallet ledger entries, newest first.

Parameters

  • limit integer 1-100 · optionalDefault 50. Always empty for a TEST key.

Response

{
  "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
  }]
}

Webhooks

We POST every event you subscribe to your endpoint, and retry a failing endpoint for a day before giving up.

order.paidorder.activeorder.failedorder.cancelled

Each request carries X-Sentadu-Signature: t=<unix>,v1=<hex>. It is an HMAC-SHA256 over the string '<t>.<raw body>', keyed with your endpoint's secret.

X-Sentadu-Signature: t=1750000000,v1=6f1c...

Verify the signature against the raw request body, not a re-serialised object, and reject a timestamp older than five minutes.

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),
  );
}

A non-2xx response is retried after 1 minute, 5, 30, 2 hours and 12 hours. After five failures the delivery is marked FAILED and shown in your dashboard.

Endpoints must be https and reachable from the public internet. Private, loopback and link-local addresses are refused.

Sandbox

A Test key runs the whole API against a fake account. The catalogue and prices are real, so the numbers you see match production.

  • Orders are TST- numbers, eSIMs have synthetic ICCIDs and an LPA that points at test.sentadu.com, and installs go nowhere. No provider call is made.
  • The sandbox starts with Rp 500.000 and can be reset from the console. A purchase that exceeds it returns the same insufficient_wallet a live key would.
  • A test key needs an active subscription, like a live one.

Rate limits

The limit is per key, counted per minute. A Reseller key gets 60 requests/minute; Enterprise starts at 300 and can buy more.

  • Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. A refusal is 429 with Retry-After.
  • Enterprise add-ons stack and expire with the subscription year. They are bought from the developer console.
  • The limit is shared across your account's keys only through your subscription, not through a global pool: each key is counted separately.
TierRequests/minutePer year
Reseller60Rp 150.000 once
Enterprise300Rp 888.000
+300 add-on300Rp 450.000
+1200 add-on1200Rp 1.500.000
+2800 add-on2800Rp 3.000.000

Maximum self-serve: 4600 requests/minute.

Errors

Every failure is one object with a stable code. Branch on code, never on message.

Shape

{
  "error": {
    "code": "insufficient_wallet",
    "message": "Wallet balance Rp 40.000 does not cover Rp 62.000.",
    "details": [{ "path": "package_code", "message": "..." }]
  }
}
CodesHTTP 
invalid_api_key401The Authorization bearer token is missing, malformed or revoked.
subscription_inactive403The account's API subscription has expired. Renew it to resume access.
rate_limited429Too many requests in the current window. Respect Retry-After.
invalid_request400The request body or query string failed validation.
not_found404The resource does not exist, or does not belong to this account.
insufficient_wallet402The Sentadu wallet balance does not cover this purchase.
package_unavailable409The package exists but is not available for purchase.
esim_not_toplup_able409This eSIM cannot be topped up (expired or unsupported).
esim_not_activatable409This eSIM is in a state that refuses the requested action.
idempotency_conflict409The same Idempotency-Key was used with a different request body.
method_not_allowed405This endpoint does not accept that HTTP method.
internal_error500Something failed on our side. Retry with backoff.

Ready to build?

Create an API key, pick a plan, and send your first request in minutes.

Open Dev AccountCompare plans
LocalBundlesRegionalGlobalDestinationsUnlimited