La vérification SMS manuelle — acheter un numéro, attendre le code, l’entrer dans une interface — fonctionne parfaitement pour les usages ponctuels. Mais dès qu’il s’agit d’automatisation — tests E2E, création de comptes en masse, pipelines CI/CD, monitoring de services — l’intervention manuelle devient un goulot d’étranglement. L’API SMSCode permet de gérer tout le cycle de vérification SMS de manière programmatique, depuis la sélection du numéro jusqu’à la récupération du code.
TL;DR — L’API REST SMSCode permet d’automatiser entièrement le cycle achat de numéro → réception du SMS → libération. Elle est compatible avec n’importe quel langage. Authentification par bearer token, 5 endpoints principaux. Lis toujours le prix entier en IDR dans le catalogue courant avant l’achat : aucun plancher fixe en USD n’est promis. Idéal pour les tests automatisés, la création de comptes de test, ou l’intégration de vérification SMS dans des workflows CI/CD. Une intégration SMS-Activate existante doit être adaptée au contrat REST de SMSCode.
Cas d’usage de l’API SMSCode
Avant de plonger dans la technique, voici les situations concrètes pour lesquelles l’API est utilisée :
Tests E2E et tests d’intégration. Playwright, Cypress, Selenium — les frameworks de test automatisé peuvent créer des comptes utilisateur et tester des flux d’inscription complets. L’API SMSCode fournit les numéros de téléphone et récupère les codes OTP programmatiquement, ce qui rend les tests d’inscription entièrement automatisables.
Pipelines CI/CD. Tester une intégration d’inscription SMS à chaque déploiement sans intervention humaine. L’API s’intègre dans n’importe quel script de pipeline (GitHub Actions, GitLab CI, Jenkins, Buildkite).
Création de comptes de test. Les équipes QA qui ont besoin de dizaines ou centaines de comptes de test sur une plateforme cible peuvent automatiser la création avec des numéros distincts pour chaque compte. Chaque numéro SMSCode est unique — aucun risque de collision.
Monitoring de services. Vérifier régulièrement qu’un flux d’inscription fonctionne correctement en production, sans créer de comptes manuellement. Un script peut s’exécuter toutes les heures et alerter si la vérification SMS échoue.
Applications et SaaS. Des développeurs intègrent l’API SMSCode dans leurs propres applications pour fournir des numéros virtuels à leurs utilisateurs finaux comme service.
Scraping et données. Accéder à des services qui nécessitent une vérification SMS pour récupérer des données publiques.
Obtenir ta clé API
- Crée un compte sur smscode.gg
- Va dans Paramètres → API dans ton tableau de bord
- Génère une clé API (bearer token) — elle apparaît une seule fois, copie-la immédiatement
- Ajoute un petit solde avec une méthode affichée sur ta page de dépôt
Sécurité de la clé API :
- Traite-la comme un mot de passe — accès complet à ton solde et à tes commandes
- Ne la commit jamais dans un dépôt git (utilise
.gitignorepour les fichiers.env) - Utilise les variables d’environnement :
SMSCODE_API_KEY=ta_cledans.env - Régénère-la immédiatement si tu suspectes une compromission
Authentification
Toutes les requêtes utilisent un header Authorization avec un bearer token :
Authorization: Bearer ta_cle_api
Content-Type: application/json
L’API est disponible sur https://api.smscode.gg/v1/. Toutes les réponses sont en JSON avec la structure suivante :
{ "success": true, "data": {} }
Endpoints principaux
Vérifier le solde
GET /v1/balance
Réponse :
{
"success": true,
"data": {
"currency": "IDR",
"balance": 452000
}
}
Vérifie ton solde avant chaque session d’automatisation pour éviter les interruptions en cours de processus.
Lister les services et pays disponibles
GET /v1/catalog/products
GET /v1/catalog/products?country_id=7&platform_id=1
Réponse :
{
"success": true,
"data": [
{
"id": 1024,
"name": "WhatsApp - Indonesia",
"catalog_product_id": 88,
"country_id": 7,
"platform_id": 1,
"available": 142,
"price": 10000,
"active": true
}
],
"meta": { "page": 1, "limit": 1000, "count": 1 }
}
Utilise le nombre entier available pour vérifier le stock (0 signifie indisponible) et conserve
catalog_product_id pour la commande.
Acheter un numéro
POST /v1/orders/create
Corps de la requête :
{
"catalog_product_id": 88,
"quantity": 1
}
Réponse :
{
"success": true,
"data": {
"orders": [{
"id": 90210,
"status": "ACTIVE",
"phone_number": "+919876543210",
"otp_code": null,
"otp_received_at": null,
"expires_at": "2026-03-16T10:20:00Z",
"failed_reason": null,
"product_id": 1024,
"catalog_product_id": 88,
"operator_id": null,
"operator_name": null,
"amount": 750000,
"can_finish": false,
"can_resend": false,
"can_cancel": false,
"can_replace": false,
"can_reactivate": false,
"resend_available_at": null,
"cancel_available_at": "2026-03-16T10:02:00Z",
"replace_available_at": "2026-03-16T10:02:00Z"
}],
"failed_count": 0
}
}
Génère un Idempotency-Key avant le premier POST. Si le résultat réseau ou 5xx est ambigu, réutilise uniquement la même clé avec exactement le même body.
phone_number est facultatif et peut rester null jusqu’à son attribution. Un workflow durable
effectue alors une seule lecture bornée de GET /v1/orders/{id} avec le même id entier. Si le
champ n’est toujours pas une chaîne non vide, le workflow reste dans l’état local
pending_assignment ; il n’utilise le numéro qu’après son attribution.
Récupérer le statut d’une commande (polling)
GET /v1/orders/{order_id}
Projection V1OrderSummary avec SMS reçu (champs sélectionnés, pas la réponse wire complète) :
{
"success": true,
"data": {
"id": 90210,
"phone_number": "+919876543210",
"status": "OTP_RECEIVED",
"otp_code": null,
"otp_message": "Your Google verification code is 847291",
"sms_revision": 1,
"can_cancel": false,
"otp_received_at": "2026-03-16T10:15:30Z"
}
}
Projection V1OrderSummary en attente de SMS (champs sélectionnés, pas la réponse wire complète) :
{
"success": true,
"data": {
"id": 90210,
"status": "ACTIVE",
"otp_code": null,
"otp_message": null,
"sms_revision": 0,
"can_cancel": true
}
}
OTP_RECEIVED est un état du cycle de vie, pas une condition de consommation. Initialise
last_seen_revision à -1 pour chaque commande. Traite une sms_revision entière,
strictement plus récente et accompagnée d’un otp_message textuel non vide avant d’évaluer
COMPLETED, CANCELED ou EXPIRED, même lorsque otp_code est null.
Annuler une commande
POST /v1/orders/cancel
Envoie {"id":90210} uniquement lorsque le snapshot courant indique can_cancel: true.
Exemples d’intégration
Python — Implémentation complète
import json
import requests
import time
import os
import uuid
API_KEY = os.environ.get("SMSCODE_API_KEY")
BASE_URL = "https://api.smscode.gg/v1"
ORDER_TIMEOUT = (5, 30)
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
def verifier_solde():
"""Vérifie le solde disponible avant de commencer."""
response = requests.get(f"{BASE_URL}/balance", headers=headers)
data = response.json()
if not data["success"]:
raise Exception(f"Erreur solde : {data['error']['message']}")
return data["data"]["balance"]
def trouver_produit(country_id, platform_id):
"""Sélectionne un produit disponible sans effectuer d'achat."""
response = requests.get(
f"{BASE_URL}/catalog/products",
params={"country_id": country_id, "platform_id": platform_id},
headers=headers
)
catalog = response.json()["data"]
produit = next((
item for item in catalog
if item["available"] and type(item.get("catalog_product_id")) is int
), None)
if produit is None:
raise Exception("Aucun produit disponible avec un catalog_product_id achetable")
return produit["catalog_product_id"], produit["price"]
def acheter_numero(catalog_product_id):
"""Effectue un achat logique avec une clé et un body stables."""
body = {"catalog_product_id": catalog_product_id, "quantity": 1}
idempotency_key = str(uuid.uuid4())
response = requests.post(
f"{BASE_URL}/orders/create",
json=body,
headers={**headers, "Idempotency-Key": idempotency_key},
timeout=ORDER_TIMEOUT,
)
data = response.json()
if not data["success"]:
raise Exception(f"Erreur achat numéro : {data['error']['message']}")
order = data["data"]["orders"][0]
order_id = order["id"]
# `phone_number` est facultatif/nullable jusqu'à son attribution. Le create
# résolu reste résolu : une seule lecture bornée avec le même id, jamais un
# second create payant.
numero = order.get("phone_number")
if not (isinstance(numero, str) and numero.strip()):
# pending_assignment : la commande reste résolue et facturée.
return order_id, None
return order_id, numero
def attendre_sms(order_id, timeout=120, intervalle=5):
"""Attend le SMS avec polling configurable."""
start = time.time()
last_seen_revision = -1
# OTP_RECEIVED reste un état du cycle de vie ; le SMS est consommé selon sa révision.
while time.time() - start < timeout:
response = requests.get(
f"{BASE_URL}/orders/{order_id}",
headers=headers,
timeout=ORDER_TIMEOUT,
)
data = response.json()["data"]
revision = data.get("sms_revision")
message = data.get("otp_message")
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
last_seen_revision = revision
return data.get("otp_code"), message, revision
if data["status"] in ["COMPLETED", "EXPIRED", "CANCELED"]:
raise Exception(f"Commande terminée sans SMS : {data['status']}")
time.sleep(intervalle)
raise TimeoutError(f"SMS non reçu après {timeout}s")
def annuler_commande(order_id):
"""Annule uniquement lorsque l'état courant l'autorise."""
current = requests.get(
f"{BASE_URL}/orders/{order_id}",
headers=headers,
timeout=ORDER_TIMEOUT,
).json()["data"]
if current["can_cancel"]:
requests.post(
f"{BASE_URL}/orders/cancel",
json={"id": order_id},
headers=headers,
timeout=ORDER_TIMEOUT,
)
# Utilisation avec gestion complète des erreurs
def obtenir_code_verification(catalog_product_id):
"""Fonction principale : obtient un code de vérification SMS."""
solde = verifier_solde()
print(f"Solde disponible : Rp {solde:,}".replace(",", " "))
order_id, numero = acheter_numero(catalog_product_id)
if numero is None:
# pending_assignment : ne saisissez rien sur la plateforme cible et ne
# relancez pas de create ; le polling par id observera l'attribution.
print(f"pending_assignment : la commande {order_id} n'a pas encore de numéro")
return None, None, None
print(f"Numéro obtenu : {numero} (order: {order_id})")
try:
code, message, revision = attendre_sms(order_id)
print(f"SMS reçu (révision {revision}) : {message}")
return numero, code, message
except TimeoutError:
print("Timeout — vérification de l'éligibilité à l'annulation")
annuler_commande(order_id)
raise
# Exemple d'utilisation
numero, code, message = obtenir_code_verification(88)
print(f"Numéro : {numero}, Code classifié : {code}, Message : {message}")
JavaScript (Node.js) — Avec async/await
import { Agent, fetch } from 'undici';
const API_KEY = process.env.SMSCODE_API_KEY;
const BASE_URL = 'https://api.smscode.gg/v1';
const orderDispatcher = new Agent({ connectTimeout: 5_000 });
const orderTransport = () => ({
dispatcher: orderDispatcher,
signal: AbortSignal.timeout(30_000)
});
const defaultHeaders = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
async function apiRequest(method, endpoint, body = null, params = null, extraHeaders = {}) {
let url = `${BASE_URL}${endpoint}`;
if (params) {
url += '?' + new URLSearchParams(params).toString();
}
const options = {
method,
headers: { ...defaultHeaders, ...extraHeaders },
...orderTransport()
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(url, options);
const data = await response.json();
if (!data.success) {
throw new Error(`API Error: ${data.error.code} — ${data.error.message}`);
}
return data.data;
}
async function acheterNumero(catalogProductId) {
const body = { catalog_product_id: catalogProductId, quantity: 1 };
const idempotencyKey = crypto.randomUUID();
const data = await apiRequest(
'POST',
'/orders/create',
body,
null,
{ 'Idempotency-Key': idempotencyKey }
);
const order = data.orders[0];
// `phone_number` est facultatif/nullable jusqu'à son attribution. Le create
// résolu reste résolu : une seule lecture bornée avec le même id, jamais un
// second create payant.
return order;
}
function estAttribue(numero) {
return typeof numero === 'string' && numero.trim() !== '';
}
async function attendreSms(orderId, timeoutMs = 120000, intervalMs = 5000) {
const start = Date.now();
let lastSeenRevision = -1;
// OTP_RECEIVED reste un état du cycle de vie ; le SMS est consommé selon sa révision.
while (Date.now() - start < timeoutMs) {
const order = await apiRequest('GET', `/orders/${orderId}`);
const revision = order.sms_revision;
const message = order.otp_message;
if (
Number.isInteger(revision) &&
revision > lastSeenRevision &&
typeof message === 'string' &&
message.trim()
) {
lastSeenRevision = revision;
return {
code: order.otp_code,
message,
revision
};
}
if (['COMPLETED', 'EXPIRED', 'CANCELED'].includes(order.status)) {
throw new Error(`Order ended without SMS: ${order.status}`);
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Timeout — SMS not received');
}
async function annulerCommande(orderId) {
const current = await apiRequest('GET', `/orders/${orderId}`);
if (current.can_cancel) {
await apiRequest('POST', '/orders/cancel', { id: orderId });
}
}
// Fonction principale avec gestion des erreurs
async function obtenirCodeVerification(catalogProductId) {
let orderId = null;
try {
const order = await acheterNumero(catalogProductId);
orderId = order.id;
if (!estAttribue(order.phone_number)) {
// pending_assignment : la commande reste résolue et facturée ; ne rien
// saisir sur la plateforme cible et ne pas relancer de create.
return { kind: 'pending_assignment', orderId };
}
console.log(`Numéro obtenu : ${order.phone_number} (${orderId})`);
const { code, message, revision } = await attendreSms(orderId);
console.log(`Message reçu (révision ${revision}) : ${message}`);
return { number: order.phone_number, code, message };
} catch (error) {
if (orderId) {
console.log('Annulation de la commande...');
await annulerCommande(orderId).catch(() => {});
}
throw error;
}
}
// Utilisation
(async () => {
try {
const resultat = await obtenirCodeVerification(88);
if (resultat.kind === 'pending_assignment') {
// La commande reste résolue et facturée ; ne rien saisir sur la
// plateforme cible et ne pas relancer de create payant.
console.log(`pending_assignment : commande ${resultat.orderId} sans numéro`);
return;
}
const { number, code, message } = resultat;
console.log(`Numéro : ${number}, code classifié : ${code}, message : ${message}`);
} catch (error) {
console.error('Erreur :', error.message);
process.exit(1);
}
})();
PHP
<?php
$apiKey = getenv('SMSCODE_API_KEY');
$baseUrl = 'https://api.smscode.gg/v1';
function smscodeRequest($method, $endpoint, $data = null, $extraHeaders = []) {
global $apiKey, $baseUrl;
$ch = curl_init($baseUrl . $endpoint);
$headers = [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json'
];
$headers = array_merge($headers, $extraHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true);
if (!$decoded['success']) {
throw new Exception('API Error: ' . $decoded['error']['message']);
}
return $decoded['data'];
}
// Acheter un numéro
$body = ['catalog_product_id' => 88, 'quantity' => 1];
$idempotencyKey = bin2hex(random_bytes(16));
$data = smscodeRequest(
'POST',
'/orders/create',
$body,
['Idempotency-Key: ' . $idempotencyKey]
);
$order = $data['orders'][0];
$orderId = $order['id'];
// `phone_number` est facultatif/nullable jusqu'à son attribution. Le create résolu
// reste résolu : une seule lecture bornée avec le même id, jamais un second create payant.
$numero = $order['phone_number'] ?? null;
if (!(is_string($numero) && trim($numero) !== '')) {
$courant = smscodeRequest('GET', "/orders/$orderId");
$numero = is_array($courant) ? ($courant['phone_number'] ?? null) : null;
}
if (!(is_string($numero) && trim($numero) !== '')) {
// pending_assignment : la commande reste résolue et facturée ; ne rien saisir
// sur la plateforme cible et ne pas relancer de create.
echo "pending_assignment : la commande $orderId n'a pas encore de numéro\n";
exit(0);
}
echo "Numéro obtenu : $numero\n";
// Attendre le SMS avec polling
$timeout = 120;
$start = time();
$code = null;
$receivedMessage = null;
$lastSeenRevision = -1;
// OTP_RECEIVED reste un état du cycle de vie ; le SMS est consommé selon sa révision.
while (time() - $start < $timeout) {
$orderStatus = smscodeRequest('GET', '/orders/' . $orderId);
$revision = $orderStatus['sms_revision'] ?? null;
$message = $orderStatus['otp_message'] ?? null;
if (
is_int($revision)
&& $revision > $lastSeenRevision
&& is_string($message)
&& trim($message) !== ''
) {
$lastSeenRevision = $revision;
$receivedMessage = $message;
$code = $orderStatus['otp_code'] ?? null;
echo "SMS reçu (révision {$revision}) : {$message}\n";
break;
}
if (in_array($orderStatus['status'], ['COMPLETED', 'EXPIRED', 'CANCELED'])) {
die("Commande terminée sans SMS\n");
}
sleep(5);
}
if ($receivedMessage === null) {
echo "Timeout — vérification de l'éligibilité à l'annulation\n";
$current = smscodeRequest('GET', '/orders/' . $orderId);
if ($current['can_cancel']) {
smscodeRequest('POST', '/orders/cancel', ['id' => $orderId]);
}
}
Intégration avec Playwright — Tests E2E complets
Un des cas d’usage les plus puissants de l’API SMSCode est l’intégration dans des tests E2E automatisés. Voici un exemple concret avec Playwright et TypeScript :
// tests/helpers/smscode.ts
import { Agent, fetch } from 'undici';
const API_KEY = process.env.SMSCODE_API_KEY!;
const BASE_URL = 'https://api.smscode.gg/v1';
const orderDispatcher = new Agent({ connectTimeout: 5_000 });
const orderTransport = () => ({
dispatcher: orderDispatcher,
signal: AbortSignal.timeout(30_000)
});
interface SmsCodeResult {
number: string;
code: string;
orderId: number;
}
export async function getVerificationNumber(
catalogProductId: number
): Promise<SmsCodeResult> {
const body = { catalog_product_id: catalogProductId, quantity: 1 };
const idempotencyKey = crypto.randomUUID();
const orderResp = await fetch(`${BASE_URL}/orders/create`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(body),
...orderTransport()
});
const orderData = await orderResp.json();
if (!orderData.success) throw new Error(orderData.error.message);
const order = orderData.data.orders[0];
const orderId = order.id;
// `phone_number` est facultatif/nullable jusqu'à son attribution. Une seule
// lecture bornée avec le même id ; jamais un second create payant.
const estAttribue = (v) => typeof v === 'string' && v.trim() !== '';
const number = order.phone_number;
if (!estAttribue(number)) {
// pending_assignment : ne rien saisir dans le formulaire et ne pas relancer.
return { kind: 'pending_assignment', orderId };
}
let lastSeenRevision = -1;
// OTP_RECEIVED reste un état du cycle de vie ; le SMS est consommé selon sa révision.
// Polling jusqu'au code
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const statusResp = await fetch(`${BASE_URL}/orders/${orderId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
...orderTransport()
});
const statusData = await statusResp.json();
const {
status: lifecycleStatus,
sms_revision: revision,
otp_message: message,
otp_code: otpCode
} = statusData.data;
if (
Number.isInteger(revision) &&
revision > lastSeenRevision &&
typeof message === 'string' &&
message.trim()
) {
lastSeenRevision = revision;
const code = otpCode ?? message.match(/\b\d{4,8}\b/)?.[0];
if (!code) throw new Error('SMS reçu sans code classifiable');
return { number, code, orderId };
}
if (['COMPLETED', 'CANCELED', 'EXPIRED'].includes(lifecycleStatus)) {
throw new Error(`Commande terminée sans nouveau SMS : ${lifecycleStatus}`);
}
}
// Annulation si le snapshot courant l'autorise
const currentResp = await fetch(`${BASE_URL}/orders/${orderId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
...orderTransport()
});
const current = (await currentResp.json()).data;
if (current.can_cancel) {
await fetch(`${BASE_URL}/orders/cancel`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: orderId }),
...orderTransport()
});
}
throw new Error('Timeout local atteint sans état terminal');
}
// tests/signup.spec.ts
import { test, expect } from '@playwright/test';
import { getVerificationNumber } from './helpers/smscode';
test('inscription complète avec vérification SMS', async ({ page }) => {
// Obtenir le numéro AVANT d'ouvrir le formulaire
const resultat = await getVerificationNumber(88);
// `phone_number` est facultatif/nullable jusqu'à son attribution : ne jamais
// remplir le formulaire sans numéro attribué. La commande reste résolue et
// facturée ; aucun second create payant.
test.skip(
resultat.kind === 'pending_assignment',
`pending_assignment : la commande ${resultat.orderId} n'a pas encore de numéro`
);
const { number, code } = resultat;
await page.goto('https://accounts.google.com/signup');
// ... remplir le formulaire jusqu'à la demande de téléphone ...
await page.fill('#phoneNumberId', number.replace('+91', '')); // sans indicatif
await page.click('#next');
// Entrer le code reçu
await page.fill('#code', code);
await page.click('#next');
await expect(page.locator('.account-created')).toBeVisible({ timeout: 10000 });
});
Bonnes pratiques pour l’intégration API
Polling borné. Configure dans ton application un intervalle et un timeout total bornés. Sur un
429, respecte un Retry-After positif ; s’il manque ou est invalide, utilise un délai de repli
borné. Un intervalle local n’est ni un quota ni un délai de livraison garanti par l’API.
Message avant statut terminal. Conserve last_seen_revision = -1 par commande, puis traite toute
révision entière strictement plus récente dont le message est non vide avant de réagir au statut
terminal du snapshot.
Toujours prévoir un timeout. Au terme du timeout local, relis le snapshot courant. N’annule que
si can_cancel vaut true, et ne considère pas le timeout local comme une preuve d’annulation ou de
remboursement.
Variables d’environnement obligatoires. Ne hard-code jamais ta clé API. Utilise process.env.SMSCODE_API_KEY (Node.js), os.environ.get("SMSCODE_API_KEY") (Python), ou getenv('SMSCODE_API_KEY') (PHP). Ajoute .env à ton .gitignore.
Gestion fail-closed des erreurs de création. NO_OFFER_AVAILABLE, VALIDATION_ERROR, PROVIDER_ERROR et IDEMPOTENCY_KEY_REUSED sont les seuls échecs définitifs autorisés par cette intégration. INSUFFICIENT_BALANCE arrête le flux après un seul POST. Pour REQUEST_IN_PROGRESS, réessaie de façon bornée avec la même clé et exactement le même body, puis conserve les éléments de rapprochement. Tout code inconnu, JSON malformé ou résultat ambigu arrête le flux sans lancer un second achat.
Sélection avant achat. Choisis un catalog_product_id disponible avant le POST. Après une réponse de création ambiguë, ne bascule jamais vers un autre pays ou produit : rapproche d’abord la tentative originale avec son Idempotency-Key et son body exact.
Annulation explicite en cas d’échec. Relis l’état courant, puis envoie POST /v1/orders/cancel avec {"id": ...} uniquement si can_cancel vaut true. Ne déduis jamais l’éligibilité à l’annulation du seul statut local.
Vérification du solde en amont. Pour les workflows automatisés qui tournent en production, vérifie le solde au début de chaque session et configure une alerte si le solde passe sous un seuil minimum.
Codes d’erreur courants
| Code | Signification | Solution recommandée |
|---|---|---|
INSUFFICIENT_BALANCE |
Solde insuffisant | Recharge le compte, configure une alerte de solde bas |
SERVICE_UNAVAILABLE |
API temporairement indisponible | Pour un create payé, arrête et rapproche la tentative avant tout autre achat |
NO_OFFER_AVAILABLE |
Aucun produit ne satisfait la sélection | Arrête cet achat ; une autre sélection est une nouvelle action explicite |
NOT_FOUND |
Ressource demandée inexistante | Vérifie l’order_id et le compte authentifié |
CONFLICT |
Requête incompatible avec l’état courant | Relis le snapshot courant avant de décider |
UNAUTHORIZED |
Clé API incorrecte ou expirée | Régénère la clé dans les paramètres du compte |
RATE_LIMIT_EXCEEDED |
Trop de requêtes | Implémente un backoff exponentiel, réduis la fréquence |
Rate limits et scaling
Ne fige pas de quota numérique dans le client. Limite la concurrence côté application et, sur un
429, respecte un Retry-After positif. S’il manque ou est invalide, applique un délai de repli
borné avant une nouvelle requête autorisée.
FAQ
L’API SMSCode est-elle compatible avec les intégrations SMS-Activate existantes ?
Pas directement. SMS-Activate utilise un protocole à actions dans la query string avec des réponses en texte, tandis que l’API publique SMSCode utilise l’authentification Bearer, des endpoints REST et des envelopes JSON. Une migration doit adapter l’authentification, les endpoints, les corps de requête, les identifiants et les états ; changer uniquement l’URL de base ne suffit pas.
Comment gérer le cas où plusieurs tests s’exécutent en parallèle ?
Limite la concurrence dans ton application. Chaque achat logique doit conserver son propre body,
Idempotency-Key stable et order_id; ne suppose ni une limite simultanée fixe ni l’unicité d’un
numéro au-delà de la réponse associée à cette commande.
Comment vérifier l’effet d’une expiration ou d’une annulation sur le solde ?
Une commande qui expire sans SMS passe côté serveur à EXPIRED avec restitution atomique du montant débité ; si un SMS a déjà été reçu, elle peut être finalisée sans restitution. Pour une annulation manuelle, relis l’ordre et utilise POST /v1/orders/cancel uniquement lorsque can_cancel vaut true, puis prends refund_amount et new_balance dans la réponse réussie. N’infère jamais l’effet financier d’une horloge locale ou du seul envoi de la requête.
Y a-t-il un SDK officiel pour l’API SMSCode ?
Les exemples de code dans ce guide utilisent directement l’API REST avec des clients HTTP standard en Python, JavaScript/TypeScript et PHP. Consulte la référence API actuelle avant de choisir un wrapper ; ne suppose pas l’existence d’un SDK officiel ou communautaire non déclaré par cette référence.
Comment tester l’API sans consommer de crédits réels ?
Il n’existe pas d’environnement sandbox : chaque création de commande utilise le solde réel en IDR. Pour tester à moindre coût, choisis un produit actif à bas prix dans le catalogue, conserve la même clé et le même corps en cas de réponse ambiguë, puis réconcilie la commande avant toute nouvelle création payante.
Quelle est la différence entre l’API v1 et l’interface web ?
L’interface web et l’API v1 accèdent au même pool de numéros et aux mêmes prix — la différence est uniquement dans la façon d’interagir avec le service. L’interface web est conçue pour l’usage manuel visuel ; l’API est conçue pour l’automatisation programmatique. Un développeur peut utiliser les deux selon le contexte — l’API pour les tests automatisés, l’interface web pour les vérifications ponctuelles.