Tổng Kho eSIM
OpenAPI 2.4.0 · 30 opérations

Référence API

Tous les endpoints /v1 depuis la spécification générée par le backend : chemins, paramètres, schémas, enums et erreurs correspondent au serveur.

Réel

https://api.tongkhoesim.com/v1

Mode test

https://api-testing.tongkhoesim.com/v1

Réel : api.tongkhoesim.com / tke_live_ (partenaires) ou tke_member_live_ (membres autorisés). Test : api-testing.tongkhoesim.com / tke_test_ / tke_member_test_ (partenaires / membres).

Conventions communes

Ces 6 règles s’appliquent aux 30 opérations.

Authentification

Tous les endpoints, GET compris : HMAC-SHA256 avec 4 headers : X-TKE-Key · X-TKE-Timestamp · X-TKE-Nonce · X-TKE-Signature. Aucun accès sans signature.

Enveloppe
Succès : { ok: true, data }. Erreur : { ok: false, error: { code, message, field?, meta?, request_id } }. Utilisez le statut HTTP adapté, jamais 200 pour un échec.
Types de données
Montants entiers VND (BIGINT). Volumes en octets, catalogue inclus (data.total_bytes / data.daily_bytes). Dates ISO-8601 UTC à convertir pour l’affichage.
Limitation de débit
429 fournit Retry-After et X-RateLimit-Limit / Remaining / Reset. Lisez ces headers : les limites varient par clé.
request_id
Chaque réponse fournit X-Request-Id, repris dans error.request_id. Journalisez-le pour toute demande d’assistance.
Idempotency-Key

Même clé et même corps sous 24 h : résultat rejoué sans débit supplémentaire. Par api_key_id. Obligatoire sur 5 opérations : 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 : deux modèles

Les formats et conditions d’arrêt diffèrent : utilisez un traitement adapté à chaque modèle.

Offset · page / page_size

Uniquement GET /v1/webhooks/deliveries. Retourne page, page_size et total.

Hors limites : HTTP 200, items: [] et total réel. Arrêtez sur une liste vide ou quand total est atteint.

Curseur · cursor / next_cursor

Utilisé par /v1/catalog/packages, /v1/orders, /v1/esims et /v1/esims/{id}/plan-options. updated_since est réservé à orders et esims.

Arrêtez si next_cursor est absent ou identique au curseur courant. Il peut être omis dans data.

Le piège du curseur invalide et HTTP 200

Un curseur malformé repart au début sans erreur pour préserver les anciens clients. Une boucle testant seulement next_cursor !== null peut répéter la première page indéfiniment.

javascript
let cursor = null;
const seen = new Set();

while (true) {
  const params = new URLSearchParams({ page_size: "200" });
  if (cursor) params.set("cursor", cursor);

  // callSigned ajoute vos 4 headers X-TKE-* (page HMAC).
  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;
}

Catalogue

Catalogue des forfaits, surface commune aux deux systèmes de fourniture. Le système est un CHAMP (`market`: `domestic` | `international`) et un filtre (`?market=`), pas deux chemins distincts. AUCUNE notion de stock : ni quantité ni niveau `available`/`low`/`out_of_stock`. Le forfait apparaît simplement ; si le fournisseur ne peut livrer, `POST /v1/orders` renvoie directement une erreur (`esim.out_of_stock`…), sans annonce préalable du catalogue. Pagination unique par curseur. `total` n’est calculé que si peu coûteux (filtre sur le système national), sinon `null`.

GET/v1/catalog/packages

Lister les forfaits

scope catalog:readget_catalog_packages

Autorisation requise: catalog:read.

Catalogue de votre organisation : une surface commune aux deux systèmes de fourniture, filtrée par ?market=.

Seuls les forfaits réellement ACHETABLES apparaissent : publiés, toujours distribués par le fournisseur, autorisés par la clé ET tarifés pour l’organisation. Un forfait sans prix est absent parce qu’il ne peut être commandé.

Pagination par curseur : premier appel sans cursor, puis renvoyer next_cursor à l’identique jusqu’à null. Une page plus courte que page_size n’indique PAS la fin.

capabilities reflète uniquement les déclarations réelles du forfait : sans déclaration, tous les indicateurs valent false. Mieux vaut omettre un bouton possible que promettre un endpoint qui renverrait 422.

Le catalogue ne présente AUCUNE notion de stock, ni quantité ni niveau disponible/épuisé. Si le fournisseur ne peut exécuter la commande, POST /v1/orders renvoie directement une erreur claire (esim.out_of_stock…), sans déduction à partir d’un champ préalable.

Paramètres de query (18)

page_sizeintegerfacultatifpar défaut 50min 1max 100

Lignes par page. Valeur par défaut 50, maximum 100. Une valeur supérieure est ramenée à 100 sans erreur.

marketstringfacultatif

Système de fourniture : domestic | international. Une valeur hors liste renvoie internal.invalid_filter (400), et non le catalogue complet : une faute ne doit pas ressembler à un filtre sans effet.

countrystringfacultatif

Code pays ISO-3166 alpha-2 (ex. VN). Uniquement les forfaits couvrant ce pays.

regionstringfacultatif

Identifiant de région (par exemple chau-a). Inclut les forfaits couvrant au moins un pays de cette région et tous les forfaits mondiaux.

qstringfacultatif

Recherche libre sur le code public et le nom du forfait (partielle, sans distinction de casse).

tagstringfacultatif

Filtrer sur UNE étiquette commerciale. La normalisation supprime les espaces superflus, utilise les minuscules et les tirets : Ban Le et ban-le désignent la même étiquette.

data_modelstringfacultatif

Modèle de données : daily | fixed | unlimited, remplaçant supports_topup. Valeur inconnue : internal.invalid_filter (400).

data_min_bytesinteger · int64facultatif

Quota minimum en OCTETS. Les forfaits quotidiens sont comparés sur leur quota PAR JOUR ; les forfaits illimités satisfont TOUJOURS une borne minimale.

data_max_bytesinteger · int64facultatif

Quota maximum en OCTETS. Les forfaits illimités sont EXCLUS : un plafond ne peut s’appliquer à l’illimité et laisser passer ces forfaits fausserait une recherche du plus petit forfait.

days_mininteger · int64facultatif

Validité minimum en jours.

days_maxinteger · int64facultatif

Validité maximum en jours.

price_mininteger · int64facultatif

Prix minimum (VND), comparé au prix de VOTRE ORGANISATION. Ce filtre est appliqué APRÈS la pagination serveur : une page plus courte ne signifie donc pas la fin des données.

price_maxinteger · int64facultatif

Prix maximum (VND), mêmes règles que price_min.

network_genstringfacultatif

Uniquement les forfaits compatibles avec cette génération (2g | 3g | 4g | 5g). Une valeur inconnue renvoie internal.invalid_filter (400).

hotspotstringfacultatif
Valeurs autorisées :truefalse

true : partage Wi-Fi déclaré ; false : absence déclarée. Sans déclaration (features.hotspot = null), aucun des deux filtres ne correspond. Accepte seulement les chaînes true et false.

appstringfacultatif

Uniquement les forfaits DÉCLARANT prendre en charge cette application sur au moins un système (app=tiktok). Une absence de déclaration ne correspond PAS au filtre : le silence n’est pas un engagement. Seul tiktok est accepté ; tout autre code renvoie internal.invalid_filter (400).

voicestringfacultatif
Valeurs autorisées :truefalse

true : voix déclarée ; false : absence de voix déclarée. Aucune déclaration ne correspond à aucun côté. Accepte seulement les chaînes true et false.

cursorstringfacultatif

Renvoyez next_cursor sans modification. Arrêtez uniquement à null, pas sur une page courte : restrictions de clé, prix de l'organisation et filtre de prix s'appliquent après pagination. Le curseur contient l'empreinte des filtres : changement ou corruption → catalog.cursor_mismatch (400), pour éviter de fusionner des pages de requêtes différentes.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
itemsobject[]
—
capabilitiesobject

Capacités après-vente déclarées par le forfait, jamais déduites du type de fournisseur. Sans déclaration, tous les indicateurs sont false, pour ne pas promettre une action renvoyant forcément 422 capability.not_supported.

cancelobject
—
restrictionstring
peut être null

code_not_disclosed : annulation uniquement si personne n'a vu le code (ni client, ni tableau de bord, ni ?include=lpa). Condition réelle de l'annulation, sans lien avec l'installation, que le système ne peut pas connaître. L'ancien libellé unused_only était trompeur.

code_not_disclosednull
supportedboolean
—
plan_changeobject
—
policyobject
peut être null

Politique du fournisseur lors de la RECHARGE AVEC UN NOUVEAU FORFAIT. null signifie que la recharge est impossible OU que cette politique n’a pas été mesurée. null interdit de supposer ; un objet entièrement false garantirait au contraire qu’aucun forfait n’est annulé.

……

Champs plus imbriqués : consultez le JSON exemple.

supportedboolean
—
usage_checkobject
—
supportedboolean
—
codestring

Code de forfait de VOTRE catalogue, soit public_sku. IMMUABLE par forfait, même si le fournisseur change ; utilisable comme clé de rapprochement. L’exemple a le bon format mais ne fonctionne PAS : lire un vrai code via GET /v1/catalog/packages.

coverageobject

Zones où le forfait fonctionne.

countriesstring[]

Codes ISO-3166 alpha-2. Tableau vide pour les forfaits mondiaux (sans énumération de 200 pays) et les forfaits sans couverture déclarée.

country_countinteger

Nombre de pays couverts, pour trier ou filtrer les forfaits multi-pays sans tout télécharger ni compter vous-même le tableau.

groupstring

Couverture : pays (country), région (region) ou monde (global). Énumération ouverte : ignorer les valeurs inconnues.

regionstring
peut être null

Toujours null : les régions sont associées aux pays, pas aux forfaits. Le filtre ?region= reste utilisable.

dataobject

Quota de données du forfait. Toutes les valeurs sont en OCTETS, pas en Mo/Go, pour éviter un double arrondi.

daily_bytesinteger · int64
peut être null

Quota QUOTIDIEN en octets. null pour les forfaits fixed ou unlimited.

labelstring

Libellé lisible généré par la même fonction que dans toutes nos interfaces.

modelstring

Mode de calcul : daily (quota quotidien), fixed (total sur la période), unlimited. ÉNUMÉRATION OUVERTE : les clients DOIVENT ignorer les valeurs inconnues.

dailyfixedunlimited
resetobject
peut être null

Moment de remise à zéro du quota quotidien, important pour une vente en fin de journée. null = non déclaré.

atstring
peut être null

Heure de remise à zéro dans le fuseau timezone (00:00, 00:00:01).

notestring
peut être null
—
timezonestring
peut être null

Fuseau horaire de la remise à zéro (UTC+8, UTC+7).

total_bytesinteger · int64
peut être null

Quota total pour la période, en octets. null pour daily ou unlimited : multiplier un quota journalier par le nombre de jours ne crée pas un engagement fournisseur.

descriptionstring
peut être null

Description commerciale du forfait. null si non rédigée.

description_enstring
peut être null

Description anglaise ; null si non fournie.

featuresobject

Fonctionnalités du forfait dans un objet typé, pas un tableau : false et null distinguent l'absence de partage Wi-Fi de l'information inconnue, contrairement à [].

appsobject[]

État de prise en charge par SYSTÈME D’EXPLOITATION.

Une application ABSENTE du tableau signifie information inconnue, PAS compatibilité. Seules les entrées présentes sont déclarées par le fournisseur ; actuellement seul TikTok est déclaré.

L’absence de drapeau global est volontaire : sur le catalogue réel, 2 465 forfaits prennent en charge TikTok sur iPhone mais PAS sur Android. Un drapeau unique les classerait tous « non », faussant près d’un tiers du catalogue.

androidstring

supported (compatibilité déclarée), blocked (incompatibilité déclarée), unknown (OS non couvert par la déclaration). Énumération ouverte.

supportedblockedunknown
appstring

Code d'application en minuscules (ex. tiktok).

iosstring

supported (compatibilité déclarée), blocked (incompatibilité déclarée), unknown (OS non couvert par la déclaration). Énumération ouverte.

supportedblockedunknown
notestring
peut être null
—
hotspotboolean
peut être null

Partage Wi-Fi avec d'autres appareils. null = non déclaré, différent de false.

kyc_requiredboolean
peut être null

Vérification d’identité (KYC) requise avant utilisation. null = non déclaré.

smsboolean
peut être null

SMS. false signifie AUCUNE réception de SMS, donc aucun OTP bancaire. null = non déclaré.

voiceboolean
peut être null

Appels vocaux. false signifie absence de voix déclarée : une ligne de données seule ne reçoit pas d'appels. null = non déclaré.

fupobject
peut être null

Comportement après épuisement du quota à pleine vitesse. null = aucune politique de réduction déclarée.

behaviorstring
peut être null

throttle (débit réduit), cutoff (déconnexion) ou null (non déclaré). Énumération ouverte.

throttlecutoffnull
high_speed_bytesinteger · int64
peut être null

Octets à pleine vitesse avant réduction du débit.

notestring
peut être null
—
speed_kbpsinteger
peut être null

Débit APRÈS épuisement du quota rapide (kbps). null si la ligne est coupée ou l’information non déclarée. Distinct de network.speed_cap_kbps, plafond du forfait entier.

marketstring

SYSTÈME DE FOURNITURE du forfait, énumération OUVERTE : ignorer les valeurs inconnues. Détermine la présence d’un stock et les opérations après-vente. domestic vend des kits avec recharge, annulation et consultation d’usage ; international commande au fournisseur à la demande, sans stock.

domesticinternational
namestring
—
name_enstring
peut être null

Nom anglais ; null si non fourni.

networkobject

Réseaux utilisables par le forfait.

apnstring
peut être null

APN à saisir manuellement sur certains appareils Android sans configuration automatique. Nécessaire pour rédiger le guide d’installation et éviter des demandes d’assistance. null = non déclaré.

carriersobject[]

Opérateurs réseau utilisés par le forfait. Il s’agit d’une caractéristique de l’eSIM (réseau radio), pas du fournisseur.

namestring
—
generationsstring[]
peut être null

Générations réseau utilisables (["4g", "5g"]). null = non déclaré, et NON « 2G uniquement ».

speed_cap_kbpsinteger
peut être null

Plafond de débit du forfait ENTIER (kbps), distinct de fup.speed_kbps, applicable après épuisement du quota rapide.

priceobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
tagsstring[]

Étiquettes commerciales normalisées (minuscules, tirets), également utilisées par les restrictions de catalogue de votre clé. Aucun forfait n’est étiqueté actuellement ; ?tag= renvoie donc une liste vide jusqu’à l’ajout d’étiquettes par le vendeur.

validityobject

Durée de validité du forfait et début du décompte.

daysinteger
—
install_by_daysinteger
peut être null

Délai en jours pour scanner le QR code et installer l’eSIM après achat. null = non déclaré, PAS « installation sans limite de temps ».

labelstring
—
reclaim_on_expiryboolean
peut être null

Le fournisseur récupère l'abonnement à expiration : recharge et changement deviennent impossibles. Important pour les renouvellements. null = non déclaré.

starts_whenstring
peut être null

Début du décompte : on_install (installation), on_purchase (achat), ou null. Information essentielle pour calculer l'expiration : avec null, ne promettez pas de date précise.

on_installon_purchasenull
unused_valid_daysinteger
peut être null

Nombre de jours avant expiration si le forfait reste inutilisé en stock. null = non déclaré, PAS illimité. Distinct de install_by_days : l’un concerne la CONSERVATION, l’autre la date limite d’INSTALLATION.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de forfaits correspondant aux filtres ; null si le comptage est coûteux (international ou sans market). Calculé avant restrictions de clé et prix organisation : ce total décrit le catalogue, pas les forfaits achetables.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filtercatalog.cursor_mismatch
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
GET/v1/catalog/packages/{code}

Lire un forfait

scope catalog:readget_catalog_packages__code_

Autorisation requise: catalog:read.

Détail d’UN SEUL forfait par son code public. data contient les MÊMES CHAMPS qu’un élément de GET /v1/catalog/packages, plus mode. Aucun champ propre au détail : un seul type Package suffit.

Un code invisible renvoie partner.package_not_visible (404), identique pour toutes les causes : code inexistant, restrictions de clé, tarif absent pour l’organisation ou lien fournisseur perdu. Distinguer les causes permettrait à un inconnu d’énumérer le catalogue.

Paramètres du chemin (1)

codestringobligatoire

Code du forfait, soit code dans le catalogue (public_sku). IMMUABLE même en cas de changement de fournisseur ; utilisable comme clé de rapprochement. Code hors restrictions de clé ou sans prix pour l’organisation : 404 partner.package_not_visible, comme pour un code inexistant.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
partner.package_not_visible
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "data": null,
  "ok": true
}

Commandes

Passer des commandes et rapprocher

GET/v1/orders

Lister les commandes (rapprochement par interrogation)

scope orders:readget_orders

Autorisation requise: orders:read.

Paramètres de query (5)

page_sizeintegerfacultatifpar défaut 50min 1max 200

Lignes par page. Valeur par défaut 50, maximum 200. Une valeur supérieure est ramenée à 200 sans erreur.

statusstringfacultatif
Valeurs autorisées :completedpending_providerprovider_failedcancelled

Filtrer par état de commande. Une valeur hors liste renvoie internal.invalid_filter (400).

external_refstringfacultatif

Filtrer par la référence envoyée à la validation de commande (correspondance EXACTE).

updated_sincestring · date-timefacultatif

Date ISO-8601 UTC. Renvoie les enregistrements où updated_at >= updated_since, dans l’ordre croissant : voie standard de RAPPROCHEMENT PAR INTERROGATION (§8.5), permettant de retrouver l’état même après un webhook manqué. Format incorrect : internal.invalid_filter (400).

cursorstringfacultatif

Curseur suivant : renvoyez next_cursor sans modification. Un curseur mal formé relit le début sans erreur pour préserver les intégrations lors des changements d'encodage. Vérifiez que le curseur change pour éviter une boucle infinie sur la première page.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
itemsobject[]
—
created_atstring · date-time
—
external_refstring
peut être null
—
fulfillment_statusstring
—
fulfilledpartially_fulfilledunfulfilled
idstring · uuid
—
item_countinteger · int64
—
netobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
public_refstring
peut être null
—
refundedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
statusstring
—
totalobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
updated_atstring · date-time
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de lignes correspondant au filtre. null si le comptage coûte trop cher. Ne l’utilisez pas pour détecter la fin : seul next_cursor l’indique.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filter
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/orders

Créer une commande (achat direct)

scope orders:writeIdempotency-Key obligatoirepost_orders

Autorisation requise: orders:write.

Headers (1)

Idempotency-Keystringobligatoire

Chaîne créée par le client : même clé et même corps exécutés une seule fois, avec résultat initial aux reprises. Portée par api_key_id (§11) : deux clés distinctes sont deux commandes.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
external_refstring
peut être null

Votre propre référence, restituée dans la réponse et les webhooks.

includestring[]
peut être null

Blocs supplémentaires : ["lpa"] ou ["lpa","qr"]. Sans ce champ, items[].install reste null et chaque eSIM conserve son droit d'annulation. RETOURNER LE CODE LE DIVULGUE : demander lpa ferme la possibilité d'annulation dès la commande.

invoice_buyerobject
peut être null

Clés membre uniquement. Sans données, le flux consommateur ne nécessite aucune organisation ni entité juridique vietnamienne. Les données fournies suivent le validateur personnel : full_name, tax_code personnel de 12 chiffres et règles de paiement pour address/email. Les factures professionnelles sont demandées après achat. Le test valide seulement les données et n’émet aucune facture MISA.

addressstring
peut être null
—
emailstring
peut être null
—
full_namestring
peut être null
—
legal_namestring
peut être null
—
tax_codestring
peut être null
—
itemsobject[]
obligatoire

Forfaits et quantités à acheter (50 eSIM maximum).

package_codestring
obligatoire

Code de forfait de VOTRE catalogue, soit public_sku. IMMUABLE par forfait, même si le fournisseur change ; utilisable comme clé de rapprochement. L’exemple a le bon format mais ne fonctionne PAS : lire un vrai code via GET /v1/catalog/packages.

quantityinteger
obligatoire

Quantité d’eSIM à acheter pour ce forfait (≥ 1).

Réponse

200Résultat de commande rejoué

Reprise dans les 24 h avec les mêmes Idempotency-Key et corps : corps IDENTIQUE, aucun nouveau débit, en-tête Idempotent-Replayed: true. Une nouvelle commande renvoie 201 ; ne déduisez pas le succès de ce seul statut.

ChampTypeContraintesDescription
dataobject
obligatoire

Commande partenaire.

created_atstring · date-time
—
external_refstring
peut être null
—
fulfillment_statusstring

Quantité réellement livrée, axe DISTINCT de status. Une commande de 3 eSIM dont 2 livrées et 1 en échec reste status=completed, avec fulfillment_status=partially_fulfilled et refunded_amount > 0.

fulfilledpartially_fulfilledunfulfilled
idstring · uuid

ID de la commande (UUID).

idempotent_replayboolean
peut être null

Vrai si cette requête rejoue un succès antérieur de manière idempotente.

invoice_nostring
peut être null

Numéro de facture après émission.

invoice_statusstring

État de la facture TVA de la commande. none = pas encore émise.

nonependingissuingissuedfailedcancelled
itemsobject[]
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
money_settledboolean

Montant de commande soldé, sans remboursement en cours. false empêche l'émission d'une facture.

netobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
public_refstring
peut être null

Code court de commande, à utiliser dans les demandes d'assistance.

refundedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
statusstring
—
completedpending_providerprovider_failedcancelled
totalobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
okboolean
obligatoire
—
true
201Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Commande partenaire.

created_atstring · date-time
—
external_refstring
peut être null
—
fulfillment_statusstring

Quantité réellement livrée, axe DISTINCT de status. Une commande de 3 eSIM dont 2 livrées et 1 en échec reste status=completed, avec fulfillment_status=partially_fulfilled et refunded_amount > 0.

fulfilledpartially_fulfilledunfulfilled
idstring · uuid

ID de la commande (UUID).

idempotent_replayboolean
peut être null

Vrai si cette requête rejoue un succès antérieur de manière idempotente.

invoice_nostring
peut être null

Numéro de facture après émission.

invoice_statusstring

État de la facture TVA de la commande. none = pas encore émise.

nonependingissuingissuedfailedcancelled
itemsobject[]
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
money_settledboolean

Montant de commande soldé, sans remboursement en cours. false empêche l'émission d'une facture.

netobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
public_refstring
peut être null

Code court de commande, à utiliser dans les demandes d'assistance.

refundedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
statusstring
—
completedpending_providerprovider_failedcancelled
totalobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filter
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
402
wallet.insufficient_balance
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
partner.package_not_visible
409
esim.out_of_stockidempotency.conflict
422
invoice.buyer_invalid
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "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
    }
  ]
}
GET/v1/orders/{id}

Détail de commande et eSIM

scope orders:readget_orders__id_

Autorisation requise: orders:read.

Paramètres du chemin (1)

idstringobligatoire

ID de la commande (UUID).

Paramètres de query (1)

includestringfacultatif

Blocs supplémentaires séparés par des virgules : lpa retourne install, qr ajoute install.qr_code_base64 (détail uniquement). RETOURNER LE CODE LE DIVULGUE et ferme définitivement l'annulation de chaque eSIM concernée (cancellable.allowed = false, reason = code_disclosed). Demandez-le à la livraison, omettez-le pour conserver l'annulation. Valeur inconnue : internal.invalid_filter (400, field: "include").

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Commande partenaire.

created_atstring · date-time
—
external_refstring
peut être null
—
fulfillment_statusstring

Quantité réellement livrée, axe DISTINCT de status. Une commande de 3 eSIM dont 2 livrées et 1 en échec reste status=completed, avec fulfillment_status=partially_fulfilled et refunded_amount > 0.

fulfilledpartially_fulfilledunfulfilled
idstring · uuid

ID de la commande (UUID).

idempotent_replayboolean
peut être null

Vrai si cette requête rejoue un succès antérieur de manière idempotente.

invoice_nostring
peut être null

Numéro de facture après émission.

invoice_statusstring

État de la facture TVA de la commande. none = pas encore émise.

nonependingissuingissuedfailedcancelled
itemsobject[]
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
money_settledboolean

Montant de commande soldé, sans remboursement en cours. false empêche l'émission d'une facture.

netobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
public_refstring
peut être null

Code court de commande, à utiliser dans les demandes d'assistance.

refundedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
statusstring
—
completedpending_providerprovider_failedcancelled
totalobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filter
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/orders/{id}/resend

Renvoyer l’e-mail ou le QR de la commande

scope orders:writepost_orders__id__resend

Autorisation requise: orders:write.

Paramètres du chemin (1)

idstringobligatoire

ID de la commande (UUID).

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
idstring · uuid

ID de la commande qui vient d’être renvoyée.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
resent_atstring · date-time
—
resent_tostring

Adresse du destinataire partiellement masquée ; l'interface partenaire ne restitue jamais l'adresse e-mail complète du client.

okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
409
order.not_deliverable
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "data": {
    "id": "018f2b00-0000-7000-8000-00000000009a",
    "mode": "live",
    "resent_at": "2026-07-11T08:12:30Z",
    "resent_to": "ab***@x.com"
  },
  "ok": true
}
POST/v1/orders/bulk

Passer un lot multi-lignes (une commande par ligne)

scope orders:writeIdempotency-Key obligatoirepost_orders_bulk

Autorisation requise: orders:write.

Commander un lot multi-lignes : chaque ligne crée une commande indépendante, suivant le cycle quote→commit de POST /v1/orders.

SUCCÈS PARTIEL : les erreurs d’une ligne ne bloquent pas les autres, et seules les lignes réussies sont débitées. Les erreurs métier (rupture, solde insuffisant, forfait indisponible, seuil anti-abus) figurent dans lines[].error, sans changer le statut HTTP du lot. Même après épuisement du solde, les lignes suivantes sont tentées car elles peuvent coûter moins cher.

Le statut est toujours 200 : résultats par ligne, pas une nouvelle ressource unique (201), et traitement terminé dans la requête, sans 202 ni interrogation.

Réessayer le MÊME Idempotency-Key avec le MÊME corps recharge le lot existant, conserve les résultats terminés et traite uniquement les lignes pending, sans double débit. Si aucune ligne n’a été exécutée, Idempotent-Replayed: true. Même clé avec autre corps : idempotency.conflict.

Deux lots au contenu identique pour une même organisation en 10 minutes sont traités comme un doublon accidentel, même avec des clés différentes : l’ancien lot est renvoyé avec Idempotent-Replayed: true, sans nouveau débit. Pour un nouvel achat réellement identique, changez external_ref : inclus dans le contenu signé, il distingue les lots et facilite le rapprochement. Un lot totalement échoué (aucune commande créée) n’est jamais bloqué ; après 10 minutes l’achat est à nouveau normal.

Les références de ligne sont renvoyées dans lines[].external_ref, celle du lot en tête de réponse.

Aucun LPA/QR dans la réponse : lire GET /v1/orders/{order_id} pour chaque commande.

Headers (1)

Idempotency-Keystringobligatoire

Chaîne créée par le client : même clé et même corps exécutés une seule fois, avec résultat initial aux reprises. Portée par api_key_id (§11) : deux clés distinctes sont deux commandes.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
external_refstring
peut être null

Référence du lot ENTIER, renvoyée dans la réponse du lot.

itemsobject[]
obligatoire

Chaque élément crée une commande indépendante. Limite configurable (100 par défaut) ; dépassement : internal.limit_exceeded (422).

external_refstring
peut être null

Référence de CETTE ligne, renvoyée à l’identique dans son résultat.

package_codestring
obligatoire

Code de forfait de VOTRE catalogue, soit public_sku. IMMUABLE par forfait, même si le fournisseur change ; utilisable comme clé de rapprochement. L’exemple a le bon format mais ne fonctionne PAS : lire un vrai code via GET /v1/catalog/packages.

quantityinteger
obligatoire
—

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Résultat d’un lot. SUCCÈS PARTIEL : une ligne en échec ne bloque pas les autres ; seules les lignes done sont débitées. Aucun LPA/QR : lire GET /v1/orders/{id} pour chaque order_id.

created_atstring · date-time
—
external_refstring
peut être null
—
failed_linesinteger
—
idstring · uuid
—
itemsobject[]
—
chargedobject
peut être null
—
errorobject
peut être null

Code d'erreur réel domain.reason, par exemple wallet.insufficient_balance, esim.out_of_stock, order.package_not_allowed, wallet.velocity_exceeded.

codestring
—
messagestring
—
external_refstring
peut être null
—
line_nointeger

À partir de 1, dans l'ordre exact de l'envoi.

order_idstring · uuid
peut être null
—
package_codestring
—
quantityinteger
—
statusstring
—
pendingdonefailed
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
statusstring

pending : des lignes restent non exécutées après interruption. Rappelez avec le même Idempotency-Key pour les terminer.

pendingcompletedpartialfailed
succeeded_linesinteger
—
total_chargedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
total_linesinteger
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
partner.package_not_visible
409
idempotency.conflict
422
internal.limit_exceeded
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "external_ref": "batch-2026-001",
  "items": [
    {
      "external_ref": "po-1001",
      "package_code": "TKE-VN-30D-X3K9F",
      "quantity": 5
    }
  ]
}
GET/v1/orders/bulk/{id}

Résultat d’une commande par lot

scope orders:readget_orders_bulk__id_

Autorisation requise: orders:read.

Relire les résultats par ligne d’un lot déjà soumis, pour récupérer une réponse perdue sans renvoyer le corps. Un lot d’une autre organisation ou d’un autre environnement (production/test) renvoie 404.

Paramètres du chemin (1)

idstringobligatoire

ID du lot (UUID) renvoyé par POST /v1/orders/bulk.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Résultat d’un lot. SUCCÈS PARTIEL : une ligne en échec ne bloque pas les autres ; seules les lignes done sont débitées. Aucun LPA/QR : lire GET /v1/orders/{id} pour chaque order_id.

created_atstring · date-time
—
external_refstring
peut être null
—
failed_linesinteger
—
idstring · uuid
—
itemsobject[]
—
chargedobject
peut être null
—
errorobject
peut être null

Code d'erreur réel domain.reason, par exemple wallet.insufficient_balance, esim.out_of_stock, order.package_not_allowed, wallet.velocity_exceeded.

codestring
—
messagestring
—
external_refstring
peut être null
—
line_nointeger

À partir de 1, dans l'ordre exact de l'envoi.

order_idstring · uuid
peut être null
—
package_codestring
—
quantityinteger
—
statusstring
—
pendingdonefailed
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
statusstring

pending : des lignes restent non exécutées après interruption. Rappelez avec le même Idempotency-Key pour les terminer.

pendingcompletedpartialfailed
succeeded_linesinteger
—
total_chargedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
total_linesinteger
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

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

eSIM

Enregistrements eSIM, consommation, recharge et annulation

GET/v1/esims

Lister les eSIM (rapprochement par interrogation)

scope esims:readget_esims

Autorisation requise: esims:read.

Paramètres de query (8)

page_sizeintegerfacultatifpar défaut 50min 1max 200

Lignes par page. Valeur par défaut 50, maximum 200. Une valeur supérieure est ramenée à 200 sans erreur.

statusstringfacultatif
Valeurs autorisées :pendingavailablereservedsolddeliveredin_useused_upexpiredsuspendedcancelledprovider_failed

Filtrer par esim_status. Une valeur hors liste renvoie internal.invalid_filter (400), et non une liste vide : une faute comme canceled au lieu de cancelled ne doit pas passer pour une absence d’eSIM.

order_idstring · uuidfacultatif

Uniquement les eSIM d’une même commande.

iccidstringfacultatif

Filtrer par ICCID (chaîne, correspondance exacte).

external_refstringfacultatif

Toutes les eSIM des commandes portant votre référence external_ref (correspondance exacte).

updated_sincestring · date-timefacultatif

Date ISO-8601 UTC. Renvoie les enregistrements où updated_at >= updated_since, dans l’ordre croissant : voie standard de RAPPROCHEMENT PAR INTERROGATION (§8.5), permettant de retrouver l’état même après un webhook manqué. Format incorrect : internal.invalid_filter (400).

cursorstringfacultatif

Curseur suivant : renvoyez next_cursor sans modification. Un curseur mal formé relit le début sans erreur pour préserver les intégrations lors des changements d'encodage. Vérifiez que le curseur change pour éviter une boucle infinie sur la première page.

includestringfacultatif

Blocs supplémentaires séparés par des virgules : lpa retourne install, qr ajoute install.qr_code_base64 (détail uniquement). RETOURNER LE CODE LE DIVULGUE et ferme définitivement l'annulation de chaque eSIM concernée (cancellable.allowed = false, reason = code_disclosed). Demandez-le à la livraison, omettez-le pour conserver l'annulation. Valeur inconnue : internal.invalid_filter (400, field: "include").

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
itemsobject[]
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de lignes correspondant au filtre. null si le comptage coûte trop cher. Ne l’utilisez pas pour détecter la fin : seul next_cursor l’indique.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filter
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
GET/v1/esims/{id}

Lire une eSIM

scope esims:readget_esims__id_

Autorisation requise: esims:read.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Paramètres de query (1)

includestringfacultatif

Blocs supplémentaires séparés par des virgules : lpa retourne install, qr ajoute install.qr_code_base64 (détail uniquement). RETOURNER LE CODE LE DIVULGUE et ferme définitivement l'annulation de chaque eSIM concernée (cancellable.allowed = false, reason = code_disclosed). Demandez-le à la livraison, omettez-le pour conserver l'annulation. Valeur inconnue : internal.invalid_filter (400, field: "include").

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failedinternal.invalid_filter
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/esims/{id}/cancel

Annuler une eSIM inutilisée

scope esims:writeIdempotency-Key obligatoirepost_esims__id__cancel

Autorisation requise: esims:write.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Headers (1)

Idempotency-Keystringobligatoire

Chaîne créée par le client : même clé et même corps exécutés une seule fois, avec résultat initial aux reprises. Portée par api_key_id (§11) : deux clés distinctes sont deux commandes.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
cancelled_atstring · date-time
—
esim_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
refund_suggestedboolean

true : remboursement en attente d'approbation humaine, argent non transféré. refunded.amount = 0 ne signifie pas absence de remboursement.

refundedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
statusstring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
409
esim.not_cancellableidempotency.conflict
422
capability.not_supported
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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)"

Exemple de réponse

json
{
  "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
}
POST/v1/esims/{id}/change-plan

Recharger une eSIM

scope esims:writeIdempotency-Key obligatoirepost_esims__id__change-plan

Autorisation requise: esims:write.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Headers (1)

Idempotency-Keystringobligatoire

Chaîne créée par le client : même clé et même corps exécutés une seule fois, avec résultat initial aux reprises. Portée par api_key_id (§11) : deux clés distinctes sont deux commandes.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
package_codestring
obligatoire

Code de forfait de VOTRE catalogue, soit public_sku. IMMUABLE par forfait, même si le fournisseur change ; utilisable comme clé de rapprochement. L’exemple a le bon format mais ne fonctionne PAS : lire un vrai code via GET /v1/catalog/packages.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Résultat du changement de forfait eSIM.

chargedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
current_planobject
—
codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
esim_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
order_idstring · uuid
—
previous_planobject
peut être null
—
cancelled_atstring · date-time
peut être null
—
codestring
—
remaining_bytes_forfeitedinteger · int64
peut être null
—
warningstring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
402
wallet.insufficient_balance
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
partner.package_not_visible
409
esim.topup_not_supportedesim.topup_provider_mismatchesim.not_eligibleidempotency.conflict
422
capability.not_supported
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "package_code": "TKE-VN-7D-P4M2Q"
}
GET/v1/esims/{id}/plan-options

Forfaits de recharge valides pour cette eSIM

scope esims:readget_esims__id__plan-options

Autorisation requise: esims:read.

Renvoie les recharges valides pour CETTE eSIM. Le serveur déduit le fournisseur de son id et ne renvoie que ses forfaits. Aucun provider_id n’est exposé sur /v1 : ce filtrage empêche structurellement de choisir un forfait d’un autre fournisseur. Envoyer à POST /v1/esims/{id}/change-plan un code absent de cette liste renvoie 409 esim.topup_provider_mismatch.

Une eSIM sans fournisseur associé (import brut ou origine inconnue) renvoie une liste vide (total: 0), résultat normal et non une erreur.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Paramètres de query (2)

page_sizeintegerfacultatifpar défaut 50min 1max 200

Lignes par page. Valeur par défaut 50, maximum 200. Une valeur supérieure est ramenée à 200 sans erreur.

cursorstringfacultatif

Curseur suivant : renvoyez next_cursor sans modification. Un curseur mal formé relit le début sans erreur pour préserver les intégrations lors des changements d'encodage. Vérifiez que le curseur change pour éviter une boucle infinie sur la première page.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire

Forfaits de recharge disponibles pour cette eSIM, filtrés par son fournisseur. Chaque entrée est un Package complet, de même structure que le catalogue. policy décrit le fournisseur actuel de l'eSIM ; null signifie politique inconnue, pas absence d'annulation.

itemsobject[]
—
capabilitiesobject

Capacités après-vente déclarées par le forfait, jamais déduites du type de fournisseur. Sans déclaration, tous les indicateurs sont false, pour ne pas promettre une action renvoyant forcément 422 capability.not_supported.

cancelobject
—
restrictionstring
peut être null

code_not_disclosed : annulation uniquement si personne n'a vu le code (ni client, ni tableau de bord, ni ?include=lpa). Condition réelle de l'annulation, sans lien avec l'installation, que le système ne peut pas connaître. L'ancien libellé unused_only était trompeur.

code_not_disclosednull
supportedboolean
—
plan_changeobject
—
policyobject
peut être null

Politique du fournisseur lors de la RECHARGE AVEC UN NOUVEAU FORFAIT. null signifie que la recharge est impossible OU que cette politique n’a pas été mesurée. null interdit de supposer ; un objet entièrement false garantirait au contraire qu’aucun forfait n’est annulé.

……

Champs plus imbriqués : consultez le JSON exemple.

supportedboolean
—
usage_checkobject
—
supportedboolean
—
codestring

Code de forfait de VOTRE catalogue, soit public_sku. IMMUABLE par forfait, même si le fournisseur change ; utilisable comme clé de rapprochement. L’exemple a le bon format mais ne fonctionne PAS : lire un vrai code via GET /v1/catalog/packages.

coverageobject

Zones où le forfait fonctionne.

countriesstring[]

Codes ISO-3166 alpha-2. Tableau vide pour les forfaits mondiaux (sans énumération de 200 pays) et les forfaits sans couverture déclarée.

country_countinteger

Nombre de pays couverts, pour trier ou filtrer les forfaits multi-pays sans tout télécharger ni compter vous-même le tableau.

groupstring

Couverture : pays (country), région (region) ou monde (global). Énumération ouverte : ignorer les valeurs inconnues.

regionstring
peut être null

Toujours null : les régions sont associées aux pays, pas aux forfaits. Le filtre ?region= reste utilisable.

dataobject

Quota de données du forfait. Toutes les valeurs sont en OCTETS, pas en Mo/Go, pour éviter un double arrondi.

daily_bytesinteger · int64
peut être null

Quota QUOTIDIEN en octets. null pour les forfaits fixed ou unlimited.

labelstring

Libellé lisible généré par la même fonction que dans toutes nos interfaces.

modelstring

Mode de calcul : daily (quota quotidien), fixed (total sur la période), unlimited. ÉNUMÉRATION OUVERTE : les clients DOIVENT ignorer les valeurs inconnues.

dailyfixedunlimited
resetobject
peut être null

Moment de remise à zéro du quota quotidien, important pour une vente en fin de journée. null = non déclaré.

atstring
peut être null

Heure de remise à zéro dans le fuseau timezone (00:00, 00:00:01).

notestring
peut être null
—
timezonestring
peut être null

Fuseau horaire de la remise à zéro (UTC+8, UTC+7).

total_bytesinteger · int64
peut être null

Quota total pour la période, en octets. null pour daily ou unlimited : multiplier un quota journalier par le nombre de jours ne crée pas un engagement fournisseur.

descriptionstring
peut être null

Description commerciale du forfait. null si non rédigée.

description_enstring
peut être null

Description anglaise ; null si non fournie.

featuresobject

Fonctionnalités du forfait dans un objet typé, pas un tableau : false et null distinguent l'absence de partage Wi-Fi de l'information inconnue, contrairement à [].

appsobject[]

État de prise en charge par SYSTÈME D’EXPLOITATION.

Une application ABSENTE du tableau signifie information inconnue, PAS compatibilité. Seules les entrées présentes sont déclarées par le fournisseur ; actuellement seul TikTok est déclaré.

L’absence de drapeau global est volontaire : sur le catalogue réel, 2 465 forfaits prennent en charge TikTok sur iPhone mais PAS sur Android. Un drapeau unique les classerait tous « non », faussant près d’un tiers du catalogue.

androidstring

supported (compatibilité déclarée), blocked (incompatibilité déclarée), unknown (OS non couvert par la déclaration). Énumération ouverte.

supportedblockedunknown
appstring

Code d'application en minuscules (ex. tiktok).

iosstring

supported (compatibilité déclarée), blocked (incompatibilité déclarée), unknown (OS non couvert par la déclaration). Énumération ouverte.

supportedblockedunknown
notestring
peut être null
—
hotspotboolean
peut être null

Partage Wi-Fi avec d'autres appareils. null = non déclaré, différent de false.

kyc_requiredboolean
peut être null

Vérification d’identité (KYC) requise avant utilisation. null = non déclaré.

smsboolean
peut être null

SMS. false signifie AUCUNE réception de SMS, donc aucun OTP bancaire. null = non déclaré.

voiceboolean
peut être null

Appels vocaux. false signifie absence de voix déclarée : une ligne de données seule ne reçoit pas d'appels. null = non déclaré.

fupobject
peut être null

Comportement après épuisement du quota à pleine vitesse. null = aucune politique de réduction déclarée.

behaviorstring
peut être null

throttle (débit réduit), cutoff (déconnexion) ou null (non déclaré). Énumération ouverte.

throttlecutoffnull
high_speed_bytesinteger · int64
peut être null

Octets à pleine vitesse avant réduction du débit.

notestring
peut être null
—
speed_kbpsinteger
peut être null

Débit APRÈS épuisement du quota rapide (kbps). null si la ligne est coupée ou l’information non déclarée. Distinct de network.speed_cap_kbps, plafond du forfait entier.

marketstring

SYSTÈME DE FOURNITURE du forfait, énumération OUVERTE : ignorer les valeurs inconnues. Détermine la présence d’un stock et les opérations après-vente. domestic vend des kits avec recharge, annulation et consultation d’usage ; international commande au fournisseur à la demande, sans stock.

domesticinternational
namestring
—
name_enstring
peut être null

Nom anglais ; null si non fourni.

networkobject

Réseaux utilisables par le forfait.

apnstring
peut être null

APN à saisir manuellement sur certains appareils Android sans configuration automatique. Nécessaire pour rédiger le guide d’installation et éviter des demandes d’assistance. null = non déclaré.

carriersobject[]

Opérateurs réseau utilisés par le forfait. Il s’agit d’une caractéristique de l’eSIM (réseau radio), pas du fournisseur.

namestring
—
generationsstring[]
peut être null

Générations réseau utilisables (["4g", "5g"]). null = non déclaré, et NON « 2G uniquement ».

speed_cap_kbpsinteger
peut être null

Plafond de débit du forfait ENTIER (kbps), distinct de fup.speed_kbps, applicable après épuisement du quota rapide.

priceobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
tagsstring[]

Étiquettes commerciales normalisées (minuscules, tirets), également utilisées par les restrictions de catalogue de votre clé. Aucun forfait n’est étiqueté actuellement ; ?tag= renvoie donc une liste vide jusqu’à l’ajout d’étiquettes par le vendeur.

validityobject

Durée de validité du forfait et début du décompte.

daysinteger
—
install_by_daysinteger
peut être null

Délai en jours pour scanner le QR code et installer l’eSIM après achat. null = non déclaré, PAS « installation sans limite de temps ».

labelstring
—
reclaim_on_expiryboolean
peut être null

Le fournisseur récupère l'abonnement à expiration : recharge et changement deviennent impossibles. Important pour les renouvellements. null = non déclaré.

starts_whenstring
peut être null

Début du décompte : on_install (installation), on_purchase (achat), ou null. Information essentielle pour calculer l'expiration : avec null, ne promettez pas de date précise.

on_installon_purchasenull
unused_valid_daysinteger
peut être null

Nombre de jours avant expiration si le forfait reste inutilisé en stock. null = non déclaré, PAS illimité. Distinct de install_by_days : l’un concerne la CONSERVATION, l’autre la date limite d’INSTALLATION.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

policyobject
peut être null

Politique du fournisseur lors de la RECHARGE AVEC UN NOUVEAU FORFAIT. null signifie que la recharge est impossible OU que cette politique n’a pas été mesurée. null interdit de supposer ; un objet entièrement false garantirait au contraire qu’aucun forfait n’est annulé.

notestring
peut être null
—
requires_cancel_firstboolean

Le forfait EN COURS est annulé avant l’achat du nouveau : ce fournisseur ne cumule pas les forfaits. Informez le client AVANT qu’il lance la recharge d’un nouveau forfait.

stackingboolean

Indique si durée et quota du nouveau forfait s'ajoutent au forfait en cours.

totalinteger · int64
peut être null

Nombre de forfaits correspondant aux filtres ; null si le comptage est coûteux (international ou sans market). Calculé avant restrictions de clé et prix organisation : ce total décrit le catalogue, pas les forfaits achetables.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
GET/v1/esims/{id}/refund-requests

Historique des demandes de remboursement eSIM (membres uniquement)

scope esims:readget_esims__id__refund-requests

Autorisation requise: esims:read.

Clés membre réelles autorisées uniquement. Renvoie au maximum 50 demandes pour l’eSIM du membre, les plus récentes d’abord ; next_cursor et total valent null. has_open_request signale une validation en attente. Les clés organisation et test reçoivent capability.not_supported.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
has_open_requestboolean
—
itemsobject[]
—
decided_atstring · date-time
peut être null
—
decision_notestring
peut être null
—
idstring · uuid
—
order_idstring · uuid
—
order_item_idstring · uuid
—
reasonstring
—
refundedobject
peut être null
—
requested_atstring · date-time
—
statusstring
—
pendingapprovedrejected
suggestedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de lignes correspondant au filtre. null si le comptage coûte trop cher. Ne l’utilisez pas pour détecter la fin : seul next_cursor l’indique.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
422
capability.not_supported
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/esims/{id}/refund-requests

Créer une demande de remboursement eSIM (membres uniquement)

scope esims:writeIdempotency-Key obligatoirepost_esims__id__refund-requests

Autorisation requise: esims:write.

Clés membre réelles autorisées uniquement. Soumettez une demande pour l’eSIM du membre avec Idempotency-Key ; aucun transfert avant validation interne. Les clés organisation ou test reçoivent capability.not_supported. En test, annulez une eSIM pour essayer le remboursement virtuel.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Headers (1)

Idempotency-Keystringobligatoire

Chaîne créée par le client : même clé et même corps exécutés une seule fois, avec résultat initial aux reprises. Portée par api_key_id (§11) : deux clés distinctes sont deux commandes.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
reasonstring
obligatoire

Motif du remboursement, de 5 à 1000 caractères après suppression des espaces en début et fin.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
decided_atstring · date-time
peut être null
—
decision_notestring
peut être null
—
idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
order_idstring · uuid
—
order_item_idstring · uuid
—
reasonstring
—
refundedobject
peut être null
—
requested_atstring · date-time
—
statusstring
—
pendingapprovedrejected
suggestedobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
internal.not_found
409
esim.refund_not_eligibleesim.refund_request_pendingidempotency.conflict
422
capability.not_supported
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "reason": "<reason>"
}
GET/v1/esims/{id}/usage

Données utilisées

scope esims:readget_esims__id__usage

Autorisation requise: esims:read.

Lit les données consommées et restantes d’une eSIM, avec l’horodatage updated_at des chiffres (fraîcheur réelle). Pour une eSIM d’un fournisseur lu uniquement à la demande (actuellement SkyFi), cet appel met un scan en file d’attente et ATTEND le résultat jusqu’à environ 15 secondes ; la réponse peut donc prendre jusqu’à ~15 secondes et les chiffres renvoyés proviennent du scan qui vient de se terminer. Si le scan échoue, si l’eSIM a été scannée depuis moins de 15 secondes ou si l’attente expire, AUCUNE erreur n’est renvoyée : les derniers chiffres enregistrés sont renvoyés, vérifiez updated_at. Les autres fournisseurs renvoient les chiffres enregistrés, actualisés périodiquement.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
esim_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
422
capability.not_supported
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

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

Portefeuille

Solde du portefeuille

GET/v1/wallet/balance

Solde du portefeuille

scope wallet:readget_wallet_balance

Autorisation requise: wallet:read.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
availableobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
balanceobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
credit_limitobject

Montant monétaire : même structure partout sur /v1, jamais d'alternance entre objet et nombre brut.

amountinteger · int64

Entier dans la plus petite unité de currency (le VND n’a pas de sous-unité).

currencystring
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "data": {
    "available": {
      "amount": 273000,
      "currency": "VND"
    },
    "balance": {
      "amount": 273000,
      "currency": "VND"
    },
    "credit_limit": {
      "amount": 273000,
      "currency": "VND"
    },
    "mode": "live"
  },
  "ok": true
}

Webhooks

Configuration webhook, journal d'envoi et simulateur

POST/v1/testmode/webhooks/fail-next

Simuler le prochain échec webhook (clés de test uniquement)

scope webhook:managepost_testmode_webhooks_fail-next

Autorisation requise: webhook:manage.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
countinteger
—

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
endpoint_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
testmode_fail_nextinteger
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
404
internal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

json
{
  "data": {
    "endpoint_id": "018f2b00-0000-7000-8000-000000000000",
    "mode": "live",
    "testmode_fail_next": 1
  },
  "ok": true
}

Exemple de corps

json
{
  "count": 1
}
POST/v1/testmode/webhooks/fire

Envoyer un événement webhook de test (clés de test uniquement)

scope webhook:managepost_testmode_webhooks_fire

Autorisation requise: webhook:manage.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
dataobject

Remplacer partiellement le champ data de la charge utile d’exemple.

event_typestring
obligatoire

Type d'événement webhook à simuler.

order.completedorder.failedesim.deliveredesim.activatedesim.usage_thresholdesim.expiringwallet.low_balancecatalog.updated

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
delivery_idstring · uuid
—
event_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
404
webhook.not_configuredinternal.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

json
{
  "data": {
    "delivery_id": "018f2b00-0000-7000-8000-000000000000",
    "event_id": "018f3c00-0000-7000-8000-0000000000b0",
    "mode": "live"
  },
  "ok": true
}

Exemple de corps

json
{
  "data": {},
  "event_type": "order.completed"
}
GET/v1/webhooks

Lister les points de terminaison webhook

scope webhook:manageget_webhooks

Autorisation requise: webhook:manage.

Endpoints de la clé appelante uniquement. Aucune clé ne voit l’endpoint d’une autre, même dans la même organisation (§5.11 : liaison à api_key_id, sans partage entre production et test).

Sans configuration : 200 avec un tableau vide, pas 404. La collection existe toujours, même sans élément.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
itemsobject[]
—
activeboolean

false : aucun nouvel événement accepté, après suspension dans le tableau de bord ou révocation de clé (arrêt dans la même transaction). Un PUT /v1/webhooks/{id} validé par CHECK_HEALTH le réactive.

created_atstring · date-time
—
eventsstring[]

Vide = tout recevoir.

idstring · uuid

ID de l’endpoint pour /v1/webhooks/{id}. Un NOUVEL ID est généré à chaque création : supprimer puis recréer produit un autre endpoint, pas la restauration de l’ancien.

last_delivery_atstring · date-time
peut être null

Dernier envoi via cet endpoint. null = jamais envoyé, distinct d’un échec d’envoi ; à vérifier d’abord lorsqu’un partenaire ne reçoit aucun webhook.

secret_last4string

Quatre derniers caractères du secret webhook, pour vérifier que votre secret correspond à celui utilisé pour signer sans le révéler. Le secret complet n’est affiché qu’UNE FOIS, à la création.

urlstring
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de lignes correspondant au filtre. null si le comptage coûte trop cher. Ne l’utilisez pas pour détecter la fin : seul next_cursor l’indique.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/webhooks

Créer un endpoint webhook

scope webhook:managepost_webhooks

Autorisation requise: webhook:manage.

Créer un endpoint pour la clé appelante. Avant sauvegarde, le serveur envoie un POST signé ping à l’URL (CHECK_HEALTH). Sans 2xx, rien n’est sauvegardé : 422 webhook.health_check_failed. URL publique https obligatoire ; adresse interne ou de métadonnées : 422 webhook.url_not_allowed.

webhook_secret n’est affiché qu’UNE FOIS et ne peut être relu. En cas de perte, effectuer une rotation dans le tableau de bord ; l’ancien secret reste valide pendant 24 h (§8.4).

Un endpoint existe déjà : 409 webhook.already_configured, même en pause (is_active: false). La pause conserve URL, secret et journal. Pour recommencer, utiliser DELETE /v1/webhooks/{id}, puis créer un nouvel endpoint.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
eventsstring[]

Vide = tout recevoir. Valeur inconnue : 400 internal.validation_failed (champ events).

urlstring · uri
obligatoire
—

Réponse

201Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
activeboolean

false : aucun nouvel événement accepté, après suspension dans le tableau de bord ou révocation de clé (arrêt dans la même transaction). Un PUT /v1/webhooks/{id} validé par CHECK_HEALTH le réactive.

created_atstring · date-time
—
eventsstring[]

Vide = tout recevoir.

health_checkstring
—
passed
idstring · uuid

ID de l’endpoint pour /v1/webhooks/{id}. Un NOUVEL ID est généré à chaque création : supprimer puis recréer produit un autre endpoint, pas la restauration de l’ancien.

last_delivery_atstring · date-time
peut être null

Dernier envoi via cet endpoint. null = jamais envoyé, distinct d’un échec d’envoi ; à vérifier d’abord lorsqu’un partenaire ne reçoit aucun webhook.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
secret_last4string

Quatre derniers caractères du secret webhook, pour vérifier que votre secret correspond à celui utilisé pour signer sans le révéler. Le secret complet n’est affiché qu’UNE FOIS, à la création.

urlstring
—
webhook_secretstring

Affiché UNE SEULE FOIS à la création (préfixe whsec_).

okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
409
webhook.already_configured
422
webhook.health_check_failedwebhook.url_not_allowed
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "events": [
    "order.completed"
  ],
  "url": "https://partner.example.com/hooks/tke"
}
GET/v1/webhooks/{id}

Lire un point de terminaison webhook

scope webhook:manageget_webhooks__id_

Autorisation requise: webhook:manage.

Paramètres du chemin (1)

idstringobligatoire

Identifiant de l'endpoint webhook (id de POST /v1/webhooks ou GET /v1/webhooks).

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
activeboolean

false : aucun nouvel événement accepté, après suspension dans le tableau de bord ou révocation de clé (arrêt dans la même transaction). Un PUT /v1/webhooks/{id} validé par CHECK_HEALTH le réactive.

created_atstring · date-time
—
eventsstring[]

Vide = tout recevoir.

idstring · uuid

ID de l’endpoint pour /v1/webhooks/{id}. Un NOUVEL ID est généré à chaque création : supprimer puis recréer produit un autre endpoint, pas la restauration de l’ancien.

last_delivery_atstring · date-time
peut être null

Dernier envoi via cet endpoint. null = jamais envoyé, distinct d’un échec d’envoi ; à vérifier d’abord lorsqu’un partenaire ne reçoit aucun webhook.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
secret_last4string

Quatre derniers caractères du secret webhook, pour vérifier que votre secret correspond à celui utilisé pour signer sans le révéler. Le secret complet n’est affiché qu’UNE FOIS, à la création.

urlstring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
webhook.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
PUT/v1/webhooks/{id}

Modifier un endpoint webhook

scope webhook:manageput_webhooks__id_

Autorisation requise: webhook:manage.

Paramètres du chemin (1)

idstringobligatoire

Identifiant de l'endpoint webhook (id de POST /v1/webhooks ou GET /v1/webhooks).

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
eventsstring[]

Vide = tout recevoir. Valeur inconnue : 400 internal.validation_failed (champ events).

urlstring · uri
obligatoire
—

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
activeboolean

false : aucun nouvel événement accepté, après suspension dans le tableau de bord ou révocation de clé (arrêt dans la même transaction). Un PUT /v1/webhooks/{id} validé par CHECK_HEALTH le réactive.

created_atstring · date-time
—
eventsstring[]

Vide = tout recevoir.

idstring · uuid

ID de l’endpoint pour /v1/webhooks/{id}. Un NOUVEL ID est généré à chaque création : supprimer puis recréer produit un autre endpoint, pas la restauration de l’ancien.

last_delivery_atstring · date-time
peut être null

Dernier envoi via cet endpoint. null = jamais envoyé, distinct d’un échec d’envoi ; à vérifier d’abord lorsqu’un partenaire ne reçoit aucun webhook.

modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
secret_last4string

Quatre derniers caractères du secret webhook, pour vérifier que votre secret correspond à celui utilisé pour signer sans le révéler. Le secret complet n’est affiché qu’UNE FOIS, à la création.

urlstring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
webhook.not_found
422
webhook.health_check_failedwebhook.url_not_allowed
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "events": [
    "order.completed"
  ],
  "url": "https://partner.example.com/hooks/tke"
}
DELETE/v1/webhooks/{id}

Supprimer un endpoint webhook (arrêter les nouveaux événements)

scope webhook:managedelete_webhooks__id_

Autorisation requise: webhook:manage.

Supprimer l’endpoint arrête les nouveaux événements de la clé appelante.

Le webhook_secret est perdu avec l’endpoint. Une recréation par POST /v1/webhooks génère un NOUVEL id et un NOUVEAU secret : adaptez votre vérification. Pour suspendre temporairement en conservant le secret, utilisez Pause dans le tableau de bord.

Le journal complet reste disponible pour le rapprochement, mais les envois de cet endpoint supprimé ne peuvent plus être rejoués.

Les envois pending en attente de réessai s’arrêtent avec endpoint_disabled et restent journalisés (§8.5).

Un id d’une autre clé ou déjà supprimé renvoie 404 webhook.not_found, jamais un 200 silencieux.

Paramètres du chemin (1)

idstringobligatoire

Identifiant de l'endpoint webhook (id de POST /v1/webhooks ou GET /v1/webhooks).

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
deleted_atstring · date-time
—
idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
webhook.not_found
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "data": {
    "deleted_at": "2026-07-11T08:12:30Z",
    "id": "018f2b00-0000-7000-8000-00000000009a",
    "mode": "live"
  },
  "ok": true
}
GET/v1/webhooks/deliveries

Journal des envois webhook

scope webhook:manageget_webhooks_deliveries

Autorisation requise: webhook:manage.

Paramètres de query (5)

pageintegerfacultatifpar défaut 1min 1

Numéro de page à partir de 1. Au-delà des données : items: [] et le vrai total (200, pas 404).

page_sizeintegerfacultatifpar défaut 50min 1max 200

Lignes par page. Valeur par défaut 50, maximum 200. Une valeur supérieure est ramenée à 200 sans erreur.

event_typestringfacultatif

Filtrer par type d’événement (voir le catalogue des événements webhook).

statusstringfacultatif
Valeurs autorisées :pendingdeliveredfailed

Filtrer par état d’envoi. ?status=failed sert au rapprochement des webhooks par interrogation (§13). Une valeur inconnue est IGNORÉE sans erreur : le journal complet est renvoyé.

event_idstring · uuidfacultatif

Toutes les tentatives d'envoi d'un événement, reprises incluses.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
itemsobject[]
—
attemptsinteger
—
created_atstring · date-time
—
event_idstring · uuid

Identifiant de l’ÉVÉNEMENT. Un rejeu crée un NOUVEL envoi avec le MÊME event_id : dédupliquez sur event_id, pas sur l’id d’envoi.

event_typestring
—
idstring · uuid
—
last_attempt_atstring · date-time
peut être null
—
next_retry_atstring · date-time
peut être null
—
response_codeinteger
peut être null
—
statusstring
—
pendingdeliveredfailed
webhook_idstring · uuid
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_cursorstring
peut être null

Renvoyez le curseur SANS MODIFICATION pour charger la page suivante. null est le SEUL signal de fin : une page plus courte que page_size peut être suivie d’autres pages.

page_sizeinteger
peut être null

Nombre MAXIMUM d’enregistrements utilisé par le serveur pour cette page, déjà plafonné ; ce n’est pas le nombre réel d’éléments dans items.

totalinteger · int64
peut être null

Nombre de lignes correspondant au filtre. null si le comptage coûte trop cher. Ne l’utilisez pas pour détecter la fin : seul next_cursor l’indique.

warningsobject[]

Explications de la liste ; vide si rien d'inhabituel. Codes fermés : filtered_by_key_package_filters, filtered_by_provider_scope, no_price_for_org, price_filter_removed_rows, provider_disabled, capability_missing. Ils indiquent restrictions de clé, périmètre fournisseur, absence de prix, filtre de prix, fournisseur désactivé ou capacité absente. count donne le nombre de lignes concernées, sinon null.

codestring
—
filtered_by_key_package_filtersfiltered_by_provider_scopeno_price_for_orgprice_filter_removed_rowsprovider_disabledcapability_missing
countinteger · int64
peut être null
—
messagestring
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
webhook.not_configured
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

json
{
  "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
}
POST/v1/webhooks/deliveries/{id}/replay

Renvoyer un webhook

scope webhook:managepost_webhooks_deliveries__id__replay

Autorisation requise: webhook:manage.

Paramètres du chemin (1)

idstringobligatoire

ID d’un envoi webhook (items[].id de GET /v1/webhooks/deliveries).

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
attemptsinteger
—
created_atstring · date-time
—
event_idstring · uuid

Identifiant de l’ÉVÉNEMENT. Un rejeu crée un NOUVEL envoi avec le MÊME event_id : dédupliquez sur event_id, pas sur l’id d’envoi.

event_typestring
—
idstring · uuid
—
last_attempt_atstring · date-time
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
next_retry_atstring · date-time
peut être null
—
response_codeinteger
peut être null
—
statusstring
—
pendingdeliveredfailed
webhook_idstring · uuid
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspended
404
webhook.delivery_not_foundwebhook.not_configured
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

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

Mode test

Utilisez uniquement les clés test tke_test_ ou tke_member_test_ sur api-testing. Simulez les données et eSIM du propriétaire sans fonds ni fournisseurs réels. Le mode test membre ne prend pas en charge webhooks, commandes groupées ou demandes de remboursement à valider.

POST/v1/testmode/esims/{id}/simulate

Simuler le cycle de vie d’une eSIM de test (clés de test uniquement)

scope esims:writepost_testmode_esims__id__simulate

Autorisation requise: esims:write.

Clés test tke_test_ ou tke_member_test_ uniquement. Appliquez un scénario à l’eSIM test du propriétaire : consommation, installation, expiration, débit limité ou rejet simulé de la prochaine opération. Le corps doit contenir au moins un champ ; sinon 422 testmode.invalid_scenario. Consultez le résultat par GET /v1/esims/{id} et /usage comme en mode réel. Les politiques de changement de forfait et d’annulation sont communes, sans appel au fournisseur réel.

Paramètres du chemin (1)

idstringobligatoire

Identifiant stable d'une eSIM vendue, différent de l'ICCID qui peut être réutilisé.

Corps de la requêteobligatoireapplication/json

ChampTypeContraintesDescription
data_used_mbinteger · int64

Données déjà utilisées en Mo (1 MiB binaire). Une valeur dépassant le quota est plafonnée : c'est le scénario d'épuisement, pas une erreur.

expire_atstring · date-time

Définir une date d’expiration précise (ISO-8601 UTC).

expire_nowboolean

true : faire expirer le forfait immédiatement (esim_status devient expired).

provider_rejectboolean

Simuler un refus fournisseur à la prochaine opération (recharge ou annulation).

statusstring

Cycle de vie TECHNIQUE du profil eSIM, selon le vocabulaire du fournisseur. Distinct du cycle commercial esim_status renvoyé par les endpoints eSIM.

releasedinstalledenableddisabled
throttledboolean

true : ligne ralentie (forfait illimité ayant dépassé le quota à pleine vitesse).

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
cancellableobject

Possibilité d'annuler l'eSIM et motif du refus ; à lire avant POST /v1/esims/{order_item_id}/cancel. Ce bloc est toujours présent, jamais null : c'est une réponse.

allowedboolean
—
disclosed_atstring · date-time
peut être null

Moment où le code a quitté nos systèmes (ISO-8601 UTC), null tant qu'il n'a été vu par personne. Trace utile en cas de contestation de la divulgation empêchant l'annulation.

reasonstring
peut être null

Motif du refus d'annulation : code_disclosed (code envoyé ou affiché), wrong_status (état incompatible), expired (forfait expiré), not_supported (capacité non déclarée). null si allowed = true. Même vocabulaire que meta.reason pour esim.not_cancellable.

code_disclosedwrong_statusexpirednot_supportednull
external_refstring
peut être null

Votre référence de commande (external_ref envoyé à l'achat), pour rattacher l'eSIM à votre commande. null si absente.

iccidstring
peut être null
—
idstring · uuid

ID de l’eSIM, utilisé dans tous les chemins /v1/esims/{id}/… et comme order_item_id des webhooks. Avec seulement l’ICCID, appeler GET /v1/esims?iccid=… pour obtenir l’id.

installobject
peut être null

Toutes les données nécessaires à l’INSTALLATION de cette eSIM. null signifie que vous ne les avez PAS DEMANDÉES (sans include=lpa). Si l’eSIM n’a pas encore de code, le bloc est présent avec lpa: null.

Une réponse contenant ce bloc porte toujours Cache-Control: no-store.

android_urlstring
peut être null
—
apnstring
peut être null

Nom du point d’accès (APN) à saisir si la configuration automatique échoue. Toujours null dans cette version : null signifie inconnu, pas inutile.

ios_urlstring
peut être null
—
lpastring
peut être null

Code d’activation de l’eSIM, secret commercialisable. Cette chaîne suffit pour générer votre QR code.

matching_idstring
peut être null
—
qr_code_base64string
peut être null

QR code en data-URI PNG. Présent seulement avec include=qr sur un endpoint de DÉTAIL ; jamais dans les listes (environ 11 Ko par ligne, soit 1,1 Mo pour 100 lignes). Générez plutôt le QR à partir de lpa.

smdp_addressstring
peut être null
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
msisdnstring
peut être null
—
order_idstring · uuid
—
package_codestring
peut être null

CODE du forfait catalogue, utilisé pour rattacher l’eSIM à votre catalogue. null uniquement si le forfait source a été supprimé du catalogue.

package_namestring

NOM d’affichage lisible, susceptible de changer. Ne PAS l’utiliser comme clé de rapprochement.

planobject
peut être null

Le forfait actuellement appliqué à cette eSIM.

current_codestring
—
expires_atstring · date-time
peut être null
—
started_atstring · date-time
peut être null
—
simulatedobject

État simulé après application du scénario.

esim_statestring

Cycle TECHNIQUE de l’eSIM de test. En production il provient de smdp_status, mais une eSIM de test n’a aucun profil SM-DP+ ; il figure donc ici avec les autres paramètres propres au test.

releasedinstalledenableddisabled
last_viastring

Flux ayant MODIFIÉ cette eSIM le plus récemment.

open_apisimulateseed
viastring

Flux ayant CRÉÉ cette eSIM de test.

open_apisimulateseed
smdp_statusstring
peut être null
—
statusstring
—
updated_atstring · date-time
—
usageobject
peut être null

Statistiques d'utilisation des données de l'eSIM. updated_at = null si jamais interrogée.

expires_atstring · date-time
peut être null
—
remaining_bytesinteger · int64
peut être null
—
sourcestring
peut être null
—
pollwebhook
total_bytesinteger · int64
peut être null
—
updated_atstring · date-time
peut être null
—
used_bytesinteger · int64
peut être null
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
404
internal.not_found
422
testmode.invalid_scenario
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

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

Exemple de corps

json
{
  "data_used_mb": 4096,
  "expire_at": "2026-07-11T08:12:30Z",
  "expire_now": true,
  "provider_reject": true,
  "status": "enabled",
  "throttled": true
}
POST/v1/testmode/reset

Réinitialiser les données de test (clés de test uniquement)

scope orders:writepost_testmode_reset

Autorisation requise: orders:write.

Headers (1)

Idempotency-Keystringfacultatif

Corps de la requêtefacultatifapplication/json

ChampTypeContraintesDescription
reasonstring
—

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
esim_states_deletedinteger · int64
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
order_items_deletedinteger · int64
—
orders_deletedinteger · int64
—
wallet_balanceinteger · int64

Solde du portefeuille test APRÈS réinitialisation, toujours 0. Le grand livre n’est PAS supprimé : une écriture compensatrice remet le solde à zéro.

okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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"

Exemple de réponse

json
{
  "data": {
    "esim_states_deleted": 1,
    "mode": "live",
    "order_items_deleted": 1,
    "orders_deleted": 1,
    "wallet_balance": 1
  },
  "ok": true
}

Exemple de corps

json
{
  "reason": "Reset test data"
}
POST/v1/testmode/seed

Créer les données d’exemple du mode test (clés de test uniquement)

scope orders:writepost_testmode_seed

Autorisation requise: orders:write.

Headers (1)

Idempotency-Keystringfacultatif

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
already_seededboolean
—
esims_createdinteger
—
modestring

Environnement de la clé appelante, identique à X-TKE-Mode. Les organisations utilisent tke_live_/tke_test_ et les membres tke_member_live_/tke_member_test_. Les hôtes réel et test imposent le mode de la clé.

testlive
orders_createdinteger
—
seededboolean
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
422
testmode.no_sellable_package
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -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)"

Exemple de réponse

json
{
  "data": {
    "already_seeded": true,
    "esims_created": 1,
    "mode": "live",
    "orders_created": 1,
    "seeded": true
  },
  "ok": true
}
GET/v1/testmode/status

Vue d’ensemble du mode test (clés de test uniquement)

scope esims:readget_testmode_status

Autorisation requise: esims:read.

Réponse

200Succès

Succès

ChampTypeContraintesDescription
dataobject
obligatoire
—
base_url_testingstring

Hôte du mode test. Utilisez uniquement des clés tke_test_ ou tke_member_test_ ; un appel à l’hôte réel renvoie 403 partner.wrong_host.

capabilitiesobject
—
bulk_ordersboolean
—
cancel_refundboolean
—
refund_requestsboolean
—
webhooksboolean
—
checklistobject[]

Les étapes sont calculées à partir des appels API réels du même propriétaire en mode test. Les webhooks concernent uniquement les organisations.

doneboolean
—
labelstring
—
last_atstring · date-time
peut être null
—
stepstring
—
driftobject

Boucle de consommation simulée : activation, intervalle et dernière exécution. Les webhooks de seuil concernent uniquement les organisations compatibles.

enabledboolean
—
interval_secsinteger · int64
—
last_run_atstring · date-time
peut être null
—
modestring
—
testlive
readyboolean

Indique si le propriétaire de la clé a terminé toutes les étapes prises en charge. Les membres ne doivent pas tester les webhooks.

sample_countsobject
—
test_esimsinteger · int64
—
test_ordersinteger · int64
—
seededboolean

Indique si le propriétaire de la clé dispose déjà de commandes et d’eSIM de test.

test_key_countinteger · int64
—
wallet_balanceinteger · int64

Solde du portefeuille test en VND virtuels du propriétaire de la clé, séparé du portefeuille réel.

webhookobject
—
endpoint_urlstring
peut être null
—
fail_nextinteger
—
statusstring
peut être null
—
okboolean
obligatoire
—
true

Réponses d’erreur

Toutes les erreurs retournent { ok: false, error } avec le bon statut HTTP, jamais 200. Détails sur la page des erreurs.

400
internal.validation_failed
401
partner.invalid_keypartner.invalid_signaturepartner.timestamp_expiredpartner.nonce_reused
403
partner.ip_not_allowedpartner.scope_deniedpartner.key_suspendedpartner.key_revokedorg.suspendedtestmode.live_key_forbidden
429
partner.rate_limited
503
partner.endpoint_disabledinternal.service_unavailable

Exemple d’appel

Toutes les valeurs TKE-VN-30D-X3K9F sont à remplacer. Obtenez les codes réels via GET /v1/catalog/packages.

bash
#!/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: fr" \
  -H "X-TKE-Key: $KEY_ID" \
  -H "X-TKE-Timestamp: $TS" \
  -H "X-TKE-Nonce: $NONCE" \
  -H "X-TKE-Signature: $SIG"

Exemple de réponse

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