Documentation API

Accès programmatique aux numéros virtuels, commandes et solde du compte.

Apercu

Tous les champs monétaires de l'API /v1 sont en IDR (roupie indonésienne), sous forme d'unités entières — par exemple, "price": 15000 et "balance": 500000 signifient Rp 15 000 et Rp 500 000. Pour une projection native en USD du même registre, passez à l'API v2 à l'aide du sélecteur de version ci-dessus.

Authentification

Toutes les requêtes API nécessitent un Bearer token. Générez-en un depuis les Paramètres du compte dans le tableau de bord, puis incluez-le dans chaque requête :

Authorization: Bearer YOUR_API_TOKEN

Les requêtes sans token valide reçoivent une réponse 401 UNAUTHORIZED.

URL de base

Tous les chemins d'endpoints ci-dessous sont relatifs à :

https://api.smscode.gg/v1

Format de réponse

Chaque réponse renvoie du JSON avec une enveloppe cohérente. Toutes les réponses incluent un en-tête x-request-id pour le débogage.

Succès
{
  "success": true,
  "data": { ... }
}
Erreur
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message"
  }
}

Tous les champs monétaires de l'API /v1 sont en IDR (roupie indonésienne), sous forme d'unités entières — par exemple, "price": 15000 et "balance": 500000 signifient Rp 15 000 et Rp 500 000. Pour une projection native en USD du même registre, passez à l'API v2 à l'aide du sélecteur de version ci-dessus.

GET /catalog/countries

Renvoie la liste de tous les pays disponibles.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v1/catalog/countries \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/countries", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/countries",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 6,
      "code": "ID",
      "name": "Indonesia",
      "dial_code": "+62",
      "emoji": "🇮🇩",
      "active": true
    }
  ]
}
GET /catalog/services

Renvoie la liste des services (plateformes) disponibles. Filtrage optionnel par pays.

Paramètres de requête

NomTypeRequisDescription
country_idintegerNonFiltrer les services disponibles pour ce pays

Exemple de requête

curl -s "https://api.smscode.gg/v1/catalog/services?country_id=6" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/services?country_id=6", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/services",
    params={"country_id": 6},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 3,
      "code": "wa",
      "name": "WhatsApp",
      "active": true
    }
  ]
}
GET /catalog/products

Renvoie une liste paginée des produits disponibles. Filtrage par pays et/ou plateforme.

Paramètres de requête

NomTypeRequisDescription
country_idintegerNonFiltrer par identifiant de pays
platform_idintegerNonFiltrer par identifiant de plateforme/service
sortstringNonOrdre de tri : price_asc (défaut), price_desc, available_asc, available_desc, name_asc, name_desc
limitintegerNonRésultats par page (1-10 000, défaut 1 000)
pageintegerNonNuméro de page (min. 1, défaut 1)

Exemple de requête

curl -s "https://api.smscode.gg/v1/catalog/products?country_id=6&platform_id=3&limit=10&page=1" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  country_id: "6", platform_id: "3", limit: "10", page: "1",
});
const res = await fetch(`https://api.smscode.gg/v1/catalog/products?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/products",
    params={"country_id": 6, "platform_id": 3, "limit": 10, "page": 1},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "catalog_product_id": 87,
      "available": 42,
      "price": 15000,
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1 }
}
GET /catalog/exchange-rate

Renvoie le taux de change USD/IDR actuel utilisé pour la conversion de devises.

Paramètres de requête

NomTypeRequisDescription
pairstringNonPaire de devises (défaut : USD/IDR)

Exemple de requête

curl -s "https://api.smscode.gg/v1/catalog/exchange-rate?pair=USD/IDR" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/catalog/exchange-rate?pair=USD/IDR", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/catalog/exchange-rate",
    params={"pair": "USD/IDR"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "pair": "USD/IDR",
    "base_currency": "USD",
    "quote_currency": "IDR",
    "rate": 16250
  }
}
GET /balance

Renvoie le solde du compte de l'utilisateur authentifié.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v1/balance \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/balance", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/balance",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 500000
  }
}
GET /orders

Renvoie la liste des commandes de l'utilisateur authentifié, triées par date décroissante. Filtrage par statut et pagination via offset.

Paramètres de requête

NomTypeRequisDescription
limitintegerNonNombre max. de résultats (1-100, défaut 20)
offsetintegerNonNombre de résultats à ignorer (défaut 0)
statusstringNonFiltrer par statut : ACTIVE, OTP_RECEIVED, COMPLETED, CANCELED, EXPIRED (insensible à la casse)

Exemple de requête

curl -s "https://api.smscode.gg/v1/orders?limit=5&status=ACTIVE" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  limit: "5", status: "ACTIVE", offset: "0",
});
const res = await fetch(`https://api.smscode.gg/v1/orders?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders",
    params={"limit": 5, "status": "ACTIVE", "offset": 0},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "ACTIVE",
      "created_at": "2026-02-25T10:00:00+00:00",
      "product_id": 142,
      "phone_number": "+6281234567890",
      "amount": 15000,
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:20:00+00:00",
      "canceled_at": null,
      "failed_reason": null
    }
  ]
}
GET /orders/{id}

Renvoie une commande par son identifiant. Ne renvoie que les commandes de l'utilisateur authentifié.

Paramètres de chemin

NomTypeRequisDescription
idintegerOuiIdentifiant de commande (paramètre de chemin)

Exemple de requête

curl -s https://api.smscode.gg/v1/orders/1001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/orders/1001", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders/1001",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "id": 1001,
    "status": "OTP_RECEIVED",
    "created_at": "2026-02-25T10:00:00+00:00",
    "product_id": 142,
    "phone_number": "+6281234567890",
    "amount": 15000,
    "otp_code": "123456",
    "otp_received_at": "2026-02-25T10:05:00+00:00",
    "expires_at": "2026-02-25T10:20:00+00:00",
    "canceled_at": null,
    "failed_reason": null
  }
}
GET /orders/active

Liste toutes les commandes actuellement actives (ACTIVE + OTP_RECEIVED). Utilisez cet endpoint pour surveiller les mises à jour de statut OTP.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v1/orders/active \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/orders/active", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/orders/active",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "OTP_RECEIVED",
      "otp_code": "123456",
      "otp_message": "Your verification code is 123456",
      "otp_received_at": "2026-02-25T10:05:00+00:00",
      "expires_at": "2026-02-25T10:20:00+00:00",
      "failed_reason": null
    },
    {
      "id": 1002,
      "status": "ACTIVE",
      "otp_code": null,
      "otp_message": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:50:00+00:00",
      "failed_reason": null
    }
  ]
}
POST /orders/create

Crée une nouvelle commande de numéro virtuel. Débite le solde automatiquement. Prend en charge un en-tête Idempotency-Key optionnel pour éviter les commandes en double lors des nouvelles tentatives réseau.

Corps de la requête

NomTypeRequisDescription
product_idintegerNonIdentifiant du produit à commander. Fournissez UN SEUL des deux : product_id OU catalog_product_id, pas les deux.
catalog_product_idintegerNonIdentifiant de produit stable — le serveur choisit la meilleure offre actuelle (la moins chère, en tenant compte de la fiabilité). Fournissez celui-ci ou product_id.
max_priceinteger · stringNonPlafond de prix facultatif. v1 : entier en IDR. v2 : chaîne décimale en USD (ex. "0.50").
prefer_providerstringNonCode de fournisseur facultatif à privilégier en cas d'offres équivalentes.
policystringNonPolitique de routage facultative, valable uniquement avec catalog_product_id. Valeurs : cheapest (par défaut) choisit l'offre saine la moins chère ; best_success classe les offres d'abord selon le succès de livraison récent. best_success note chaque fournisseur sur la proportion de commandes ayant reçu un OTP au cours des 30 derniers jours complets, par tranches de 10%, et ne le comptabilise qu'à partir d'au moins 20 commandes sur cette période — les fournisseurs sous ce seuil ou sans historique sont considérés comme neutres, de sorte que les nouvelles offres ne sont jamais écartées (sur option ; le signal démarre neutre). Si prefer_provider est également défini, le fournisseur préféré reste en première position.
quantityintegerNonQuantité (1-100, défaut 1)

Passez un en-tête Idempotency-Key pour réessayer en toute sécurité sans créer de doublons. La clé peut contenir des lettres, des chiffres, un trait d'union et un tiret bas (A-Z a-z 0-9 _ -), jusqu'à 128 caractères ; une clé invalide est rejetée avec 422 VALIDATION_ERROR. Réessayer avec la même clé et le même corps rejoue le résultat d'origine (y compris le failed_count d'un succès partiel). Une nouvelle tentative qui atteint le fournisseur mais échoue est enregistrée et rejoue cette même erreur — utilisez une NOUVELLE clé pour réessayer. Les échecs sans effet de bord (solde insuffisant, aucune offre disponible) libèrent la clé, vous pouvez donc recharger votre solde et réessayer avec la même clé. Réutiliser une clé avec un corps différent renvoie 422 IDEMPOTENCY_KEY_REUSED, et une requête encore en cours avec cette clé renvoie 409 REQUEST_IN_PROGRESS. Le champ failed_reason dans les réponses de create est toujours null — il n'est renseigné qu'à la consultation/au listage des commandes.

Exemple de requête

curl -s -X POST https://api.smscode.gg/v1/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-123" \
  -d '{"product_id":142,"quantity":1}'

# Or order by stable catalog_product_id — the server picks the best current offer
# (cheapest, reliability-aware). Optional max_price caps the price; on v1 it is an
# IDR integer, on v2 it is a USD decimal string (e.g. "0.50"). prefer_provider
# (provider code) is a soft tie-breaker. Pass EITHER product_id OR catalog_product_id.
curl -s -X POST https://api.smscode.gg/v1/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-124" \
  -d '{"catalog_product_id":87,"max_price":20000}'
const res = await fetch("https://api.smscode.gg/v1/orders/create", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Idempotency-Key": "unique-request-id-123",
  },
  body: JSON.stringify({ product_id: 142, quantity: 1 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/create",
    json={"product_id": 142, "quantity": 1},
    headers={
        "Authorization": "Bearer YOUR_API_TOKEN",
        "Idempotency-Key": "unique-request-id-123",
    })
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": 1002,
        "status": "ACTIVE",
        "product_id": 142,
        "catalog_product_id": 87,
        "amount": 15000,
        "phone_number": "+6281234567891",
        "otp_code": null,
        "otp_received_at": null,
        "expires_at": "2026-02-25T10:50:00+00:00",
        "failed_reason": null
      }
    ],
    "failed_count": 0
  }
}
POST /orders/cancel

Annule une commande active. Le coût de la location est remboursé sur le solde de votre compte.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande à annuler

Exemple de requête

curl -s -X POST https://api.smscode.gg/v1/orders/cancel \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/cancel", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/cancel",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": 15000,
    "new_balance": 515000
  }
}
POST /orders/finish

Marque une commande comme terminée après réception de l'OTP. Cela libère le numéro immédiatement au lieu d'attendre l'expiration.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande à finaliser

Exemple de requête

curl -s -X POST https://api.smscode.gg/v1/orders/finish \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/finish", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/finish",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "COMPLETED"
  }
}
POST /orders/resend

Demande à la plateforme de renvoyer le SMS au numéro loué. Toutes les plateformes ne prennent pas en charge le renvoi — vérifiez le champ resent dans la réponse.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande pour le renvoi SMS

Exemple de requête

curl -s -X POST https://api.smscode.gg/v1/orders/resend \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v1/orders/resend", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/orders/resend",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "ACTIVE",
    "resent": true
  }
}
GET /webhook

Renvoie votre configuration actuelle de notifications webhook.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v1/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/webhook", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v1/webhook",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}
PATCH /webhook

Met à jour votre URL webhook et/ou votre secret. Un secret est généré automatiquement lorsque vous définissez une URL pour la première fois. Envoyez une chaîne vide pour effacer. L'URL doit utiliser HTTPS.

Corps de la requête

NomTypeRequisDescription
webhook_urlstringNonURL HTTPS pour recevoir les événements webhook (chaîne vide pour effacer)
webhook_secretstringNonSecret partagé pour la signature HMAC-SHA256 (généré automatiquement s'il est omis lors de la première configuration)

Au moins un champ est requis.

Exemple de requête

curl -s -X PATCH https://api.smscode.gg/v1/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com/webhook"}'
const res = await fetch("https://api.smscode.gg/v1/webhook", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhook_url: "https://example.com/webhook" }),
});
const data = await res.json();
import requests

res = requests.patch("https://api.smscode.gg/v1/webhook",
    json={"webhook_url": "https://example.com/webhook"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}
POST /webhook/test

Envoie un événement test à votre URL webhook configurée. Renvoie le code de statut HTTP de votre serveur. Utile pour vérifier que votre endpoint fonctionne avant la mise en production.

Paramètres

Aucun

Exemple de requête

curl -s -X POST https://api.smscode.gg/v1/webhook/test \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v1/webhook/test", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v1/webhook/test",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "status_code": 200
  }
}

Notifications webhook

Configurez une URL webhook pour recevoir des notifications push en temps réel pour les événements de commande au lieu du polling. C'est l'approche recommandée pour les scripts de bot.

Événements

ÉvénementDéclencheur
order.otp_receivedCode OTP livré au numéro loué
order.completedCommande marquée comme terminée (manuellement ou par expiration)
order.expiredCommande expirée sans OTP (solde remboursé)
order.canceledCommande annulée par l'utilisateur (solde remboursé)

Payload

Corps de la requête POST webhook
{
  "event": "order.otp_received",
  "timestamp": "2026-02-25T12:00:00+00:00",
  "data": {
    "order_id": 1001,
    "phone_number": "+628123456789",
    "otp_code": "1234",
    "otp_message": "Your verification code is 1234",
    "product_id": 42,
    "catalog_product_id": 87,
    "country": "Indonesia",
    "platform": "WhatsApp"
  }
}

Vérification de la signature

Chaque requête webhook inclut un en-tête X-Webhook-Signature avec une signature HMAC-SHA256 du corps de la requête, utilisant votre webhook_secret comme clé :

X-Webhook-Signature: sha256={HMAC-SHA256(body, webhook_secret)}

Vérifiez cette signature côté serveur pour vous assurer que la requête est authentique. La livraison est de type « fire-and-forget » avec un délai d'expiration de 3 secondes et sans nouvelle tentative.

Limites de débit

Les requêtes API sont soumises à des limites de débit par groupe d'endpoints. Le dépassement de la limite renvoie un code 429 Too Many Requests avec un en-tête Retry-After indiquant le nombre de secondes à attendre.

Groupe d'endpointsLimiteFenêtre
Catalogue (pays, services, produits, taux de change)5 000 requêtes60 secondes
Solde600 requêtes60 secondes
Lecture de commandes (liste, détail, actives)5 000 requêtes60 secondes
Création de commande3 000 requêtes60 secondes
Annulation de commande1 000 requêtes60 secondes
Actions sur les commandes (finaliser, renvoyer)1 000 requêtes60 secondes
Configuration webhook (lecture, mise à jour)600 requêtes60 secondes
Test webhook10 requêtes60 secondes

Codes d'erreur

Les réponses d'erreur incluent l'un de ces codes dans error.code :

CodeHTTPDescription
UNAUTHORIZED401Token API manquant ou invalide
FORBIDDEN403Accès refusé
NOT_FOUND404Ressource introuvable (commande, taux de change, etc.)
CONFLICT409Requête en double ou conflit de ressource
INSUFFICIENT_BALANCE409Solde insuffisant pour créer la commande
VALIDATION_ERROR422Les paramètres de la requête n'ont pas passé la validation
RATE_LIMIT_EXCEEDED429Trop de requêtes (vérifiez l'en-tête Retry-After)
INTERNAL_ERROR500Erreur interne du serveur
PROVIDER_ERROR422Le fournisseur SMS en amont a rejeté la requête. En cas d'échec de création de commande, l'erreur peut inclure <code>details</code> : <code>cause_counts</code> (commandes avec <code>product_id</code> hérité — un décompte regroupé par cause) ou <code>attempts</code> (commandes avec <code>catalog_product_id</code> — résultats par tentative), avec les valeurs <code>ok</code>, <code>no_numbers</code>, <code>insufficient_balance</code>, <code>price_rejected</code>, <code>provider_unavailable</code>, <code>provider_account_balance</code>, <code>provider_error</code>.
NO_OFFER_AVAILABLE422Aucune offre active ne correspond au produit et à la politique demandés (plafond de prix, disponibilité).
CANCEL_TOO_EARLY409Commande trop récente pour être annulée — patientez 2 minutes
REQUEST_IN_PROGRESS409Une requête de création avec cette clé d'idempotence est encore en cours
IDEMPOTENCY_KEY_REUSED422Cette clé d'idempotence a déjà été utilisée avec un corps de requête différent
SERVICE_UNAVAILABLE503Service temporairement indisponible (maintenance)

Apercu

Tous les champs monétaires de l'API /v2 sont en USD et renvoyés sous forme d'objet monétaire — { "amount": "0.92", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" }. amount est une chaîne décimale ; canonical_amount est la valeur IDR exacte du registre (utilisez-la pour le rapprochement). Le rate USD/IDR appliqué est communiqué une seule fois par réponse dans meta.fx. v2 est une projection en USD au moment du rendu, par-dessus le même registre IDR que v1 — elle ne stocke ni ne traite jamais d'USD.

Migration de v1 vers v2

Authentification

Toutes les requêtes API nécessitent un Bearer token. Générez-en un depuis les Paramètres du compte dans le tableau de bord, puis incluez-le dans chaque requête :

Authorization: Bearer YOUR_API_TOKEN

Les requêtes sans token valide reçoivent une réponse 401 UNAUTHORIZED.

URL de base

Tous les chemins d'endpoints ci-dessous sont relatifs à :

https://api.smscode.gg/v2

Format de réponse

Chaque réponse renvoie du JSON avec une enveloppe cohérente. Toutes les réponses incluent un en-tête x-request-id pour le débogage.

Succès
{
  "success": true,
  "data": { ... }
}
Erreur
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message"
  }
}

Erreur v2 : aucun taux de change utilisable (503)

503 Service Unavailable · Retry-After: 60
{
  "success": false,
  "error": { "code": "FX_RATE_UNAVAILABLE", "message": "USD/IDR exchange rate is unavailable" }
}

Tous les champs monétaires de l'API /v2 sont en USD et renvoyés sous forme d'objet monétaire — { "amount": "0.92", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" }. amount est une chaîne décimale ; canonical_amount est la valeur IDR exacte du registre (utilisez-la pour le rapprochement). Le rate USD/IDR appliqué est communiqué une seule fois par réponse dans meta.fx. v2 est une projection en USD au moment du rendu, par-dessus le même registre IDR que v1 — elle ne stocke ni ne traite jamais d'USD.

GET /catalog/countries

Renvoie la liste de tous les pays disponibles.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v2/catalog/countries \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/countries", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/countries",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 6,
      "code": "ID",
      "name": "Indonesia",
      "dial_code": "+62",
      "emoji": "🇮🇩",
      "active": true
    }
  ]
}

Identique à v1 — seul le chemin de base change (/v1/v2).

GET /catalog/services

Renvoie la liste des services (plateformes) disponibles. Filtrage optionnel par pays.

Paramètres de requête

NomTypeRequisDescription
country_idintegerNonFiltrer les services disponibles pour ce pays

Exemple de requête

curl -s "https://api.smscode.gg/v2/catalog/services?country_id=6" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/services?country_id=6", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/services",
    params={"country_id": 6},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 3,
      "code": "wa",
      "name": "WhatsApp",
      "active": true
    }
  ]
}

Identique à v1 — seul le chemin de base change (/v1/v2).

GET /catalog/products

Renvoie une liste paginée des produits disponibles. Filtrage par pays et/ou plateforme.

Paramètres de requête

NomTypeRequisDescription
country_idintegerNonFiltrer par identifiant de pays
platform_idintegerNonFiltrer par identifiant de plateforme/service
sortstringNonOrdre de tri : price_asc (défaut), price_desc, available_asc, available_desc, name_asc, name_desc
limitintegerNonRésultats par page (1-10 000, défaut 1 000)
pageintegerNonNuméro de page (min. 1, défaut 1)

Exemple de requête

curl -s "https://api.smscode.gg/v2/catalog/products?country_id=6&platform_id=3&limit=10&page=1" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  country_id: "6", platform_id: "3", limit: "10", page: "1",
});
const res = await fetch(`https://api.smscode.gg/v2/catalog/products?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/products",
    params={"country_id": 6, "platform_id": 3, "limit": 10, "page": 1},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "catalog_product_id": 87,
      "available": 42,
      "price": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1, "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

GET /catalog/exchange-rate

Renvoie le taux de change USD/IDR actuel utilisé pour la conversion de devises.

Paramètres

Aucun — v2 renvoie toujours USD/IDR ; le paramètre ?pair de v1 est ignoré.

Exemple de requête

curl -s "https://api.smscode.gg/v2/catalog/exchange-rate?pair=USD/IDR" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/catalog/exchange-rate?pair=USD/IDR", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/catalog/exchange-rate",
    params={"pair": "USD/IDR"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" }
}

v2 : renvoie { pair, rate, rate_as_of } (pas de base_currency/quote_currency, pas d'enveloppe meta — le taux est la donnée). ?pair est ignoré — v2 renvoie toujours USD/IDR (v1 prend en compte ?pair). Renvoie 503 FX_RATE_UNAVAILABLE s'il n'existe aucun taux utilisable.

GET /balance

Renvoie le solde du compte de l'utilisateur authentifié.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v2/balance \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/balance", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/balance",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "balance": { "amount": "30.77", "currency": "USD", "canonical_amount": 500000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

GET /orders

Renvoie la liste des commandes de l'utilisateur authentifié, triées par date décroissante. Filtrage par statut et pagination via offset.

Paramètres de requête

NomTypeRequisDescription
limitintegerNonNombre max. de résultats (1-100, défaut 20)
offsetintegerNonNombre de résultats à ignorer (défaut 0)
statusstringNonFiltrer par statut : ACTIVE, OTP_RECEIVED, COMPLETED, CANCELED, EXPIRED (insensible à la casse)

Exemple de requête

curl -s "https://api.smscode.gg/v2/orders?limit=5&status=ACTIVE" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const params = new URLSearchParams({
  limit: "5", status: "ACTIVE", offset: "0",
});
const res = await fetch(`https://api.smscode.gg/v2/orders?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders",
    params={"limit": 5, "status": "ACTIVE", "offset": 0},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "ACTIVE",
      "created_at": "2026-02-25T10:00:00+00:00",
      "product_id": 142,
      "phone_number": "+6281234567890",
      "amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:20:00+00:00",
      "canceled_at": null,
      "failed_reason": null
    }
  ],
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

GET /orders/{id}

Renvoie une commande par son identifiant. Ne renvoie que les commandes de l'utilisateur authentifié.

Paramètres de chemin

NomTypeRequisDescription
idintegerOuiIdentifiant de commande (paramètre de chemin)

Exemple de requête

curl -s https://api.smscode.gg/v2/orders/1001 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/orders/1001", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders/1001",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "id": 1001,
    "status": "OTP_RECEIVED",
    "created_at": "2026-02-25T10:00:00+00:00",
    "product_id": 142,
    "phone_number": "+6281234567890",
    "amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "otp_code": "123456",
    "otp_received_at": "2026-02-25T10:05:00+00:00",
    "expires_at": "2026-02-25T10:20:00+00:00",
    "canceled_at": null,
    "failed_reason": null
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

GET /orders/active

Liste toutes les commandes actuellement actives (ACTIVE + OTP_RECEIVED). Utilisez cet endpoint pour surveiller les mises à jour de statut OTP.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v2/orders/active \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/orders/active", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/orders/active",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": [
    {
      "id": 1001,
      "status": "OTP_RECEIVED",
      "otp_code": "123456",
      "otp_message": "Your verification code is 123456",
      "otp_received_at": "2026-02-25T10:05:00+00:00",
      "expires_at": "2026-02-25T10:20:00+00:00",
      "failed_reason": null
    },
    {
      "id": 1002,
      "status": "ACTIVE",
      "otp_code": null,
      "otp_message": null,
      "otp_received_at": null,
      "expires_at": "2026-02-25T10:50:00+00:00",
      "failed_reason": null
    }
  ]
}

v2 : cet endpoint n'est pas porteur de montant — il ne renvoie ni amount ni meta.fx (même structure que v1, sous /v2).

POST /orders/create

Crée une nouvelle commande de numéro virtuel. Débite le solde automatiquement. Prend en charge un en-tête Idempotency-Key optionnel pour éviter les commandes en double lors des nouvelles tentatives réseau.

Corps de la requête

NomTypeRequisDescription
product_idintegerNonIdentifiant du produit à commander. Fournissez UN SEUL des deux : product_id OU catalog_product_id, pas les deux.
catalog_product_idintegerNonIdentifiant de produit stable — le serveur choisit la meilleure offre actuelle (la moins chère, en tenant compte de la fiabilité). Fournissez celui-ci ou product_id.
max_priceinteger · stringNonPlafond de prix facultatif. v1 : entier en IDR. v2 : chaîne décimale en USD (ex. "0.50").
prefer_providerstringNonCode de fournisseur facultatif à privilégier en cas d'offres équivalentes.
policystringNonPolitique de routage facultative, valable uniquement avec catalog_product_id. Valeurs : cheapest (par défaut) choisit l'offre saine la moins chère ; best_success classe les offres d'abord selon le succès de livraison récent. best_success note chaque fournisseur sur la proportion de commandes ayant reçu un OTP au cours des 30 derniers jours complets, par tranches de 10%, et ne le comptabilise qu'à partir d'au moins 20 commandes sur cette période — les fournisseurs sous ce seuil ou sans historique sont considérés comme neutres, de sorte que les nouvelles offres ne sont jamais écartées (sur option ; le signal démarre neutre). Si prefer_provider est également défini, le fournisseur préféré reste en première position.
quantityintegerNonQuantité (1-100, défaut 1)

Passez un en-tête Idempotency-Key pour réessayer en toute sécurité sans créer de doublons. La clé peut contenir des lettres, des chiffres, un trait d'union et un tiret bas (A-Z a-z 0-9 _ -), jusqu'à 128 caractères ; une clé invalide est rejetée avec 422 VALIDATION_ERROR. Réessayer avec la même clé et le même corps rejoue le résultat d'origine (y compris le failed_count d'un succès partiel). Une nouvelle tentative qui atteint le fournisseur mais échoue est enregistrée et rejoue cette même erreur — utilisez une NOUVELLE clé pour réessayer. Les échecs sans effet de bord (solde insuffisant, aucune offre disponible) libèrent la clé, vous pouvez donc recharger votre solde et réessayer avec la même clé. Réutiliser une clé avec un corps différent renvoie 422 IDEMPOTENCY_KEY_REUSED, et une requête encore en cours avec cette clé renvoie 409 REQUEST_IN_PROGRESS. Le champ failed_reason dans les réponses de create est toujours null — il n'est renseigné qu'à la consultation/au listage des commandes.

Exemple de requête

curl -s -X POST https://api.smscode.gg/v2/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-123" \
  -d '{"product_id":142,"quantity":1}'

# Or order by stable catalog_product_id — the server picks the best current offer
# (cheapest, reliability-aware). Optional max_price caps the price; on v1 it is an
# IDR integer, on v2 it is a USD decimal string (e.g. "0.50"). prefer_provider
# (provider code) is a soft tie-breaker. Pass EITHER product_id OR catalog_product_id.
curl -s -X POST https://api.smscode.gg/v2/orders/create \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-124" \
  -d '{"catalog_product_id":87,"max_price":20000}'
const res = await fetch("https://api.smscode.gg/v2/orders/create", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
    "Idempotency-Key": "unique-request-id-123",
  },
  body: JSON.stringify({ product_id: 142, quantity: 1 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/create",
    json={"product_id": 142, "quantity": 1},
    headers={
        "Authorization": "Bearer YOUR_API_TOKEN",
        "Idempotency-Key": "unique-request-id-123",
    })
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": 1002,
        "status": "ACTIVE",
        "product_id": 142,
        "catalog_product_id": 87,
        "amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
        "phone_number": "+6281234567891",
        "otp_code": null,
        "otp_received_at": null,
        "expires_at": "2026-02-25T10:50:00+00:00",
        "failed_reason": null
      }
    ],
    "failed_count": 0
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

POST /orders/cancel

Annule une commande active. Le coût de la location est remboursé sur le solde de votre compte.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande à annuler

Exemple de requête

curl -s -X POST https://api.smscode.gg/v2/orders/cancel \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/cancel", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/cancel",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "new_balance": { "amount": "31.69", "currency": "USD", "canonical_amount": 515000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

v2 : les champs monétaires sont des objets monétaires en USD et la réponse contient un unique meta.fx { pair, rate, rate_as_of }. rate est le montant entier en IDR pour 1 USD, donc USD = canonical_amount / rate. Les totaux utilisent 2 décimales ; les prix/remboursements par article en utilisent 4. Un montant strictement positif n'est jamais arrondi à 0.00. rate_as_of est l'horodatage RFC3339 du taux (au format +00:00) ou null lorsqu'aucun horodatage n'est enregistré.

v2 uniquement : s'il n'existe aucun taux USD/IDR utilisable, les endpoints monétaires renvoient 503 FX_RATE_UNAVAILABLE avec un en-tête Retry-After au lieu d'un corps monétaire. v1 ne renvoie jamais cela.

POST /orders/finish

Marque une commande comme terminée après réception de l'OTP. Cela libère le numéro immédiatement au lieu d'attendre l'expiration.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande à finaliser

Exemple de requête

curl -s -X POST https://api.smscode.gg/v2/orders/finish \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/finish", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/finish",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "COMPLETED"
  }
}

Identique à v1 — seul le chemin de base change (/v1/v2).

POST /orders/resend

Demande à la plateforme de renvoyer le SMS au numéro loué. Toutes les plateformes ne prennent pas en charge le renvoi — vérifiez le champ resent dans la réponse.

Corps de la requête

NomTypeRequisDescription
idintegerOuiIdentifiant de la commande pour le renvoi SMS

Exemple de requête

curl -s -X POST https://api.smscode.gg/v2/orders/resend \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":1001}'
const res = await fetch("https://api.smscode.gg/v2/orders/resend", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ id: 1001 }),
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/orders/resend",
    json={"id": 1001},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "ACTIVE",
    "resent": true
  }
}

Identique à v1 — seul le chemin de base change (/v1/v2).

GET /webhook

Renvoie votre configuration actuelle de notifications webhook.

Paramètres

Aucun

Exemple de requête

curl -s https://api.smscode.gg/v2/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/webhook", {
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.get("https://api.smscode.gg/v2/webhook",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}

Identique à v1 — seul le chemin de base change (/v1/v2).

PATCH /webhook

Met à jour votre URL webhook et/ou votre secret. Un secret est généré automatiquement lorsque vous définissez une URL pour la première fois. Envoyez une chaîne vide pour effacer. L'URL doit utiliser HTTPS.

Corps de la requête

NomTypeRequisDescription
webhook_urlstringNonURL HTTPS pour recevoir les événements webhook (chaîne vide pour effacer)
webhook_secretstringNonSecret partagé pour la signature HMAC-SHA256 (généré automatiquement s'il est omis lors de la première configuration)

Au moins un champ est requis.

Exemple de requête

curl -s -X PATCH https://api.smscode.gg/v2/webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com/webhook"}'
const res = await fetch("https://api.smscode.gg/v2/webhook", {
  method: "PATCH",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ webhook_url: "https://example.com/webhook" }),
});
const data = await res.json();
import requests

res = requests.patch("https://api.smscode.gg/v2/webhook",
    json={"webhook_url": "https://example.com/webhook"},
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhook",
    "webhook_secret": "a1b2c3d4e5f6..."
  }
}

Identique à v1 — seul le chemin de base change (/v1/v2).

POST /webhook/test

Envoie un événement test à votre URL webhook configurée. Renvoie le code de statut HTTP de votre serveur. Utile pour vérifier que votre endpoint fonctionne avant la mise en production.

Paramètres

Aucun

Exemple de requête

curl -s -X POST https://api.smscode.gg/v2/webhook/test \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const res = await fetch("https://api.smscode.gg/v2/webhook/test", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_API_TOKEN" },
});
const data = await res.json();
import requests

res = requests.post("https://api.smscode.gg/v2/webhook/test",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"})
data = res.json()

Exemple de réponse

200 OK
{
  "success": true,
  "data": {
    "status_code": 200
  }
}

Identique à v1 — seul le chemin de base change (/v1/v2).

Notifications webhook

Configurez une URL webhook pour recevoir des notifications push en temps réel pour les événements de commande au lieu du polling. C'est l'approche recommandée pour les scripts de bot.

Événements

ÉvénementDéclencheur
order.otp_receivedCode OTP livré au numéro loué
order.completedCommande marquée comme terminée (manuellement ou par expiration)
order.expiredCommande expirée sans OTP (solde remboursé)
order.canceledCommande annulée par l'utilisateur (solde remboursé)

Payload

Corps de la requête POST webhook
{
  "event": "order.otp_received",
  "timestamp": "2026-02-25T12:00:00+00:00",
  "data": {
    "order_id": 1001,
    "phone_number": "+628123456789",
    "otp_code": "1234",
    "otp_message": "Your verification code is 1234",
    "product_id": 42,
    "catalog_product_id": 87,
    "country": "Indonesia",
    "platform": "WhatsApp"
  }
}

Vérification de la signature

Chaque requête webhook inclut un en-tête X-Webhook-Signature avec une signature HMAC-SHA256 du corps de la requête, utilisant votre webhook_secret comme clé :

X-Webhook-Signature: sha256={HMAC-SHA256(body, webhook_secret)}

Vérifiez cette signature côté serveur pour vous assurer que la requête est authentique. La livraison est de type « fire-and-forget » avec un délai d'expiration de 3 secondes et sans nouvelle tentative.

Limites de débit

Les requêtes API sont soumises à des limites de débit par groupe d'endpoints. Le dépassement de la limite renvoie un code 429 Too Many Requests avec un en-tête Retry-After indiquant le nombre de secondes à attendre.

Groupe d'endpointsLimiteFenêtre
Catalogue (pays, services, produits, taux de change)5 000 requêtes60 secondes
Solde600 requêtes60 secondes
Lecture de commandes (liste, détail, actives)5 000 requêtes60 secondes
Création de commande3 000 requêtes60 secondes
Annulation de commande1 000 requêtes60 secondes
Actions sur les commandes (finaliser, renvoyer)1 000 requêtes60 secondes
Configuration webhook (lecture, mise à jour)600 requêtes60 secondes
Test webhook10 requêtes60 secondes

Codes d'erreur

Les réponses d'erreur incluent l'un de ces codes dans error.code :

CodeHTTPDescription
UNAUTHORIZED401Token API manquant ou invalide
FORBIDDEN403Accès refusé
NOT_FOUND404Ressource introuvable (commande, taux de change, etc.)
CONFLICT409Requête en double ou conflit de ressource
INSUFFICIENT_BALANCE409Solde insuffisant pour créer la commande
VALIDATION_ERROR422Les paramètres de la requête n'ont pas passé la validation
RATE_LIMIT_EXCEEDED429Trop de requêtes (vérifiez l'en-tête Retry-After)
INTERNAL_ERROR500Erreur interne du serveur
PROVIDER_ERROR422Le fournisseur SMS en amont a rejeté la requête. En cas d'échec de création de commande, l'erreur peut inclure <code>details</code> : <code>cause_counts</code> (commandes avec <code>product_id</code> hérité — un décompte regroupé par cause) ou <code>attempts</code> (commandes avec <code>catalog_product_id</code> — résultats par tentative), avec les valeurs <code>ok</code>, <code>no_numbers</code>, <code>insufficient_balance</code>, <code>price_rejected</code>, <code>provider_unavailable</code>, <code>provider_account_balance</code>, <code>provider_error</code>.
NO_OFFER_AVAILABLE422Aucune offre active ne correspond au produit et à la politique demandés (plafond de prix, disponibilité).
CANCEL_TOO_EARLY409Commande trop récente pour être annulée — patientez 2 minutes
REQUEST_IN_PROGRESS409Une requête de création avec cette clé d'idempotence est encore en cours
IDEMPOTENCY_KEY_REUSED422Cette clé d'idempotence a déjà été utilisée avec un corps de requête différent
SERVICE_UNAVAILABLE503Service temporairement indisponible (maintenance)
FX_RATE_UNAVAILABLE503Taux de change USD/IDR indisponible (endpoints monétaires v2) — renvoie 503 avec un en-tête Retry-After.
v1 → v2

Migration de v1 vers v2

v1 sert l'IDR ; v2 sert l'USD. Les deux versions coexistent en permanence — il n'y a pas d'arrêt prévu. Choisissez une version par intégration ; ne mélangez pas les chemins de base. v2 est identique à v1, à l'exception de la façon dont l'argent est représenté.

Aspectv1 · IDRv2 · USD
Champs monétairesIDR entier, ex. 15000Objet monétaire { amount, currency, canonical_amount, canonical_currency }
meta.fxAbsentObligatoire sur chaque réponse porteuse de montant
DeviseIDRUSD (codé en dur)
FX_RATE_UNAVAILABLENouveau 503 + Retry-After lorsqu'aucun taux utilisable n'est disponible
PrécisionTotaux 2 décimales, prix/remboursements 4 décimales, arrondi au supérieur pour les positifs
GET /catalog/exchange-rate{pair, base_currency, quote_currency, rate} ; prend en compte ?pair{pair, rate, rate_as_of} ; ?pair ignoré (USD/IDR uniquement)

Exemples côte à côte

GET/balance
v1
{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 500000
  }
}
v2
{
  "success": true,
  "data": {
    "balance": { "amount": "30.77", "currency": "USD", "canonical_amount": 500000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}
GET/catalog/products
v1
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "catalog_product_id": 87,
      "available": 42,
      "price": 15000,
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1 }
}
v2
{
  "success": true,
  "data": [
    {
      "id": 142,
      "name": "WhatsApp Indonesia",
      "country_id": 6,
      "platform_id": 3,
      "catalog_product_id": 87,
      "available": 42,
      "price": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 10, "count": 1, "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

catalog_product_id est l'identité STABLE du produit (une par pays + plateforme) — vous pouvez la stocker en toute sécurité dans vos systèmes ET commander directement avec : passez la commande avec catalog_product_id (plus les options max_price et prefer_provider) et le serveur résout l'offre active pour vous. La voie par l'id par offre (product_id) reste prise en charge et rétrocompatible — cet id est volatil et change dès qu'un palier de prix du fournisseur évolue, donc le conseil de récupérer à nouveau le catalogue avant de commander ne s'applique qu'à cette voie.

POST/orders/cancel
v1
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": 15000,
    "new_balance": 515000
  }
}
v2
{
  "success": true,
  "data": {
    "order_id": 1001,
    "status": "CANCELED",
    "refund_amount": { "amount": "0.9231", "currency": "USD", "canonical_amount": 15000, "canonical_currency": "IDR" },
    "new_balance": { "amount": "31.69", "currency": "USD", "canonical_amount": 515000, "canonical_currency": "IDR" }
  },
  "meta": { "fx": { "pair": "USD/IDR", "rate": 16250, "rate_as_of": "2026-05-27T08:00:00+00:00" } }
}

Checklist de migration

  1. Changez le chemin de base /v1/v2.
  2. Analysez les champs monétaires comme des objets — lisez amount comme une chaîne décimale ; currency vaut "USD".
  3. Pour le rapprochement du registre, utilisez canonical_amount (IDR exact) ; le amount en USD est une projection au moment du rendu et le rate est communiqué une seule fois dans meta.fx.
  4. Gérez le nouveau FX_RATE_UNAVAILABLE (503) — réessayez après Retry-After. v1 ne renvoie jamais cela.