API Reference
Complete /v1 API surface. This page renders directly from the OpenAPI specification generated during backend builds — paths, parameters, schemas, enums, and error codes match live server behavior with zero manual discrepancies.
Live
https://api.tongkhoesim.com/v1Test mode
https://api-testing.tongkhoesim.com/v1Live: api.tongkhoesim.com / tke_live_ (partners) or tke_member_live_ (approved members). Test: api-testing.tongkhoesim.com / tke_test_ / tke_member_test_ (partners / members).
Universal API Conventions
The following 6 rules apply to all 30 operations; individual endpoint descriptions do not restate them.
Every /v1 endpoint (including GET) signs with HMAC-SHA256 using 4 headers: X-TKE-Key · X-TKE-Timestamp · X-TKE-Nonce · X-TKE-Signature. No unauthenticated bypasses.
Same key + same body within 24h = replays original result without re-charging. Scoped per api_key_id. Mandatory on exactly 5 operations: POST /v1/orders, POST /v1/orders/bulk, POST /v1/esims/{id}/cancel, POST /v1/esims/{id}/change-plan, POST /v1/esims/{id}/refund-requests.
Pagination: Two Distinct Paradigms
Do not write a unified pagination helper for both: they return different response shapes and follow different termination semantics.
Used in exactly one endpoint: GET /v1/webhooks/deliveries. Returns page, page_size, total.
Requests exceeding bounds return items: [] + actual total with HTTP 200, not 404. Stop when items is empty or total items have been processed.
Used in GET /v1/catalog/packages, GET /v1/orders, GET /v1/esims, and GET /v1/esims/{id}/plan-options. GET /v1/orders and GET /v1/esims support pull reconciliation via updated_since; catalog and plan-options do not.
Stop when next_cursor is ABSENT or EQUAL to the current cursor. This field may be omitted entirely in data (type is string | null | undefined). Missing this check creates infinite loops.
Malformed cursors do not error: the server silently resets to the head of the list to avoid breaking existing clients during encoding upgrades. The trade-off is that loops checking next_cursor !== null will spin endlessly on page 1 instead of stopping.
let cursor = null;
const seen = new Set();
while (true) {
const params = new URLSearchParams({ page_size: "200" });
if (cursor) params.set("cursor", cursor);
// callSigned = your helper adding 4 X-TKE-* headers (see HMAC page).
const page = await callSigned("GET", `/v1/orders?${params}`);
for (const order of page.data.items) await handle(order);
const next = page.data.next_cursor;
if (!next || next === cursor || seen.has(next)) break;
seen.add(next);
cursor = next;
}Catalog
The package catalog: ONE surface for both supply systems. The supply system is a FIELD (`market`: `domestic` | `international`) and a filter (`?market=`), no longer two separate paths. The catalog carries NO stock concept (no figure, no `available`/`low`/`out_of_stock` level): packages simply appear here, and ordering one the supplier cannot fulfill returns a clear error from `POST /v1/orders` (`esim.out_of_stock`…) instead of the catalog announcing it upfront. One cursor pagination scheme. `total` is present only when counting is cheap (filtering to the domestic system); otherwise it is `null`.
/v1/catalog/packagesList packages
catalog:readget_catalog_packagesRequired scope: catalog:read.
Your organization's package catalog: ONE surface for both supply systems, filtered with ?market=.
It only returns packages you can ACTUALLY BUY: published, still distributed by the supplier, within your key's catalog restrictions, AND already priced for your organization. An unpriced package does not appear, not to hide it but because you could not order it.
Cursor pagination: call it first without cursor, then pass next_cursor back verbatim until it is null. A page SHORTER than page_size does NOT mean the end (see the cursor parameter).
capabilities comes from each package's REAL declaration, never from a guess: a package with nothing declared reports every flag as false. We would rather omit a button you could have used than promise an endpoint that is certain to return 422.
The catalog carries NO stock concept: no figure, no in-stock/sold-out level. Packages simply appear here; ordering one the supplier cannot fulfill returns a clear error right at order time from POST /v1/orders (esim.out_of_stock…), instead of making you infer it from a field shown beforehand.
Query Parameters (18)
page_sizeintegeroptionaldefault 50min 1max 100Records per page. Default 50, capped at 100 — sending a larger value is clamped to 100 (NOT an error).
marketstringoptionalSupply system: domestic | international. A value outside the list returns internal.invalid_filter (400) rather than the whole catalog: one typo must not read as "this filter excludes nothing".
countrystringoptionalCountry code (ISO-3166 alpha-2, e.g. VN). Only packages covering this country.
regionstringoptionalRegion slug (e.g. chau-a). Packages covering at least one country of that region, plus every global package.
qstringoptionalFree-text search over the public code and the package name (partial, case-insensitive).
tagstringoptionalFilter by ONE commercial tag. Tags are normalized exactly as they were stored (extra spaces removed, lowercased, hyphenated), so Ban Le and ban-le are the same tag.
data_modelstringoptionalData model: daily | fixed | unlimited. REPLACES the old supports_topup. An unknown value returns internal.invalid_filter (400).
data_min_bytesinteger · int64optionalMinimum data allowance in BYTES. Per-day packages are compared on their PER-DAY allowance; unlimited packages ALWAYS pass a lower bound.
data_max_bytesinteger · int64optionalMaximum data allowance in BYTES. Unlimited packages are EXCLUDED: a data ceiling cannot apply to an unlimited package, and letting it through would hand an unlimited plan to someone looking for the smallest one.
days_mininteger · int64optionalMinimum validity in days.
days_maxinteger · int64optionalMaximum validity in days.
price_mininteger · int64optionalMinimum price (VND), compared against YOUR ORGANIZATION'S price. This filter runs AFTER the server has cut the page, so it can shorten a page without meaning the data ran out.
price_maxinteger · int64optionalMaximum price (VND), same rules as price_min.
network_genstringoptionalOnly packages that run on this network generation (2g | 3g | 4g | 5g). An unknown value returns internal.invalid_filter (400).
hotspotstringoptionaltruefalsetrue = only packages DECLARED to have Wi-Fi tethering; false = only those DECLARED not to. A package with nothing declared (features.hotspot = null) matches NEITHER side — the null rule applies to filters exactly as it does to responses. Accepts only the two strings true/false.
appstringoptionalOnly packages DECLARED to run this app on at least one operating system (app=tiktok). A package that declares nothing about that app does NOT match — silence is not a promise. Only tiktok is accepted today; any other code returns internal.invalid_filter (400).
voicestringoptionaltruefalsetrue = only packages DECLARED to have voice; false = only those DECLARED not to. A package with nothing declared matches neither side. Accepts only the two strings true/false.
cursorstringoptionalCursor for the next page: pass the previous page's next_cursor back VERBATIM.
Stop when next_cursor is null; do NOT stop on a short page. Three gates run AFTER the server has cut the page (your key's catalog restrictions, the package must be priced for your organization, and the price-range filter), so a page returning 3 items may still have thousands of packages behind it.
The cursor carries a FINGERPRINT of the filters that produced it. Sending it alongside different filters (or sending a corrupted string) returns catalog.cursor_mismatch (400) instead of quietly resuming a different query: stitching two pages from two filter sets together produces a catalog that never existed, and no layer can detect that afterwards.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
items | object[] | — | |
capabilities | object | What this package can do AFTER the sale. Read from each package's REAL declaration, never inferred from the supplier type: a package with nothing declared reports every flag as | |
cancel | object | — | |
restriction | string | nullable |
code_not_disclosednull |
supported | boolean | — | |
plan_change | object | — | |
policy | object | nullable | How TOPPING UP A NEW PLAN works at this supplier. |
… | … | Deeper nested fields: see the JSON example. | |
supported | boolean | — | |
usage_check | object | — | |
supported | boolean | — | |
code | string | The package code from YOUR catalog, i.e. the | |
coverage | object | Where the package works. | |
countries | string[] | ISO-3166 alpha-2 codes. Empty for global packages (we do not enumerate 200 countries) and for packages with no declared coverage. | |
country_count | integer | How many countries the package covers — so you can sort or filter on "multi-country packages" without fetching everything and counting the array yourself. | |
group | string | Whether the package covers a list of countries ( | |
region | string | nullable | ALWAYS |
data | object | The package's data allowance. EVERY figure is in BYTES (not MB/GB — it avoids rounding twice). | |
daily_bytes | integer · int64 | nullable | PER-DAY data allowance, in bytes. |
label | string | A human-readable label, generated by the same function every one of our screens uses. | |
model | string | How the allowance is counted: dailyfixedunlimited | |
reset | object | nullable | When the daily allowance resets. A per-day package whose reset point is unknown costs a customer who buys at 22:00 almost the whole first day — this is sales information, not a technical detail. |
at | string | nullable | Reset time in the local time of |
note | string | nullable | — |
timezone | string | nullable | Time zone of the reset point ( |
total_bytes | integer · int64 | nullable | Total allowance for the whole period, in bytes. |
description | string | nullable | The sales description of the package. |
description_en | string | nullable | English description. |
features | object | What the package can and cannot do. A TYPED object rather than an array of codes: | |
apps | object[] | Per-OPERATING-SYSTEM app status. An app NOT PRESENT in this array means WE HAVE NO INFORMATION, NOT "it works". Only what is listed is something the supplier commits to; today only TikTok is declared. There is no combined field, and that is deliberate: measured on the real catalog, 2,465 packages run TikTok on iPhone but NOT on Android. A single combined flag flattens them to "no", and you would then be wrong about nearly a third of the catalog. | |
android | string |
supportedblockedunknown | |
app | string | App code, lowercase (e.g. | |
ios | string |
supportedblockedunknown | |
note | string | nullable | — |
hotspot | boolean | nullable | Wi-Fi tethering for other devices. |
kyc_required | boolean | nullable | Identity verification (KYC) is required before use. |
sms | boolean | nullable | SMS. |
voice | boolean | nullable | Voice calls. |
fup | object | nullable | What happens after the high-speed allowance runs out. |
behavior | string | nullable |
throttlecutoffnull |
high_speed_bytes | integer · int64 | nullable | BYTES at full speed before throttling kicks in. |
note | string | nullable | — |
speed_kbps | integer | nullable | Speed AFTER the high-speed allowance runs out (kbps). |
market | string | The package's SUPPLY SYSTEM, an OPEN ENUM (clients MUST ignore unknown values). This is the axis that decides whether a package has stock behind it and which post-sale operations it has: domesticinternational | |
name | string | — | |
name_en | string | nullable | English name. |
network | object | Which networks the package runs on. | |
apn | string | nullable | The APN that must be entered by hand on some Android devices that do not pick it up automatically. Without it you cannot build an installation guide, and every such case becomes a support call. |
carriers | object[] | The carriers this package RUNS ON. This is an eSIM specification (whose radio it uses), not the supplier. | |
name | string | — | |
generations | string[] | nullable | Network generations the package can use ( |
speed_cap_kbps | integer | nullable | Speed cap for the WHOLE package (kbps) — different from |
price | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
tags | string[] | Normalized commercial tags (lowercase, hyphenated). They are also what your key's catalog restrictions match on. NO package carries a tag today, so | |
validity | object | How long the package lasts, and when the clock starts. | |
days | integer | — | |
install_by_days | integer | nullable | Number of days to scan the QR / install the eSIM after purchase. |
label | string | — | |
reclaim_on_expiry | boolean | nullable | The supplier RECLAIMS the subscription when the plan expires — meaning it can no longer be topped up or changed. If you sell renewals you must know that window. |
starts_when | string | nullable | WHEN the clock starts: The HEAVIEST field in this block. If the supplier counts from INSTALL and you do not know it, you will quote the wrong expiry date to EVERY customer. on_installon_purchasenull |
unused_valid_days | integer | nullable | Number of days the package can sit unused on the shelf before it expires. |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | Total packages matching the filters. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filtercatalog.cursor_mismatchpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/catalog/packages" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"capabilities": {
"cancel": {
"restriction": "code_not_disclosed",
"supported": true
},
"plan_change": {
"policy": {
"note": "<note>",
"requires_cancel_first": true,
"stacking": true
},
"supported": true
},
"usage_check": {
"supported": true
}
},
"code": "TKE-VN-30D-X3K9F",
"coverage": {
"countries": [
"CN"
],
"country_count": 1,
"group": "country",
"region": "<region>"
},
"data": {
"daily_bytes": 1073741824,
"label": "1GB/ngày",
"model": "daily",
"reset": {
"at": "00:00",
"note": "<note>",
"timezone": "UTC+8"
},
"total_bytes": null
},
"description": "<description>",
"description_en": "<description_en>",
"features": {
"apps": [
{
"android": "blocked",
"app": "tiktok",
"ios": "supported",
"note": "<note>"
}
],
"hotspot": true,
"kyc_required": false,
"sms": false,
"voice": false
},
"fup": {
"behavior": "throttle",
"high_speed_bytes": 1048576,
"note": "<note>",
"speed_kbps": 128
},
"market": "international",
"name": "eSIM Trung Quốc Mainland Multi - 1GB/ngày - 24 ngày",
"name_en": "<name_en>",
"network": {
"apn": "e-ideas",
"carriers": [
{
"name": "China Unicom"
}
],
"generations": [
"4g",
"5g"
],
"speed_cap_kbps": 1
},
"price": {
"amount": 273000,
"currency": "VND"
},
"tags": [],
"validity": {
"days": 24,
"install_by_days": 365,
"label": "24 ngày",
"reclaim_on_expiry": true,
"starts_when": "on_install",
"unused_valid_days": 1
}
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/catalog/packages/{code}Get one package
catalog:readget_catalog_packages__code_Required scope: catalog:read.
Details of EXACTLY ONE package by its public code. data carries the SAME FIELD SET as an element of GET /v1/catalog/packages, plus mode — no field exists only on the detail endpoint, so you write exactly one type for Package.
A code you cannot see → partner.package_not_visible (404). ONE code for every cause (the code does not exist · it is outside your key's catalog restrictions · your organization has no price for it · the package lost its supply link): being able to tell them apart would hand a stranger a way to enumerate the whole catalog.
Path Parameters (1)
codestringrequiredThe package code, i.e. the code you see in the catalog (public_sku). IMMUTABLE per package: it does not change when we switch the supplier behind it, so you can store it as a reconciliation key. A code outside your key's catalog restrictions, or a code your organization has no price for, returns 404 partner.package_not_visible exactly like a code that does not exist.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.package_not_visiblepartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
CODE="THAY-BANG-CODE"
METHOD="GET"
REQ_PATH="/v1/catalog/packages/$CODE" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": null,
"ok": true
}Orders
Place orders + reconcile
/v1/ordersList orders (pull-based reconciliation)
orders:readget_ordersRequired scope: orders:read.
Query Parameters (5)
page_sizeintegeroptionaldefault 50min 1max 200Records per page. Default 50, capped at 200 — sending a larger value is clamped to 200 (NOT an error).
statusstringoptionalcompletedpending_providerprovider_failedcancelledFilter by order status. A value outside the list → internal.invalid_filter (400).
external_refstringoptionalFilter by the reference code you sent at commit time (EXACT match).
updated_sincestring · date-timeoptionalISO-8601 UTC. Returns every record with updated_at >= updated_since, sorted ascending — this is the standard PULL-BASED RECONCILIATION path (§8.5): even a missed webhook can still be reconstructed from state. Wrong format → internal.invalid_filter (400).
cursorstringoptionalCursor for the next page — pass back the next_cursor value from the previous page VERBATIM. WARNING: a malformed cursor does NOT raise an error; the server just reads from the START of the list again (a deliberate choice so that changing the cursor encoding scheme never breaks a partner's running reconciliation loop). Consequence to know: an automated reconciliation loop that keeps sending a garbage cursor will loop forever on the first page instead of stopping — check that next_cursor differs from the cursor you just sent before looping again.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
items | object[] | — | |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
fulfillment_status | string | —fulfilledpartially_fulfilledunfulfilled | |
id | string · uuid | — | |
item_count | integer · int64 | — | |
net | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
public_ref | string | nullable | — |
refunded | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
status | string | — | |
total | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
updated_at | string · date-time | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | How many rows match your filter. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filterpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/orders" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"created_at": "2026-07-11T08:12:30Z",
"external_ref": "po-1001",
"fulfillment_status": "fulfilled",
"id": "018f2b00-0000-7000-8000-00000000009a",
"item_count": 2,
"net": {
"amount": 273000,
"currency": "VND"
},
"public_ref": "TKE-DH-2A4C7Q",
"refunded": {
"amount": 273000,
"currency": "VND"
},
"status": "completed",
"total": {
"amount": 273000,
"currency": "VND"
},
"updated_at": "2026-07-11T09:03:11Z"
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/ordersCreate order (direct purchase)
orders:writeIdempotency-Key Requiredpost_ordersRequired scope: orders:write.
Headers (1)
Idempotency-KeystringrequiredA client-generated string — the same key + body executes exactly once (a retry returns the original result). Scoped by api_key_id (§11): two different keys = two separate commands.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
external_ref | string | nullable | Your own reference code — echoed in the response and in webhooks. |
include | string[] | nullable | Ask for extra blocks in the response: RETURNING THE CODE IS DISCLOSING IT: asking for |
invoice_buyer | object | nullable | Member keys only. Omitted data uses the consumer flow without an organization or Vietnamese legal entity. Supplied data follows the existing personal validator: full_name, a 12-digit personal tax_code, and address/email under checkout rules. Business invoices are requested after purchase. Test mode only validates the snapshot and never issues MISA invoices. |
address | string | nullable | — |
email | string | nullable | — |
full_name | string | nullable | — |
legal_name | string | nullable | — |
tax_code | string | nullable | — |
items | object[] | required | List of packages and quantities to buy (maximum 50 eSIMs). |
package_code | string | required | The package code from YOUR catalog, i.e. the |
quantity | integer | required | Quantity of eSIMs to buy for this package (≥ 1). |
Response
A replay of a previously committed command: same Idempotency-Key + same body within 24h. The body is IDENTICAL to the first call, money is NOT charged a second time, and the header Idempotent-Replayed: true is added. A NEW commit returns 201 — don't branch on success by status code.
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Partner order. |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
fulfillment_status | string | How much was actually delivered — a SEPARATE axis from fulfilledpartially_fulfilledunfulfilled | |
id | string · uuid | The order ID (UUID). | |
idempotent_replay | boolean | nullable | true if this request was an idempotent replay of an earlier success. |
invoice_no | string | nullable | Invoice number once issued. |
invoice_status | string | The order's VAT invoice status. nonependingissuingissuedfailedcancelled | |
items | object[] | — | |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
money_settled | boolean | Whether the order's money has settled (no refund still in progress). | |
net | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
public_ref | string | nullable | A short, human-readable order code — use it when opening a support ticket. |
refunded | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
status | string | —completedpending_providerprovider_failedcancelled | |
total | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
ok | boolean | required | —true |
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Partner order. |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
fulfillment_status | string | How much was actually delivered — a SEPARATE axis from fulfilledpartially_fulfilledunfulfilled | |
id | string · uuid | The order ID (UUID). | |
idempotent_replay | boolean | nullable | true if this request was an idempotent replay of an earlier success. |
invoice_no | string | nullable | Invoice number once issued. |
invoice_status | string | The order's VAT invoice status. nonependingissuingissuedfailedcancelled | |
items | object[] | — | |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
money_settled | boolean | Whether the order's money has settled (no refund still in progress). | |
net | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
public_ref | string | nullable | A short, human-readable order code — use it when opening a support ticket. |
refunded | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
status | string | —completedpending_providerprovider_failedcancelled | |
total | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filterpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedwallet.insufficient_balancepartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.package_not_visibleesim.out_of_stockidempotency.conflictinvoice.buyer_invalidpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/orders" # them query thi noi thang vao day TRUOC khi ky
BODY='{"external_ref":"po-1001","include":["lpa"],"invoice_buyer":{"address":"<address>","email":"<email>","full_name":"<full_name>","legal_name":"<legal_name>","tax_code":"TKE-VN-30D-X3K9F"},"items":[{"package_code":"TKE-VN-30D-X3K9F","quantity":2}]}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"created_at": "2026-07-11T08:12:30Z",
"external_ref": "po-1001",
"fulfillment_status": "fulfilled",
"id": "018f2b00-0000-7000-8000-00000000009a",
"idempotent_replay": true,
"invoice_no": "<invoice_no>",
"invoice_status": "none",
"items": [
{
"cancellable": {
"allowed": true,
"disclosed_at": "2026-07-11T08:12:30Z",
"reason": "code_disclosed"
},
"external_ref": "po-1001",
"iccid": "8984012345678901234",
"id": "018f2b00-0000-7000-8000-00000000009a",
"install": {
"android_url": "https://esimsetup.google.com/esim_qrcode_provisioning?data=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"apn": "<apn>",
"ios_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"lpa": "LPA:1$smdp.example.com$MATCH-ABC123",
"matching_id": "MATCH-ABC123",
"qr_code_base64": "<PNG base64 — đã rút gọn>",
"smdp_address": "smdp.example.com"
},
"msisdn": "+84900000000",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"package_name": "Tên gói hiển thị",
"plan": {
"current_code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"smdp_status": "released",
"status": "delivered",
"updated_at": "2026-07-11T09:03:11Z",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
}
],
"mode": "live",
"money_settled": true,
"net": {
"amount": 273000,
"currency": "VND"
},
"public_ref": "TKE-DH-2A4C7Q",
"refunded": {
"amount": 273000,
"currency": "VND"
},
"status": "completed",
"total": {
"amount": 273000,
"currency": "VND"
}
},
"ok": true
}Sample Request Body
{
"external_ref": "po-1001",
"include": [
"lpa"
],
"invoice_buyer": {
"address": "<address>",
"email": "<email>",
"full_name": "<full_name>",
"legal_name": "<legal_name>",
"tax_code": "TKE-VN-30D-X3K9F"
},
"items": [
{
"package_code": "TKE-VN-30D-X3K9F",
"quantity": 2
}
]
}/v1/orders/{id}Order details + eSIMs
orders:readget_orders__id_Required scope: orders:read.
Path Parameters (1)
idstringrequiredThe order ID (UUID).
Query Parameters (1)
includestringoptionalAsk for extra blocks, comma-separated. lpa = return the install block (the eSIM activation code); qr = also return install.qr_code_base64 (detail endpoints only).
RETURNING THE CODE IS DISCLOSING IT. Asking for lpa PERMANENTLY closes the cancellation window of every eSIM in the response (cancellable.allowed becomes false, reason = code_disclosed) — once the code has left our systems it cannot be recalled. Ask for it when you deliver to the customer right away; skip it when you order ahead and deliver later, and you keep the right to cancel. An unknown value → internal.invalid_filter (400, field: "include").
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Partner order. |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
fulfillment_status | string | How much was actually delivered — a SEPARATE axis from fulfilledpartially_fulfilledunfulfilled | |
id | string · uuid | The order ID (UUID). | |
idempotent_replay | boolean | nullable | true if this request was an idempotent replay of an earlier success. |
invoice_no | string | nullable | Invoice number once issued. |
invoice_status | string | The order's VAT invoice status. nonependingissuingissuedfailedcancelled | |
items | object[] | — | |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
money_settled | boolean | Whether the order's money has settled (no refund still in progress). | |
net | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
public_ref | string | nullable | A short, human-readable order code — use it when opening a support ticket. |
refunded | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
status | string | —completedpending_providerprovider_failedcancelled | |
total | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filterpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/orders/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"created_at": "2026-07-11T08:12:30Z",
"external_ref": "po-1001",
"fulfillment_status": "fulfilled",
"id": "018f2b00-0000-7000-8000-00000000009a",
"idempotent_replay": true,
"invoice_no": "<invoice_no>",
"invoice_status": "none",
"items": [
{
"cancellable": {
"allowed": true,
"disclosed_at": "2026-07-11T08:12:30Z",
"reason": "code_disclosed"
},
"external_ref": "po-1001",
"iccid": "8984012345678901234",
"id": "018f2b00-0000-7000-8000-00000000009a",
"install": {
"android_url": "https://esimsetup.google.com/esim_qrcode_provisioning?data=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"apn": "<apn>",
"ios_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"lpa": "LPA:1$smdp.example.com$MATCH-ABC123",
"matching_id": "MATCH-ABC123",
"qr_code_base64": "<PNG base64 — đã rút gọn>",
"smdp_address": "smdp.example.com"
},
"msisdn": "+84900000000",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"package_name": "Tên gói hiển thị",
"plan": {
"current_code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"smdp_status": "released",
"status": "delivered",
"updated_at": "2026-07-11T09:03:11Z",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
}
],
"mode": "live",
"money_settled": true,
"net": {
"amount": 273000,
"currency": "VND"
},
"public_ref": "TKE-DH-2A4C7Q",
"refunded": {
"amount": 273000,
"currency": "VND"
},
"status": "completed",
"total": {
"amount": 273000,
"currency": "VND"
}
},
"ok": true
}/v1/orders/{id}/resendResend the order's email/QR
orders:writepost_orders__id__resendRequired scope: orders:write.
Path Parameters (1)
idstringrequiredThe order ID (UUID).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
id | string · uuid | ID of the order that was just re-sent. | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
resent_at | string · date-time | — | |
resent_to | string | The recipient address, PARTIALLY MASKED — the partner surface never replays the customer's full email. | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedorder.not_deliverablepartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/orders/$ID/resend" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"id": "018f2b00-0000-7000-8000-00000000009a",
"mode": "live",
"resent_at": "2026-07-11T08:12:30Z",
"resent_to": "ab***@x.com"
},
"ok": true
}/v1/orders/bulkPlace a multi-line batch (one order per line)
orders:writeIdempotency-Key Requiredpost_orders_bulkRequired scope: orders:write.
Place a multi-line batch: each line creates ONE independent order through the same quote→commit path as POST /v1/orders.
PARTIAL SUCCESS: failed lines do not block others, and only successful lines are charged. Business errors (stock, balance, unavailable package, abuse limits) appear in lines[].error without changing the batch HTTP status. If the balance runs out, later lines are still attempted because a cheaper line may succeed.
Status is always 200: the response mixes per-line results rather than creating one resource, and the batch finishes within the request, so neither 201 nor 202 plus polling applies.
Retry or interruption: the SAME Idempotency-Key and SAME body reload the old batch, preserve completed line results, and run only remaining pending lines, without another charge. If no line ran, the response adds Idempotent-Replayed: true. Same key with another body returns idempotency.conflict.
Repeating the same cart within 10 minutes: identical batch content from one organization is treated as an accidental repeat, even with different Idempotency-Key values. The old batch is returned with Idempotent-Replayed: true, without another charge. To intentionally order an identical cart for a second customer, change external_ref: it belongs to the signed body, makes the commands distinct, and identifies each order during reconciliation. A completely failed batch (no orders created) is never blocked, and after 10 minutes a new order is allowed.
Per-line external_ref is echoed in lines[].external_ref; the batch reference is echoed at the response root.
The response contains NO LPA/QR. Fetch eSIMs through GET /v1/orders/{order_id} for each order_id.
Headers (1)
Idempotency-KeystringrequiredA client-generated string — the same key + body executes exactly once (a retry returns the original result). Scoped by api_key_id (§11): two different keys = two separate commands.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
external_ref | string | nullable | Reference code for the WHOLE batch — echoed in the batch response. |
items | object[] | required | Each element creates ONE independent order. The line-count cap is configurable (default 100) — exceeding it → |
external_ref | string | nullable | Reference code for THIS line only — echoed verbatim in the line result. |
package_code | string | required | The package code from YOUR catalog, i.e. the |
quantity | integer | required | — |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Result of a batch order. PARTIAL-SUCCESS: failed lines do not block other lines; money is ONLY charged for |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
failed_lines | integer | — | |
id | string · uuid | — | |
items | object[] | — | |
charged | object | nullable | — |
error | object | nullable | A real |
code | string | — | |
message | string | — | |
external_ref | string | nullable | — |
line_no | integer | 1-based, in the exact order submitted. | |
order_id | string · uuid | nullable | — |
package_code | string | — | |
quantity | integer | — | |
status | string | —pendingdonefailed | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
status | string |
pendingcompletedpartialfailed | |
succeeded_lines | integer | — | |
total_charged | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
total_lines | integer | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.package_not_visibleidempotency.conflictinternal.limit_exceededpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/orders/bulk" # them query thi noi thang vao day TRUOC khi ky
BODY='{"external_ref":"batch-2026-001","items":[{"external_ref":"po-1001","package_code":"TKE-VN-30D-X3K9F","quantity":5}]}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"created_at": "2026-07-11T08:12:30Z",
"external_ref": "batch-2026-001",
"failed_lines": 1,
"id": "018f2b00-0000-7000-8000-00000000009a",
"items": [
{
"charged": null,
"error": {
"code": "TKE-VN-30D-X3K9F",
"message": "<message>"
},
"external_ref": "po-1001",
"line_no": 1,
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"quantity": 2,
"status": "pending"
}
],
"mode": "live",
"status": "pending",
"succeeded_lines": 1,
"total_charged": {
"amount": 273000,
"currency": "VND"
},
"total_lines": 1
},
"ok": true
}Sample Request Body
{
"external_ref": "batch-2026-001",
"items": [
{
"external_ref": "po-1001",
"package_code": "TKE-VN-30D-X3K9F",
"quantity": 5
}
]
}/v1/orders/bulk/{id}Result of a batch order
orders:readget_orders_bulk__id_Required scope: orders:read.
Re-read per-line results of a placed bulk batch — recovery path when original response is lost without resending body. Batch belonging to other org or other environment (live ⇄ testmode) → 404.
Path Parameters (1)
idstringrequiredThe batch ID (UUID) returned by POST /v1/orders/bulk.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Result of a batch order. PARTIAL-SUCCESS: failed lines do not block other lines; money is ONLY charged for |
created_at | string · date-time | — | |
external_ref | string | nullable | — |
failed_lines | integer | — | |
id | string · uuid | — | |
items | object[] | — | |
charged | object | nullable | — |
error | object | nullable | A real |
code | string | — | |
message | string | — | |
external_ref | string | nullable | — |
line_no | integer | 1-based, in the exact order submitted. | |
order_id | string · uuid | nullable | — |
package_code | string | — | |
quantity | integer | — | |
status | string | —pendingdonefailed | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
status | string |
pendingcompletedpartialfailed | |
succeeded_lines | integer | — | |
total_charged | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
total_lines | integer | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/orders/bulk/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"created_at": "2026-07-11T08:12:30Z",
"external_ref": "batch-2026-001",
"failed_lines": 1,
"id": "018f2b00-0000-7000-8000-00000000009a",
"items": [
{
"charged": null,
"error": {
"code": "TKE-VN-30D-X3K9F",
"message": "<message>"
},
"external_ref": "po-1001",
"line_no": 1,
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"quantity": 2,
"status": "pending"
}
],
"mode": "live",
"status": "pending",
"succeeded_lines": 1,
"total_charged": {
"amount": 273000,
"currency": "VND"
},
"total_lines": 1
},
"ok": true
}eSIMs
eSIM records, usage, top-up, cancellation
/v1/esimsList eSIMs (pull-based reconciliation)
esims:readget_esimsRequired scope: esims:read.
Query Parameters (8)
page_sizeintegeroptionaldefault 50min 1max 200Records per page. Default 50, capped at 200 — sending a larger value is clamped to 200 (NOT an error).
statusstringoptionalpendingavailablereservedsolddeliveredin_useused_upexpiredsuspendedcancelledprovider_failedFilter by esim_status. A value outside the list → internal.invalid_filter (400) — it does NOT return an empty list, so a typo (e.g. canceled missing an l) isn't misread as "this customer has no eSIM in that state".
order_idstring · uuidoptionalOnly eSIMs belonging to one order.
iccidstringoptionalFilter by ICCID (string, exact match).
external_refstringoptionalEvery eSIM of the order(s) carrying this order reference of yours (the external_ref sent when ordering, exact match).
updated_sincestring · date-timeoptionalISO-8601 UTC. Returns every record with updated_at >= updated_since, sorted ascending — this is the standard PULL-BASED RECONCILIATION path (§8.5): even a missed webhook can still be reconstructed from state. Wrong format → internal.invalid_filter (400).
cursorstringoptionalCursor for the next page — pass back the next_cursor value from the previous page VERBATIM. WARNING: a malformed cursor does NOT raise an error; the server just reads from the START of the list again (a deliberate choice so that changing the cursor encoding scheme never breaks a partner's running reconciliation loop). Consequence to know: an automated reconciliation loop that keeps sending a garbage cursor will loop forever on the first page instead of stopping — check that next_cursor differs from the cursor you just sent before looping again.
includestringoptionalAsk for extra blocks, comma-separated. lpa = return the install block (the eSIM activation code); qr = also return install.qr_code_base64 (detail endpoints only).
RETURNING THE CODE IS DISCLOSING IT. Asking for lpa PERMANENTLY closes the cancellation window of every eSIM in the response (cancellable.allowed becomes false, reason = code_disclosed) — once the code has left our systems it cannot be recalled. Ask for it when you deliver to the customer right away; skip it when you order ahead and deliver later, and you keep the right to cancel. An unknown value → internal.invalid_filter (400, field: "include").
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
items | object[] | — | |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | How many rows match your filter. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filterpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/esims" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"cancellable": {
"allowed": true,
"disclosed_at": "2026-07-11T08:12:30Z",
"reason": "code_disclosed"
},
"external_ref": "po-1001",
"iccid": "8984012345678901234",
"id": "018f2b00-0000-7000-8000-00000000009a",
"install": {
"android_url": "https://esimsetup.google.com/esim_qrcode_provisioning?data=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"apn": "<apn>",
"ios_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"lpa": "LPA:1$smdp.example.com$MATCH-ABC123",
"matching_id": "MATCH-ABC123",
"qr_code_base64": "<PNG base64 — đã rút gọn>",
"smdp_address": "smdp.example.com"
},
"msisdn": "+84900000000",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"package_name": "Tên gói hiển thị",
"plan": {
"current_code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"smdp_status": "released",
"status": "delivered",
"updated_at": "2026-07-11T09:03:11Z",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/esims/{id}Get one eSIM
esims:readget_esims__id_Required scope: esims:read.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Query Parameters (1)
includestringoptionalAsk for extra blocks, comma-separated. lpa = return the install block (the eSIM activation code); qr = also return install.qr_code_base64 (detail endpoints only).
RETURNING THE CODE IS DISCLOSING IT. Asking for lpa PERMANENTLY closes the cancellation window of every eSIM in the response (cancellable.allowed becomes false, reason = code_disclosed) — once the code has left our systems it cannot be recalled. Ask for it when you deliver to the customer right away; skip it when you order ahead and deliver later, and you keep the right to cancel. An unknown value → internal.invalid_filter (400, field: "include").
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedinternal.invalid_filterpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/esims/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"cancellable": {
"allowed": true,
"disclosed_at": "2026-07-11T08:12:30Z",
"reason": "code_disclosed"
},
"external_ref": "po-1001",
"iccid": "8984012345678901234",
"id": "018f2b00-0000-7000-8000-00000000009a",
"install": {
"android_url": "https://esimsetup.google.com/esim_qrcode_provisioning?data=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"apn": "<apn>",
"ios_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"lpa": "LPA:1$smdp.example.com$MATCH-ABC123",
"matching_id": "MATCH-ABC123",
"qr_code_base64": "<PNG base64 — đã rút gọn>",
"smdp_address": "smdp.example.com"
},
"mode": "live",
"msisdn": "+84900000000",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"package_name": "Tên gói hiển thị",
"plan": {
"current_code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"smdp_status": "released",
"status": "delivered",
"updated_at": "2026-07-11T09:03:11Z",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
},
"ok": true
}/v1/esims/{id}/cancelCancel an unused eSIM
esims:writeIdempotency-Key Requiredpost_esims__id__cancelRequired scope: esims:write.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Headers (1)
Idempotency-KeystringrequiredA client-generated string — the same key + body executes exactly once (a retry returns the original result). Scoped by api_key_id (§11): two different keys = two separate commands.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
cancelled_at | string · date-time | — | |
esim_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
refund_suggested | boolean |
| |
refunded | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
status | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedesim.not_cancellableidempotency.conflictcapability.not_supportedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/esims/$ID/cancel" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)"Sample Response
{
"data": {
"cancelled_at": "2026-07-11T08:12:30Z",
"esim_id": "018f2b00-0000-7000-8000-000000000000",
"mode": "live",
"refund_suggested": true,
"refunded": {
"amount": 273000,
"currency": "VND"
},
"status": "cancelled"
},
"ok": true
}/v1/esims/{id}/change-planTop up an eSIM
esims:writeIdempotency-Key Requiredpost_esims__id__change-planRequired scope: esims:write.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Headers (1)
Idempotency-KeystringrequiredA client-generated string — the same key + body executes exactly once (a retry returns the original result). Scoped by api_key_id (§11): two different keys = two separate commands.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
package_code | string | required | The package code from YOUR catalog, i.e. the |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | Result of changing the eSIM plan. |
charged | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
current_plan | object | — | |
code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
esim_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
order_id | string · uuid | — | |
previous_plan | object | nullable | — |
cancelled_at | string · date-time | nullable | — |
code | string | — | |
remaining_bytes_forfeited | integer · int64 | nullable | — |
warning | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedwallet.insufficient_balancepartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.package_not_visibleesim.topup_not_supportedesim.topup_provider_mismatchesim.not_eligibleidempotency.conflictcapability.not_supportedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/esims/$ID/change-plan" # them query thi noi thang vao day TRUOC khi ky
BODY='{"package_code":"TKE-VN-7D-P4M2Q"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"charged": {
"amount": 273000,
"currency": "VND"
},
"current_plan": {
"code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"esim_id": "018f2b00-0000-7000-8000-000000000000",
"mode": "live",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"previous_plan": {
"cancelled_at": "2026-07-11T08:12:30Z",
"code": "TKE-VN-30D-X3K9F",
"remaining_bytes_forfeited": 2147483648
},
"warning": "Dung lượng còn lại của gói cũ KHÔNG được cộng dồn."
},
"ok": true
}Sample Request Body
{
"package_code": "TKE-VN-7D-P4M2Q"
}/v1/esims/{id}/plan-optionsValid top-up packages for this eSIM
esims:readget_esims__id__plan-optionsRequired scope: esims:read.
Return valid top-up packages FOR THIS EXACT eSIM. The server uses id to find the supplier holding it, then returns only that supplier's packages. /v1 never exposes provider_id, so this endpoint performs the filtering for you and makes selecting another supplier's package structurally impossible. Calling POST /v1/esims/{id}/change-plan directly with a package outside this list returns 409 esim.topup_provider_mismatch.
An eSIM with no linked supplier (a plain imported record or missing source data) returns an EMPTY list (total: 0). This is a valid answer, not an error.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Query Parameters (2)
page_sizeintegeroptionaldefault 50min 1max 200Records per page. Default 50, capped at 200 — sending a larger value is clamped to 200 (NOT an error).
cursorstringoptionalCursor for the next page — pass back the next_cursor value from the previous page VERBATIM. WARNING: a malformed cursor does NOT raise an error; the server just reads from the START of the list again (a deliberate choice so that changing the cursor encoding scheme never breaks a partner's running reconciliation loop). Consequence to know: an automated reconciliation loop that keeps sending a garbage cursor will loop forever on the first page instead of stopping — check that next_cursor differs from the cursor you just sent before looping again.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | The plans THIS eSIM can be topped up with — already filtered by the supplier holding it. Every element is a FULL
|
items | object[] | — | |
capabilities | object | What this package can do AFTER the sale. Read from each package's REAL declaration, never inferred from the supplier type: a package with nothing declared reports every flag as | |
cancel | object | — | |
restriction | string | nullable |
code_not_disclosednull |
supported | boolean | — | |
plan_change | object | — | |
policy | object | nullable | How TOPPING UP A NEW PLAN works at this supplier. |
… | … | Deeper nested fields: see the JSON example. | |
supported | boolean | — | |
usage_check | object | — | |
supported | boolean | — | |
code | string | The package code from YOUR catalog, i.e. the | |
coverage | object | Where the package works. | |
countries | string[] | ISO-3166 alpha-2 codes. Empty for global packages (we do not enumerate 200 countries) and for packages with no declared coverage. | |
country_count | integer | How many countries the package covers — so you can sort or filter on "multi-country packages" without fetching everything and counting the array yourself. | |
group | string | Whether the package covers a list of countries ( | |
region | string | nullable | ALWAYS |
data | object | The package's data allowance. EVERY figure is in BYTES (not MB/GB — it avoids rounding twice). | |
daily_bytes | integer · int64 | nullable | PER-DAY data allowance, in bytes. |
label | string | A human-readable label, generated by the same function every one of our screens uses. | |
model | string | How the allowance is counted: dailyfixedunlimited | |
reset | object | nullable | When the daily allowance resets. A per-day package whose reset point is unknown costs a customer who buys at 22:00 almost the whole first day — this is sales information, not a technical detail. |
at | string | nullable | Reset time in the local time of |
note | string | nullable | — |
timezone | string | nullable | Time zone of the reset point ( |
total_bytes | integer · int64 | nullable | Total allowance for the whole period, in bytes. |
description | string | nullable | The sales description of the package. |
description_en | string | nullable | English description. |
features | object | What the package can and cannot do. A TYPED object rather than an array of codes: | |
apps | object[] | Per-OPERATING-SYSTEM app status. An app NOT PRESENT in this array means WE HAVE NO INFORMATION, NOT "it works". Only what is listed is something the supplier commits to; today only TikTok is declared. There is no combined field, and that is deliberate: measured on the real catalog, 2,465 packages run TikTok on iPhone but NOT on Android. A single combined flag flattens them to "no", and you would then be wrong about nearly a third of the catalog. | |
android | string |
supportedblockedunknown | |
app | string | App code, lowercase (e.g. | |
ios | string |
supportedblockedunknown | |
note | string | nullable | — |
hotspot | boolean | nullable | Wi-Fi tethering for other devices. |
kyc_required | boolean | nullable | Identity verification (KYC) is required before use. |
sms | boolean | nullable | SMS. |
voice | boolean | nullable | Voice calls. |
fup | object | nullable | What happens after the high-speed allowance runs out. |
behavior | string | nullable |
throttlecutoffnull |
high_speed_bytes | integer · int64 | nullable | BYTES at full speed before throttling kicks in. |
note | string | nullable | — |
speed_kbps | integer | nullable | Speed AFTER the high-speed allowance runs out (kbps). |
market | string | The package's SUPPLY SYSTEM, an OPEN ENUM (clients MUST ignore unknown values). This is the axis that decides whether a package has stock behind it and which post-sale operations it has: domesticinternational | |
name | string | — | |
name_en | string | nullable | English name. |
network | object | Which networks the package runs on. | |
apn | string | nullable | The APN that must be entered by hand on some Android devices that do not pick it up automatically. Without it you cannot build an installation guide, and every such case becomes a support call. |
carriers | object[] | The carriers this package RUNS ON. This is an eSIM specification (whose radio it uses), not the supplier. | |
name | string | — | |
generations | string[] | nullable | Network generations the package can use ( |
speed_cap_kbps | integer | nullable | Speed cap for the WHOLE package (kbps) — different from |
price | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
tags | string[] | Normalized commercial tags (lowercase, hyphenated). They are also what your key's catalog restrictions match on. NO package carries a tag today, so | |
validity | object | How long the package lasts, and when the clock starts. | |
days | integer | — | |
install_by_days | integer | nullable | Number of days to scan the QR / install the eSIM after purchase. |
label | string | — | |
reclaim_on_expiry | boolean | nullable | The supplier RECLAIMS the subscription when the plan expires — meaning it can no longer be topped up or changed. If you sell renewals you must know that window. |
starts_when | string | nullable | WHEN the clock starts: The HEAVIEST field in this block. If the supplier counts from INSTALL and you do not know it, you will quote the wrong expiry date to EVERY customer. on_installon_purchasenull |
unused_valid_days | integer | nullable | Number of days the package can sit unused on the shelf before it expires. |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in | |
policy | object | nullable | How TOPPING UP A NEW PLAN works at this supplier. |
note | string | nullable | — |
requires_cancel_first | boolean | The RUNNING plan is cancelled before the new one is placed (this supplier does not stack plans). Tell the customer BEFORE they hit the top-up-a-new-plan button. | |
stacking | boolean | Whether the new plan's validity and allowance add on top of the running one. | |
total | integer · int64 | nullable | Total packages matching the filters. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/esims/$ID/plan-options" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"capabilities": {
"cancel": {
"restriction": "code_not_disclosed",
"supported": true
},
"plan_change": {
"policy": {
"note": "<note>",
"requires_cancel_first": true,
"stacking": true
},
"supported": true
},
"usage_check": {
"supported": true
}
},
"code": "TKE-VN-30D-X3K9F",
"coverage": {
"countries": [
"CN"
],
"country_count": 1,
"group": "country",
"region": "<region>"
},
"data": {
"daily_bytes": 1073741824,
"label": "1GB/ngày",
"model": "daily",
"reset": {
"at": "00:00",
"note": "<note>",
"timezone": "UTC+8"
},
"total_bytes": null
},
"description": "<description>",
"description_en": "<description_en>",
"features": {
"apps": [
{
"android": "blocked",
"app": "tiktok",
"ios": "supported",
"note": "<note>"
}
],
"hotspot": true,
"kyc_required": false,
"sms": false,
"voice": false
},
"fup": {
"behavior": "throttle",
"high_speed_bytes": 1048576,
"note": "<note>",
"speed_kbps": 128
},
"market": "international",
"name": "eSIM Trung Quốc Mainland Multi - 1GB/ngày - 24 ngày",
"name_en": "<name_en>",
"network": {
"apn": "e-ideas",
"carriers": [
{
"name": "China Unicom"
}
],
"generations": [
"4g",
"5g"
],
"speed_cap_kbps": 1
},
"price": {
"amount": 273000,
"currency": "VND"
},
"tags": [],
"validity": {
"days": 24,
"install_by_days": 365,
"label": "24 ngày",
"reclaim_on_expiry": true,
"starts_when": "on_install",
"unused_valid_days": 1
}
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"policy": {
"note": "<note>",
"requires_cancel_first": true,
"stacking": true
},
"total": 128,
"warnings": []
},
"ok": true
}/v1/esims/{id}/refund-requestseSIM refund request history (members only)
esims:readget_esims__id__refund-requestsRequired scope: esims:read.
Authorized live member keys only. Return up to 50 recent refund requests for the member-owned eSIM, newest first; next_cursor and total are null. has_open_request indicates pending review. Organization and test keys receive capability.not_supported.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
has_open_request | boolean | — | |
items | object[] | — | |
decided_at | string · date-time | nullable | — |
decision_note | string | nullable | — |
id | string · uuid | — | |
order_id | string · uuid | — | |
order_item_id | string · uuid | — | |
reason | string | — | |
refunded | object | nullable | — |
requested_at | string · date-time | — | |
status | string | —pendingapprovedrejected | |
suggested | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | How many rows match your filter. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundcapability.not_supportedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_member_live_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/esims/$ID/refund-requests" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"has_open_request": true,
"items": [
{
"decided_at": "2026-07-11T08:12:30Z",
"decision_note": "<decision_note>",
"id": "018f2b00-0000-7000-8000-00000000009a",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"order_item_id": "018f2b00-0000-7000-8000-000000000000",
"reason": "<reason>",
"refunded": null,
"requested_at": "2026-07-11T08:12:30Z",
"status": "pending",
"suggested": {
"amount": 273000,
"currency": "VND"
}
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/esims/{id}/refund-requestsCreate an eSIM refund request (members only)
esims:writeIdempotency-Key Requiredpost_esims__id__refund-requestsRequired scope: esims:write.
Authorized live member keys only. Submit a refund request for that member’s eSIM with Idempotency-Key; no money moves until admin approval. Organization and test keys receive capability.not_supported. In test mode, cancel an eSIM to exercise a virtual-wallet refund.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Headers (1)
Idempotency-KeystringrequiredA client-generated string — the same key + body executes exactly once (a retry returns the original result). Scoped by api_key_id (§11): two different keys = two separate commands.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
reason | string | required | Refund reason, 5 to 1000 characters after trimming leading and trailing whitespace. |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
decided_at | string · date-time | nullable | — |
decision_note | string | nullable | — |
id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
order_id | string · uuid | — | |
order_item_id | string · uuid | — | |
reason | string | — | |
refunded | object | nullable | — |
requested_at | string · date-time | — | |
status | string | —pendingapprovedrejected | |
suggested | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedinternal.not_foundesim.refund_not_eligibleesim.refund_request_pendingidempotency.conflictcapability.not_supportedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_member_live_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/esims/$ID/refund-requests" # them query thi noi thang vao day TRUOC khi ky
BODY='{"reason":"<reason>"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"decided_at": "2026-07-11T08:12:30Z",
"decision_note": "<decision_note>",
"id": "018f2b00-0000-7000-8000-00000000009a",
"mode": "live",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"order_item_id": "018f2b00-0000-7000-8000-000000000000",
"reason": "<reason>",
"refunded": null,
"requested_at": "2026-07-11T08:12:30Z",
"status": "pending",
"suggested": {
"amount": 273000,
"currency": "VND"
}
},
"ok": true
}Sample Request Body
{
"reason": "<reason>"
}/v1/esims/{id}/usageData used
esims:readget_esims__id__usageRequired scope: esims:read.
Read an eSIM's used and remaining data, with the updated_at timestamp of the figures (the real freshness). For an eSIM from a supplier that is read on demand only (currently SkyFi), this call queues a scan and WAITS for the result for up to about 15 seconds, so the response can take up to ~15 seconds; the figures returned are those of the scan that just finished. If the scan fails, is within the 15-second spacing between scans of the same eSIM, or the wait times out, NO error is returned: the most recently stored figures are returned, so check updated_at. Other suppliers return stored figures that are refreshed periodically.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
esim_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedcapability.not_supportedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/esims/$ID/usage" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"esim_id": "018f2b00-0000-7000-8000-000000000000",
"mode": "live",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
},
"ok": true
}Wallet
Wallet balance
/v1/wallet/balanceWallet balance
wallet:readget_wallet_balanceRequired scope: wallet:read.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
available | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
balance | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
credit_limit | object | An amount of money. ONE shape everywhere on | |
amount | integer · int64 | Integer, in the smallest unit of | |
currency | string | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/wallet/balance" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"available": {
"amount": 273000,
"currency": "VND"
},
"balance": {
"amount": 273000,
"currency": "VND"
},
"credit_limit": {
"amount": 273000,
"currency": "VND"
},
"mode": "live"
},
"ok": true
}Webhooks
Webhook configuration + delivery log + simulator
/v1/testmode/webhooks/fail-nextSimulate next webhook failure (test keys only)
webhook:managepost_testmode_webhooks_fail-nextRequired scope: webhook:manage.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
count | integer | — |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
endpoint_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
testmode_fail_next | integer | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddeninternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/testmode/webhooks/fail-next" # them query thi noi thang vao day TRUOC khi ky
BODY='{"count":1}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"endpoint_id": "018f2b00-0000-7000-8000-000000000000",
"mode": "live",
"testmode_fail_next": 1
},
"ok": true
}Sample Request Body
{
"count": 1
}/v1/testmode/webhooks/fireFire a test webhook event (test keys only)
webhook:managepost_testmode_webhooks_fireRequired scope: webhook:manage.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | Partially override the | |
event_type | string | required | Webhook event type to simulate firing. order.completedorder.failedesim.deliveredesim.activatedesim.usage_thresholdesim.expiringwallet.low_balancecatalog.updated |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
delivery_id | string · uuid | — | |
event_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddenwebhook.not_configuredinternal.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/testmode/webhooks/fire" # them query thi noi thang vao day TRUOC khi ky
BODY='{"data":{},"event_type":"order.completed"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"delivery_id": "018f2b00-0000-7000-8000-000000000000",
"event_id": "018f3c00-0000-7000-8000-0000000000b0",
"mode": "live"
},
"ok": true
}Sample Request Body
{
"data": {},
"event_type": "order.completed"
}/v1/webhooksList webhook endpoints
webhook:manageget_webhooksRequired scope: webhook:manage.
The webhook endpoints of the API key you are calling with. A key NEVER sees another key's endpoint — not even two keys of the same organization (§5.11: an endpoint is bound to an api_key_id; test and live never share one).
Not configured yet → 200 with an EMPTY array, not 404: the collection always exists, it just has no elements right now.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
items | object[] | — | |
active | boolean |
| |
created_at | string · date-time | — | |
events | string[] | Empty = receive everything. | |
id | string · uuid | The endpoint's ID — what you put into | |
last_delivery_at | string · date-time | nullable | The last time we DELIVERED through this endpoint. |
secret_last4 | string | The last four characters of the webhook secret — enough for you to check "is the secret I hold the one the system signs with?" without any endpoint handing the secret back. The full secret is shown ONCE, at creation. | |
url | string | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | How many rows match your filter. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/webhooks" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"active": true,
"created_at": "2026-07-11T08:12:30Z",
"events": [
"order.completed"
],
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_delivery_at": "2026-07-11T08:12:30Z",
"secret_last4": "a3f9",
"url": "https://partner.example.com/hooks/tke"
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/webhooksCreate a webhook endpoint
webhook:managepost_webhooksRequired scope: webhook:manage.
Create a webhook endpoint for the API key you are calling with. Before saving, the server POSTs a signed ping event to the URL you sent (CHECK_HEALTH): no 2xx means NOTHING is saved and you get 422 webhook.health_check_failed. The URL must be public https — pointing at an internal / metadata IP → 422 webhook.url_not_allowed.
The webhook_secret in the response is shown exactly once: no endpoint can read it back. Lose it and you must rotate from the dashboard (the old secret stays valid in parallel for 24h, §8.4).
The key already has an endpoint → 409 webhook.already_configured, even when that endpoint is paused (is_active: false). A paused endpoint is still the key's endpoint — its url, secret and log are all intact. To start over, DELETE /v1/webhooks/{id} and then create a new one.
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
events | string[] | Empty = receive everything. An unknown value → 400 | |
url | string · uri | required | — |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
active | boolean |
| |
created_at | string · date-time | — | |
events | string[] | Empty = receive everything. | |
health_check | string | —passed | |
id | string · uuid | The endpoint's ID — what you put into | |
last_delivery_at | string · date-time | nullable | The last time we DELIVERED through this endpoint. |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
secret_last4 | string | The last four characters of the webhook secret — enough for you to check "is the secret I hold the one the system signs with?" without any endpoint handing the secret back. The full secret is shown ONCE, at creation. | |
url | string | — | |
webhook_secret | string | Shown ONLY ONCE, at creation time (prefix | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.already_configuredwebhook.health_check_failedwebhook.url_not_allowedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/webhooks" # them query thi noi thang vao day TRUOC khi ky
BODY='{"events":["order.completed"],"url":"https://partner.example.com/hooks/tke"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"active": true,
"created_at": "2026-07-11T08:12:30Z",
"events": [
"order.completed"
],
"health_check": "passed",
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_delivery_at": "2026-07-11T08:12:30Z",
"mode": "live",
"secret_last4": "a3f9",
"url": "https://partner.example.com/hooks/tke",
"webhook_secret": "<webhook secret — chỉ hiện đúng một lần>"
},
"ok": true
}Sample Request Body
{
"events": [
"order.completed"
],
"url": "https://partner.example.com/hooks/tke"
}/v1/webhooks/{id}Get one webhook endpoint
webhook:manageget_webhooks__id_Required scope: webhook:manage.
Path Parameters (1)
idstringrequiredThe webhook endpoint ID (the id returned by POST /v1/webhooks or GET /v1/webhooks).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
active | boolean |
| |
created_at | string · date-time | — | |
events | string[] | Empty = receive everything. | |
id | string · uuid | The endpoint's ID — what you put into | |
last_delivery_at | string · date-time | nullable | The last time we DELIVERED through this endpoint. |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
secret_last4 | string | The last four characters of the webhook secret — enough for you to check "is the secret I hold the one the system signs with?" without any endpoint handing the secret back. The full secret is shown ONCE, at creation. | |
url | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="GET"
REQ_PATH="/v1/webhooks/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"active": true,
"created_at": "2026-07-11T08:12:30Z",
"events": [
"order.completed"
],
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_delivery_at": "2026-07-11T08:12:30Z",
"mode": "live",
"secret_last4": "a3f9",
"url": "https://partner.example.com/hooks/tke"
},
"ok": true
}/v1/webhooks/{id}Update a webhook endpoint
webhook:manageput_webhooks__id_Required scope: webhook:manage.
Path Parameters (1)
idstringrequiredThe webhook endpoint ID (the id returned by POST /v1/webhooks or GET /v1/webhooks).
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
events | string[] | Empty = receive everything. An unknown value → 400 | |
url | string · uri | required | — |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
active | boolean |
| |
created_at | string · date-time | — | |
events | string[] | Empty = receive everything. | |
id | string · uuid | The endpoint's ID — what you put into | |
last_delivery_at | string · date-time | nullable | The last time we DELIVERED through this endpoint. |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
secret_last4 | string | The last four characters of the webhook secret — enough for you to check "is the secret I hold the one the system signs with?" without any endpoint handing the secret back. The full secret is shown ONCE, at creation. | |
url | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.not_foundwebhook.health_check_failedwebhook.url_not_allowedpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="PUT"
REQ_PATH="/v1/webhooks/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY='{"events":["order.completed"],"url":"https://partner.example.com/hooks/tke"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"active": true,
"created_at": "2026-07-11T08:12:30Z",
"events": [
"order.completed"
],
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_delivery_at": "2026-07-11T08:12:30Z",
"mode": "live",
"secret_last4": "a3f9",
"url": "https://partner.example.com/hooks/tke"
},
"ok": true
}Sample Request Body
{
"events": [
"order.completed"
],
"url": "https://partner.example.com/hooks/tke"
}/v1/webhooks/{id}Remove a webhook endpoint (stop receiving new events)
webhook:managedelete_webhooks__id_Required scope: webhook:manage.
Remove the endpoint: stop receiving any new events on the API key you are calling with.
What you lose: the webhook_secret goes with the deleted endpoint. Re-creating via POST /v1/webhooks mints a NEW id and a NEW secret — your verify function must be updated. If you need a temporary stop that KEEPS the secret, use the "Pause" button in the dashboard, not this endpoint.
What you keep: the full delivery log stays for reconciliation. But those deliveries belong to a removed endpoint, so they can no longer be replayed.
Events waiting on retry: per §8.5, once an endpoint is removed its pending deliveries stop with reason endpoint_disabled and remain in the log.
An id that does not belong to the calling key, or one already removed → 404 webhook.not_found (never a silent 200).
Path Parameters (1)
idstringrequiredThe webhook endpoint ID (the id returned by POST /v1/webhooks or GET /v1/webhooks).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
deleted_at | string · date-time | — | |
id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.not_foundpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="DELETE"
REQ_PATH="/v1/webhooks/$ID" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"deleted_at": "2026-07-11T08:12:30Z",
"id": "018f2b00-0000-7000-8000-00000000009a",
"mode": "live"
},
"ok": true
}/v1/webhooks/deliveriesWebhook delivery log
webhook:manageget_webhooks_deliveriesRequired scope: webhook:manage.
Query Parameters (5)
pageintegeroptionaldefault 1min 11-based page number. A page past the end of the data returns items: [] + the real total (200, not 404).
page_sizeintegeroptionaldefault 50min 1max 200Records per page. Default 50, capped at 200 — sending a larger value is clamped to 200 (NOT an error).
event_typestringoptionalFilter by event type (see the webhook events catalog).
statusstringoptionalpendingdeliveredfailedFilter by delivery status. ?status=failed is the filter used for pull-based webhook reconciliation (§13). An unknown value does NOT raise an error — the filter is simply IGNORED and the full log is returned.
event_idstring · uuidoptionalEvery delivery attempt (including replays) of ONE event.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
items | object[] | — | |
attempts | integer | — | |
created_at | string · date-time | — | |
event_id | string · uuid | The EVENT identifier. A replay creates a NEW delivery carrying the SAME | |
event_type | string | — | |
id | string · uuid | — | |
last_attempt_at | string · date-time | nullable | — |
next_retry_at | string · date-time | nullable | — |
response_code | integer | nullable | — |
status | string | —pendingdeliveredfailed | |
webhook_id | string · uuid | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_cursor | string | nullable | Pass it back VERBATIM to fetch the next page. |
page_size | integer | nullable | The MAXIMUM number of records the server used for this page (already clamped to the cap), not the actual number of elements in |
total | integer · int64 | nullable | How many rows match your filter. |
warnings | object[] | WHY the list looks the way it does. Empty = nothing unusual.
| |
code | string | —filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing | |
count | integer · int64 | nullable | — |
message | string | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.not_configuredpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/webhooks/deliveries" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"items": [
{
"attempts": 3,
"created_at": "2026-07-11T08:12:30Z",
"event_id": "018f3c00-0000-7000-8000-0000000000b0",
"event_type": "order.completed",
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_attempt_at": "2026-07-11T09:05:00Z",
"next_retry_at": "2026-07-11T09:10:00Z",
"response_code": 200,
"status": "pending",
"webhook_id": "018f2b00-0000-7000-8000-000000000000"
}
],
"mode": "live",
"next_cursor": "eyJ1cGRhdGVkX2F0IjoiMjAyNi0wNy0xMVQwODoxMjozMFoifQ",
"page_size": 50,
"total": 128,
"warnings": []
},
"ok": true
}/v1/webhooks/deliveries/{id}/replayReplay a webhook
webhook:managepost_webhooks_deliveries__id__replayRequired scope: webhook:manage.
Path Parameters (1)
idstringrequiredThe ID of one webhook delivery (items[].id from GET /v1/webhooks/deliveries).
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
attempts | integer | — | |
created_at | string · date-time | — | |
event_id | string · uuid | The EVENT identifier. A replay creates a NEW delivery carrying the SAME | |
event_type | string | — | |
id | string · uuid | — | |
last_attempt_at | string · date-time | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
next_retry_at | string · date-time | nullable | — |
response_code | integer | nullable | — |
status | string | —pendingdeliveredfailed | |
webhook_id | string · uuid | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedwebhook.delivery_not_foundwebhook.not_configuredpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/webhooks/deliveries/$ID/replay" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"attempts": 3,
"created_at": "2026-07-11T08:12:30Z",
"event_id": "018f3c00-0000-7000-8000-0000000000b0",
"event_type": "order.completed",
"id": "018f2b00-0000-7000-8000-00000000009a",
"last_attempt_at": "2026-07-11T09:05:00Z",
"mode": "live",
"next_retry_at": "2026-07-11T09:10:00Z",
"response_code": 200,
"status": "pending",
"webhook_id": "018f2b00-0000-7000-8000-000000000000"
},
"ok": true
}Test Mode
Use test keys tke_test_ or tke_member_test_ on the api-testing host only. Simulate the owner’s test eSIMs and data without live funds or suppliers. Member test mode does not support webhooks, bulk orders or refund-review requests.
/v1/testmode/esims/{id}/simulateSimulate an eSIM test mode lifecycle (test keys only)
esims:writepost_testmode_esims__id__simulateRequired scope: esims:write.
Test keys tke_test_ or tke_member_test_ only. Apply a scenario to the owner’s test eSIM: used data, installation state, expiry, throttling or simulated provider rejection of the next operation. The body must contain at least one field; an empty body returns 422 testmode.invalid_scenario. Read the result through GET /v1/esims/{id} and /usage as in live mode. Plan-change and cancellation policies are shared with live mode; no live supplier is called.
Path Parameters (1)
idstringrequiredThe stable ID of a sold eSIM (NOT the ICCID — an ICCID can be reused).
Request bodyrequiredapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
data_used_mb | integer · int64 | Data ALREADY USED (MB, binary 1 MiB units). A value above the package allowance is clamped to the allowance — that is the ran-out-of-data scenario, not an error. | |
expire_at | string · date-time | Set a specific expiry timestamp (ISO-8601 UTC). | |
expire_now | boolean |
| |
provider_reject | boolean | Simulate provider rejection on the next operation (top-up/cancel). | |
status | string | The MACHINE lifecycle of the eSIM profile (the provider's vocabulary). DIFFERENT from the releasedinstalledenableddisabled | |
throttled | boolean |
|
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
cancellable | object | Whether this eSIM can still be cancelled, and if not, why — read it BEFORE calling | |
allowed | boolean | — | |
disclosed_at | string · date-time | nullable | When the code left our systems (ISO-8601 UTC) — |
reason | string | nullable | Why it cannot be cancelled. code_disclosedwrong_statusexpirednot_supportednull |
external_ref | string | nullable | Your own order reference (the |
iccid | string | nullable | — |
id | string · uuid | The eSIM id — used in every | |
install | object | nullable | Everything needed to INSTALL this eSIM. A response carrying this block always has |
android_url | string | nullable | — |
apn | string | nullable | The access point name (APN) to enter when the device does not pick up the configuration automatically. ALWAYS |
ios_url | string | nullable | — |
lpa | string | nullable | The eSIM activation code (a sellable secret). This string is all you need to render the QR code on your side. |
matching_id | string | nullable | — |
qr_code_base64 | string | nullable | PNG data-URI of the QR code. Only present when you ask for |
smdp_address | string | nullable | — |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
msisdn | string | nullable | — |
order_id | string · uuid | — | |
package_code | string | nullable | The package CODE from the catalog — the key to map this eSIM back to your catalog. |
package_name | string | The display NAME — a human-readable label that can change. Do NOT use it as a reconciliation key. | |
plan | object | nullable | The plan currently applied to this eSIM. |
current_code | string | — | |
expires_at | string · date-time | nullable | — |
started_at | string · date-time | nullable | — |
simulated | object | The simulated state AFTER the scenario has been applied. | |
esim_state | string | The MACHINE lifecycle of the test eSIM. On live this comes from releasedinstalledenableddisabled | |
last_via | string | The flow that TOUCHED it most recently. open_apisimulateseed | |
via | string | The flow that CREATED this test eSIM. open_apisimulateseed | |
smdp_status | string | nullable | — |
status | string | — | |
updated_at | string · date-time | — | |
usage | object | nullable | eSIM data usage statistics. |
expires_at | string · date-time | nullable | — |
remaining_bytes | integer · int64 | nullable | — |
source | string | nullable | —pollwebhook |
total_bytes | integer · int64 | nullable | — |
updated_at | string · date-time | nullable | — |
used_bytes | integer · int64 | nullable | — |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddeninternal.not_foundtestmode.invalid_scenariopartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
ID="THAY-BANG-ID"
METHOD="POST"
REQ_PATH="/v1/testmode/esims/$ID/simulate" # them query thi noi thang vao day TRUOC khi ky
BODY='{"data_used_mb":4096,"expire_at":"2026-07-11T08:12:30Z","expire_now":true,"provider_reject":true,"status":"enabled","throttled":true}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"cancellable": {
"allowed": true,
"disclosed_at": "2026-07-11T08:12:30Z",
"reason": "code_disclosed"
},
"external_ref": "po-1001",
"iccid": "8984012345678901234",
"id": "018f2b00-0000-7000-8000-00000000009a",
"install": {
"android_url": "https://esimsetup.google.com/esim_qrcode_provisioning?data=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"apn": "<apn>",
"ios_url": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24smdp.example.com%24MATCH-ABC123",
"lpa": "LPA:1$smdp.example.com$MATCH-ABC123",
"matching_id": "MATCH-ABC123",
"qr_code_base64": "<PNG base64 — đã rút gọn>",
"smdp_address": "smdp.example.com"
},
"mode": "live",
"msisdn": "+84900000000",
"order_id": "018f2b00-0000-7000-8000-00000000009a",
"package_code": "TKE-VN-30D-X3K9F",
"package_name": "Tên gói hiển thị",
"plan": {
"current_code": "TKE-VN-30D-X3K9F",
"expires_at": "2026-08-10T00:00:00Z",
"started_at": "2026-07-11T08:12:30Z"
},
"simulated": {
"esim_state": "released",
"last_via": "open_api",
"via": "open_api"
},
"smdp_status": "released",
"status": "delivered",
"updated_at": "2026-07-11T09:03:11Z",
"usage": {
"expires_at": "2026-08-10T00:00:00Z",
"remaining_bytes": 5368709120,
"source": "poll",
"total_bytes": 5368709120,
"updated_at": "2026-07-11T09:03:11Z",
"used_bytes": 5368709120
}
},
"ok": true
}Sample Request Body
{
"data_used_mb": 4096,
"expire_at": "2026-07-11T08:12:30Z",
"expire_now": true,
"provider_reject": true,
"status": "enabled",
"throttled": true
}/v1/testmode/resetReset test mode data (test keys only)
orders:writepost_testmode_resetRequired scope: orders:write.
Headers (1)
Idempotency-KeystringoptionalRequest bodyoptionalapplication/json
| Field | Type | Constraints | Description |
|---|---|---|---|
reason | string | — |
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
esim_states_deleted | integer · int64 | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
order_items_deleted | integer · int64 | — | |
orders_deleted | integer · int64 | — | |
wallet_balance | integer · int64 | Test wallet balance AFTER the reset — always 0. The ledger is NOT deleted: the wallet is zeroed by an offsetting entry. | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddenpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/testmode/reset" # them query thi noi thang vao day TRUOC khi ky
BODY='{"reason":"Reset test data"}'
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data-raw "$BODY"Sample Response
{
"data": {
"esim_states_deleted": 1,
"mode": "live",
"order_items_deleted": 1,
"orders_deleted": 1,
"wallet_balance": 1
},
"ok": true
}Sample Request Body
{
"reason": "Reset test data"
}/v1/testmode/seedSeed test mode sample data (test keys only)
orders:writepost_testmode_seedRequired scope: orders:write.
Headers (1)
Idempotency-KeystringoptionalResponse
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
already_seeded | boolean | — | |
esims_created | integer | — | |
mode | string | The calling key environment, matching the X-TKE-Mode header. Organization keys use tke_live_/tke_test_; member keys use tke_member_live_/tke_member_test_. Live and testing hosts enforce the key mode. testlive | |
orders_created | integer | — | |
seeded | boolean | — | |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddentestmode.no_sellable_packagepartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="POST"
REQ_PATH="/v1/testmode/seed" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)"Sample Response
{
"data": {
"already_seeded": true,
"esims_created": 1,
"mode": "live",
"orders_created": 1,
"seeded": true
},
"ok": true
}/v1/testmode/statusTest mode overview (test keys only)
esims:readget_testmode_statusRequired scope: esims:read.
Response
Success
| Field | Type | Constraints | Description |
|---|---|---|---|
data | object | required | — |
base_url_testing | string | Test-mode host. Use tke_test_ or tke_member_test_ keys only; calling the live host returns 403 partner.wrong_host. | |
capabilities | object | — | |
bulk_orders | boolean | — | |
cancel_refund | boolean | — | |
refund_requests | boolean | — | |
webhooks | boolean | — | |
checklist | object[] | Integration milestones are derived from actual API traces for the same owner and test mode. The webhook step applies to organizations only. | |
done | boolean | — | |
label | string | — | |
last_at | string · date-time | nullable | — |
step | string | — | |
drift | object | The simulated usage loop: enabled state, interval and last run. Threshold webhooks are emitted only for organizations with webhook support. | |
enabled | boolean | — | |
interval_secs | integer · int64 | — | |
last_run_at | string · date-time | nullable | — |
mode | string | —testlive | |
ready | boolean | Whether the key owner has completed every supported checklist step. Members do not need a webhook test. | |
sample_counts | object | — | |
test_esims | integer · int64 | — | |
test_orders | integer · int64 | — | |
seeded | boolean | Whether the key owner already has sample test orders and eSIMs. | |
test_key_count | integer · int64 | — | |
wallet_balance | integer · int64 | Test wallet balance in virtual VND for the key owner, isolated from the live wallet. | |
webhook | object | — | |
endpoint_url | string | nullable | — |
fail_next | integer | — | |
status | string | nullable | — |
ok | boolean | required | —true |
Error Responses
All failure branches return the envelope { ok: false, error } with the corresponding HTTP status code, never 200 for errors. See the Error Codes page for code details.
internal.validation_failedpartner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reusedpartner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbiddenpartner.rate_limitedpartner.endpoint_disabledinternal.service_unavailableSample Invocation
All TKE-VN-30D-X3K9F values are placeholders you must supply. Query real package codes via GET /v1/catalog/packages.
#!/usr/bin/env bash
# Can: bash + openssl + uuidgen.
KEY_ID="tke_test_THAY-BANG-KEY-ID-CUA-BAN"
SECRET="THAY-BANG-SECRET-CUA-BAN"
METHOD="GET"
REQ_PATH="/v1/testmode/status" # them query thi noi thang vao day TRUOC khi ky
BODY=""
TS="$(date +%s000)" # epoch milliseconds
NONCE="$(uuidgen)"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')"
SIGNING="$METHOD"$'\n'"$REQ_PATH"$'\n'"$TS"$'\n'"$NONCE"$'\n'"$BODY_HASH"
SIG="$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"
curl -sS -X "$METHOD" "https://api-testing.tongkhoesim.com$REQ_PATH" \
-H "Accept-Language: en" \
-H "X-TKE-Key: $KEY_ID" \
-H "X-TKE-Timestamp: $TS" \
-H "X-TKE-Nonce: $NONCE" \
-H "X-TKE-Signature: $SIG"Sample Response
{
"data": {
"base_url_testing": "<base_url_testing>",
"capabilities": {
"bulk_orders": true,
"cancel_refund": true,
"refund_requests": true,
"webhooks": true
},
"checklist": [
{
"done": true,
"label": "<label>",
"last_at": "2026-07-11T08:12:30Z",
"step": "<step>"
}
],
"drift": {
"enabled": true,
"interval_secs": 1,
"last_run_at": "2026-07-11T08:12:30Z"
},
"mode": "test",
"ready": true,
"sample_counts": {
"test_esims": 1,
"test_orders": 1
},
"seeded": true,
"test_key_count": 1,
"wallet_balance": 1,
"webhook": {
"endpoint_url": "https://partner.example.com/hooks/tke",
"fail_next": 1,
"status": "completed"
}
},
"ok": true
}