Wer virtuelle Nummern regelmäßig oder in größeren Mengen nutzt, kommt an der API nicht vorbei. Statt jede Nummer manuell über das Dashboard zu buchen, lässt sich der gesamte Prozess – von der Nummernbeschaffung bis zum SMS-Empfang – vollständig automatisieren. Dieser Guide erklärt, wie das geht, welche Fallstricke es gibt und wie man die API sicher und effizient in eigene Anwendungen integriert.
TL;DR: Die SMSCode REST API ermöglicht vollautomatische Nummer-Buchung und SMS-Abholung. Mit Bearer-Token-Auth, klaren Endpunkten und Polling-Mechanismus lässt sich die API in wenigen Minuten integrieren. Code-Beispiele in JavaScript, Python und cURL inklusive. Für CI/CD-Pipelines, automatisierte Tests und Konto-Provisioning ist die API das richtige Werkzeug.
Warum die API statt dem Dashboard?
Das Dashboard von SMSCode ist komfortabel für gelegentliche, manuelle Nutzung. Sobald aber wiederkehrende Abläufe entstehen — beim Testen von Registrierungs-Flows in einer CI/CD-Pipeline, beim automatisierten Aufsetzen von Testkonten für verschiedene Plattformen oder beim Aufbau eigener interner Tools — wird manuelle Bedienung zum Engpass.
Die API löst dieses Problem auf mehreren Ebenen:
- Skalierbarkeit: Hunderte Nummern in einem einzigen Skript buchen, ohne manuellen Eingriff
- Integration: Direkt in CI/CD-Pipelines, Test-Frameworks (Playwright, Cypress, Selenium) oder eigene Backend-Services einbauen
- Echtzeit: SMS-Empfang über Polling ohne manuelles Nachschauen im Browser — der Code landet direkt in der Anwendung
- Zuverlässigkeit: Programmatische Fehlerbehandlung, automatische Retry-Logik und sauberes Stornieren nicht genutzter Nummern
- Reproduzierbarkeit: Verifizierungsflows können als Code versioniert, getestet und reproduziert werden
Typische Szenarien für API-Nutzung:
- Automatisierte End-to-End-Tests, die echte SMS-Verifizierung erfordern
- Provisioning von Unternehmenskonten auf verschiedenen Plattformen
- Entwicklertools und interne Portale für Teams
- Massenregistrierung bei Datenerhebungsprojekten (unter Einhaltung der jeweiligen Nutzungsbedingungen)
Authentifizierung
Die SMSCode API verwendet Bearer-Token-Authentifizierung. Den API-Token findet man im Dashboard unter den Account-Einstellungen. Token generieren, sicher verwahren und im Code niemals hartcodiert hinterlegen.
Authorization: Bearer <dein-api-token>
Sicherheitsregeln für den API-Token:
- Den Token niemals in öffentlichen Repositories, client-seitigem Code oder Anwendungs-Logs speichern
- Umgebungsvariablen (
process.env.SMSCODE_API_TOKEN) oder dedizierte Secret-Manager (HashiCorp Vault, AWS Secrets Manager, GitHub Secrets, GitLab CI/CD Variables) verwenden - Bei Verdacht auf Kompromittierung sofort im Dashboard invalidieren und einen neuen Token generieren
- Token regelmäßig rotieren, besonders nach dem Ausscheiden von Teammitgliedern mit Zugang
Der Token ist direkt mit dem SMSCode-Konto verknüpft — alle API-Anfragen werden entsprechend abgerechnet. Es gibt keine granulare Rechteverwaltung pro Token; jeder Token hat vollen Kontozugang.
Basis-URL und Versioning
https://api.smscode.gg/v1/
Die veröffentlichte OpenAPI beschreibt versionierte v1- und v2-Pfade; dieser Leitfaden verwendet
die IDR-native v1. Leiten Sie daraus keine feste Kompatibilitäts- oder Übergangsfrist ab, sondern
prüfen Sie die aktuelle OpenAPI bei Änderungen Ihrer Integration.
Kernendpunkte im Überblick
Katalog abfragen
Bevor eine Nummer gebucht wird, empfiehlt sich die Katalogabfrage — um verfügbare Länder, Dienste und aktuelle Preise zu kennen. Preise ändern sich dynamisch je nach Verfügbarkeit; immer die aktuellen Preise beim Buchen prüfen.
Alle Länder abfragen:
GET /v1/catalog/countries HTTP/1.1
Antwort (Auszug):
{
"success": true,
"data": [
{
"id": 7,
"code": "id",
"name": "Indonesia",
"dial_code": "62",
"emoji": "🇮🇩",
"active": true
}
]
}
Bestellbare Produkte für Land und Plattform abfragen:
GET /v1/catalog/products?country_id=7&platform_id=1 HTTP/1.1
Die Antwort enthält den aktuellen Preis, available und die stabile catalog_product_id, die für die Bestellung verwendet wird.
Nummer buchen (Order erstellen)
POST /v1/orders/create HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
Idempotency-Key: order-example-001
{
"catalog_product_id": 88,
"quantity": 1
}
Erfolgreiche Antwort:
{
"success": true,
"data": {
"orders": [{
"id": 90210,
"status": "ACTIVE",
"phone_number": "+12025551234",
"otp_code": null,
"otp_received_at": null,
"expires_at": "2026-03-16T12:15: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-16T12:02:00Z",
"replace_available_at": "2026-03-16T12:02:00Z"
}],
"failed_count": 0
}
}
phone_number ist in der Create-Antwort optional und kann bis zur Zuweisung null sein. Ein
robuster Ablauf liest dann mit derselben ganzzahligen id genau einmal und mit einem begrenzten
Request GET /v1/orders/{id}. Ist phone_number auch dort keine nicht leere Zeichenfolge, bleibt
der Ablauf im lokalen Zustand pending_assignment. Erst eine zugewiesene Nummer kann an den
externen Dienst (z. B. WhatsApp, eBay oder Discord) übergeben werden.
Wichtig: expires_at kommt vom Server. Läuft eine Bestellung ohne empfangene SMS ab, setzt der Server sie auf EXPIRED und erstattet den belasteten Betrag atomar. Ist bereits eine SMS eingegangen, kann der Server sie stattdessen ohne Erstattung abschließen. Leite die Geldwirkung deshalb nicht aus einer lokalen Uhr ab, sondern lies den aktuellen Snapshot und das Guthaben erneut.
SMS-Status pollen
GET /v1/orders/{order_id} HTTP/1.1
V1OrderSummary-Projektion mit eingegangener SMS (ausgewählte Felder, keine vollständige Wire-Response):
{
"success": true,
"data": {
"id": 90210,
"status": "OTP_RECEIVED",
"otp_code": null,
"otp_message": "Dein WhatsApp-Code: 123456",
"sms_revision": 1,
"otp_received_at": "2026-03-16T12:03:45Z"
}
}
Status-Werte im Überblick:
| Status | Bedeutung | Nächste Aktion |
|---|---|---|
ACTIVE |
Nummer aktiv, noch keine SMS | Weiter pollen |
OTP_RECEIVED |
Lifecycle-Zustand, keine Auswertungsbedingung | otp_message anhand von sms_revision verarbeiten |
COMPLETED |
Bestellung abgeschlossen | Erst eine neue SMS-Revision verarbeiten, dann Polling beenden |
CANCELED |
Bestellung storniert | Erst eine neue SMS-Revision verarbeiten, dann Polling beenden |
EXPIRED |
Server-Ablauf erreicht | Erst eine neue SMS-Revision verarbeiten, dann Polling beenden |
Ein später gelesener Snapshot kann bereits einen terminalen Status tragen und trotzdem eine noch
nicht verarbeitete, dauerhaft erfasste SMS enthalten. Initialisieren Sie deshalb pro Bestellung
last_seen_revision mit -1 und verarbeiten Sie eine ganzzahlige, strikt neuere sms_revision mit
nicht leerer otp_message, bevor Sie den terminalen Status auswerten.
Bestellung stornieren
Falls die SMS nicht ankommt, eine andere Nummer benötigt wird oder der Prozess abgebrochen werden soll:
POST /v1/orders/cancel HTTP/1.1
Content-Type: application/json
{"id":90210}
Den Request nur senden, wenn der aktuelle Order-Snapshot can_cancel: true meldet. Sobald eine SMS dauerhaft erfasst wurde, ist eine Erstattung ausgeschlossen.
Kontostand abfragen
GET /v1/balance HTTP/1.1
{
"success": true,
"data": {
"currency": "IDR",
"balance": 542000
}
}
Nützlich, um vor Massenoperationen sicherzustellen, dass ausreichend Guthaben vorhanden ist.
Code-Beispiele
JavaScript (Node.js)
Ein vollständiges Beispiel mit Fehlerbehandlung, Polling und automatischer Stornierung bei Timeout:
import { Agent } from 'undici';
const API_TOKEN = process.env.SMSCODE_API_TOKEN;
const BASE_URL = 'https://api.smscode.gg/v1';
const ORDER_CONNECT_TIMEOUT_MS = 5_000;
const ORDER_TOTAL_TIMEOUT_MS = 30_000;
const orderDispatcher = new Agent({ connectTimeout: ORDER_CONNECT_TIMEOUT_MS });
const orderTransport = () => ({
dispatcher: orderDispatcher,
signal: AbortSignal.timeout(ORDER_TOTAL_TIMEOUT_MS)
});
const headers = {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json'
};
async function getVirtualNumber(catalogProductId) {
const body = { catalog_product_id: catalogProductId, quantity: 1 };
const idempotencyKey = crypto.randomUUID();
const orderRes = await fetch(`${BASE_URL}/orders/create`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(body),
...orderTransport()
});
const order = await orderRes.json();
if (!order.success) {
throw new Error(`Bestellfehler: ${order.error.code} — ${order.error.message}`);
}
const { id, phone_number } = order.data.orders[0];
// `phone_number` ist optional/nullable bis zur Zuweisung. Der aufgelöste
// Auftrag bleibt aufgelöst: eine einzige gebundene Lesung mit derselben id,
// niemals ein zweites bezahltes create.
let number = phone_number;
if (!(typeof number === 'string' && number.trim())) {
const currentRes = await fetch(`${BASE_URL}/orders/${id}`, {
headers,
...orderTransport()
});
const current = await currentRes.json();
number = current.success ? current.data.phone_number : null;
}
if (!(typeof number === 'string' && number.trim())) {
return { kind: 'pending_assignment', orderId: id };
}
console.log(`Nummer reserviert: ${number} (Order: ${id})`);
try {
const otpMessage = await pollForSms(id);
return { number, message: otpMessage, orderId: id };
} catch (err) {
await cancelOrder(id);
throw err;
}
}
async function pollForSms(orderId, maxAttempts = 40, intervalMs = 4000) {
let lastSeenRevision = -1;
// OTP_RECEIVED bleibt ein Lifecycle-Zustand; die SMS wird revisionsbasiert verarbeitet.
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await sleep(intervalMs);
const res = await fetch(`${BASE_URL}/orders/${orderId}`, {
headers,
...orderTransport()
});
const data = await res.json();
if (!data.success) {
throw new Error(`Statusabfrage fehlgeschlagen: ${data.error?.message}`);
}
const {
status,
otp_message: message,
sms_revision: revision,
can_cancel
} = data.data;
if (
Number.isInteger(revision) &&
revision > lastSeenRevision &&
typeof message === 'string' &&
message.trim()
) {
lastSeenRevision = revision;
console.log(`SMS Revision ${revision}: ${message}`);
return message;
}
if (status === 'COMPLETED' || status === 'CANCELED' || status === 'EXPIRED') {
throw new Error(`Bestellung beendet mit Status: ${status}`);
}
if (!can_cancel) console.log('Stornierung ist derzeit nicht möglich');
console.log(`Warte auf SMS... (Versuch ${attempt + 1}/${maxAttempts})`);
}
throw new Error('Timeout: SMS nicht innerhalb des Zeitlimits empfangen');
}
async function cancelOrder(orderId) {
try {
const current = await fetch(`${BASE_URL}/orders/${orderId}`, {
headers,
...orderTransport()
}).then(r => r.json());
if (!current.data.can_cancel) return false;
const res = await fetch(`${BASE_URL}/orders/cancel`, {
method: 'POST',
headers,
body: JSON.stringify({ id: orderId }),
...orderTransport()
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error('Stornierung abgelehnt');
console.log(
`Bestellung ${orderId} storniert; ` +
`Erstattung Rp ${data.data.refund_amount}; ` +
`neues Guthaben Rp ${data.data.new_balance}`
);
} catch (err) {
console.error('Stornierung fehlgeschlagen:', err.message);
}
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
// Verwendung
getVirtualNumber(88)
.then((ergebnis) => {
if (ergebnis.kind === 'pending_assignment') {
// Der Auftrag bleibt aufgelöst und belastet; keine Nummer verwenden,
// kein zweites bezahltes create. `orderId` für die Abstimmung behalten.
console.log(`pending_assignment: Order ${ergebnis.orderId} ohne Nummer`);
return;
}
console.log(`Bereit: Nummer ${ergebnis.number}, SMS ${ergebnis.message}`);
})
.catch(err => {
console.error('Fehler:', err.message);
process.exit(1);
});
Python
import os
import time
import json
import requests
import uuid
from typing import Optional
API_TOKEN = os.environ['SMSCODE_API_TOKEN']
BASE_URL = 'https://api.smscode.gg/v1'
ORDER_TIMEOUT = (5, 30)
HEADERS = {
'Authorization': f'Bearer {API_TOKEN}',
'Content-Type': 'application/json'
}
class SmscodeError(Exception):
def __init__(self, code: str, message: str):
self.code = code
self.message = message
super().__init__(f"{code}: {message}")
def get_virtual_number(
catalog_product_id: int,
) -> dict:
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,
)
order = response.json()
if not order['success']:
raise SmscodeError(
order['error']['code'],
order['error']['message']
)
created = order['data']['orders'][0]
order_id = created['id']
# `phone_number` ist optional/nullable bis zur Zuweisung. Der aufgelöste
# Auftrag bleibt aufgelöst: eine einzige gebundene Lesung mit derselben id,
# niemals ein zweites bezahltes create.
phone_number = created.get('phone_number')
if not (isinstance(phone_number, str) and phone_number.strip()):
return {'kind': 'pending_assignment', 'order_id': order_id}
print(f'Nummer reserviert: {phone_number} (Order: {order_id})')
try:
otp_message = poll_for_sms(order_id)
return {'number': phone_number, 'message': otp_message, 'order_id': order_id}
except Exception as e:
cancel_order(order_id)
raise
def poll_for_sms(
order_id: int,
max_attempts: int = 40,
interval: float = 4.0
) -> str:
last_seen_revision = -1
# OTP_RECEIVED bleibt ein Lifecycle-Zustand; die SMS wird revisionsbasiert verarbeitet.
for attempt in range(max_attempts):
time.sleep(interval)
response = requests.get(
f'{BASE_URL}/orders/{order_id}',
headers=HEADERS,
timeout=ORDER_TIMEOUT,
)
data = response.json()
if not data['success']:
raise SmscodeError('POLL_ERROR', data.get('error', {}).get('message', 'Unbekannt'))
snapshot = data['data']
status = snapshot['status']
revision = snapshot.get('sms_revision')
message = snapshot.get('otp_message')
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
last_seen_revision = revision
print(f"SMS Revision {revision}: {message}")
return message
if status in ('COMPLETED', 'CANCELED', 'EXPIRED'):
raise SmscodeError('ORDER_ENDED', f'Bestellung beendet: {status}')
print(f'Warte auf SMS... (Versuch {attempt + 1}/{max_attempts})')
raise TimeoutError('SMS nicht innerhalb des Zeitlimits empfangen')
def cancel_order(order_id: int) -> bool:
try:
current = requests.get(
f'{BASE_URL}/orders/{order_id}',
headers=HEADERS,
timeout=ORDER_TIMEOUT,
).json()
if not current['data']['can_cancel']:
return False
response = requests.post(
f'{BASE_URL}/orders/cancel',
json={'id': order_id},
headers=HEADERS,
timeout=ORDER_TIMEOUT,
)
data = response.json()
if response.ok and data['success']:
result = data['data']
print(
f"Bestellung {order_id} storniert; "
f"Erstattung Rp {result['refund_amount']}; "
f"neues Guthaben Rp {result['new_balance']}"
)
return True
except Exception as e:
print(f'Stornierung fehlgeschlagen: {e}')
return False
def get_balance() -> int:
response = requests.get(f'{BASE_URL}/balance', headers=HEADERS)
data = response.json()
if data['success']:
return data['data']['balance']
raise SmscodeError('BALANCE_ERROR', 'Kontostand konnte nicht abgerufen werden')
# Verwendung
if __name__ == '__main__':
try:
balance = get_balance()
print(f'Guthaben: Rp {balance:,}')
result = get_virtual_number(catalog_product_id=88)
if result.get('kind') == 'pending_assignment':
# Auftrag bleibt aufgelöst und belastet; keine Nummer verwenden,
# kein zweites bezahltes create.
print(f"pending_assignment: Order {result['order_id']} ohne Nummer")
else:
print(f"SMS: {result['message']}")
except SmscodeError as e:
print(f'API-Fehler: {e}')
except TimeoutError as e:
print(f'Timeout: {e}')
cURL (für schnelle Tests und Shell-Skripte)
#!/bin/bash
# Guthaben prüfen
curl -s https://api.smscode.gg/v1/balance \
-H "Authorization: Bearer $SMSCODE_API_TOKEN" | jq .
# Länder abfragen
curl -s https://api.smscode.gg/v1/catalog/countries \
-H "Authorization: Bearer $SMSCODE_API_TOKEN" | jq '.data[] | {id, name, code}'
# Nummer buchen; denselben Schlüssel und denselben Body bei unklarem Ausgang wiederverwenden
IDEMPOTENCY_KEY="$(uuidgen)"
ORDER=$(curl -s --connect-timeout 5 --max-time 30 -X POST https://api.smscode.gg/v1/orders/create \
-H "Authorization: Bearer $SMSCODE_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{"catalog_product_id": 88, "quantity": 1}')
echo $ORDER | jq .
ORDER_ID=$(echo $ORDER | jq -r '.data.orders[0].id')
# `phone_number` ist optional/nullable bis zur Zuweisung. Eine einzige gebundene
# Lesung mit derselben id; niemals ein zweites bezahltes create.
# `// empty` fängt null/absent ab; `gsub` verwirft eine reine Whitespace-Zuweisung,
# denn die OpenAPI garantiert kein `minLength` für eine nicht leere Zeichenfolge.
NONBLANK='.data.orders[0].phone_number // empty | gsub("^\\s+|\\s+$";"") | select(length > 0)'
PHONE=$(echo $ORDER | jq -r "$NONBLANK")
if [ -z "$PHONE" ]; then
PHONE=$(curl -s --connect-timeout 5 --max-time 30 "https://api.smscode.gg/v1/orders/$ORDER_ID" \
-H "Authorization: Bearer $SMSCODE_API_TOKEN" \
| jq -r '.data.phone_number // empty | gsub("^\\s+|\\s+$";"") | select(length > 0)')
fi
if [ -z "$PHONE" ]; then
echo "pending_assignment: Order $ORDER_ID hat noch keine Nummer"
exit 0
fi
echo "Nummer: $PHONE (Order: $ORDER_ID)"
# Lokale Beispielgrenzen: höchstens 10 Polls mit je 5 Sekunden Abstand
LAST_SEEN_REVISION=-1
DELIVERED_MESSAGE=
# OTP_RECEIVED bleibt ein Lifecycle-Zustand; die SMS wird revisionsbasiert verarbeitet.
for i in $(seq 1 10); do
sleep 5
STATUS=$(curl -s --connect-timeout 5 --max-time 30 "https://api.smscode.gg/v1/orders/$ORDER_ID" \
-H "Authorization: Bearer $SMSCODE_API_TOKEN")
echo "Status: $(echo $STATUS | jq -r '.data.status')"
CURRENT_STATUS=$(echo "$STATUS" | jq -r '.data.status')
SMS_REVISION=$(echo "$STATUS" | jq -r '.data.sms_revision // empty')
SMS_MESSAGE=$(echo "$STATUS" | jq -r 'if (.data.otp_message | type) == "string" then .data.otp_message else "" end')
if [[ "$SMS_REVISION" =~ ^[0-9]+$ ]] \
&& [ "$SMS_REVISION" -gt "$LAST_SEEN_REVISION" ] \
&& [ -n "${SMS_MESSAGE//[[:space:]]/}" ]; then
LAST_SEEN_REVISION=$SMS_REVISION
DELIVERED_MESSAGE="$SMS_MESSAGE"
echo "SMS: $SMS_MESSAGE"
break
fi
if [ "$CURRENT_STATUS" = "COMPLETED" ] || [ "$CURRENT_STATUS" = "CANCELED" ] || [ "$CURRENT_STATUS" = "EXPIRED" ]; then
break
fi
done
# Nur ohne zugestellte SMS neu lesen und gegebenenfalls stornieren
if [ -z "${DELIVERED_MESSAGE//[[:space:]]/}" ]; then
CURRENT=$(curl -s --connect-timeout 5 --max-time 30 "https://api.smscode.gg/v1/orders/$ORDER_ID" \
-H "Authorization: Bearer $SMSCODE_API_TOKEN")
if [ "$(echo "$CURRENT" | jq -r '.data.can_cancel')" = "true" ]; then
curl -s --connect-timeout 5 --max-time 30 -X POST "https://api.smscode.gg/v1/orders/cancel" \
-H "Authorization: Bearer $SMSCODE_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"id\":$ORDER_ID}" | jq .
fi
fi
TypeScript-Typdefinitionen
Für TypeScript-Projekte empfehlen sich explizite Projektionen der verwendeten OpenAPI-Felder:
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: unknown;
};
}
interface OrderView {
id: number;
phone_number: string | null;
status: 'ACTIVE' | 'OTP_RECEIVED' | 'COMPLETED' | 'CANCELED' | 'EXPIRED';
otp_code: string | null;
otp_message: string | null;
sms_revision: number;
can_cancel: boolean;
expires_at: string | null;
otp_received_at: string | null;
}
interface Balance {
balance: number;
currency: 'IDR';
}
interface CountryView {
id: number;
name: string;
code: string | null;
active: boolean;
}
interface ProductView {
id: number;
name: string | null;
price: number;
available: number;
active: boolean;
catalog_product_id?: number | null;
country_id?: number | null;
platform_id?: number | null;
}
Polling-Strategie und Best Practices
Der SMS-Empfang ist grundsätzlich asynchron — nach dem Buchen der Nummer muss der Status periodisch abgefragt werden. Dabei sind mehrere Aspekte zu beachten:
Polling-Intervall: Legen Sie in Ihrer Anwendung ein begrenztes Intervall und ein Gesamt-Timeout
fest. Bei 429 gilt ein positiver Retry-After-Wert; fehlt er oder ist er ungültig, verwenden Sie
einen begrenzten Fallback. Lokale Intervalle sind keine von der API garantierte Quote oder
Zustellzeit.
SMS vor Status auswerten: Führen Sie pro Bestellung last_seen_revision = -1. Verarbeiten Sie
nur eine ganzzahlige, strikt neuere Revision mit nicht leerer Nachricht und tun Sie das vor der
Prüfung auf COMPLETED, CANCELED oder EXPIRED.
Maximale Anzahl von Versuchen: Lesen Sie nach Ablauf Ihres lokalen Timeouts den neuesten
Bestell-Snapshot. Stornieren Sie nur bei can_cancel: true; Serverantwort und Bestellrichtlinie sind
für Status und Saldoeffekt maßgeblich.
Exponentielles Backoff bei Netzwerkfehlern: Bei 5xx-Antworten oder Netzwerkproblemen sollte ein exponentielles Backoff implementiert werden, damit die API bei vorübergehenden Problemen nicht mit Anfragen überflutet wird:
import { Agent } from 'undici';
const pollDispatcher = new Agent({ connectTimeout: 5_000 });
const pollTransport = () => ({
dispatcher: pollDispatcher,
signal: AbortSignal.timeout(30_000)
});
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
await sleep(delay);
}
}
}
// Polling mit Retry
async function pollWithRetry(orderId) {
return withRetry(() => fetch(`${BASE_URL}/orders/${orderId}`, {
headers,
...pollTransport()
}));
}
Stornieren bei eigenem Timeout: Vor einer Stornierung den aktuellen Snapshot lesen und nur dann POST /v1/orders/cancel senden, wenn can_cancel wahr ist. Sobald eine SMS dauerhaft erfasst wurde, ist eine Erstattung nicht mehr möglich.
Idempotenz der Bestellungen: Den Idempotency-Key vor dem ersten bezahlten POST erzeugen. Bei einem unklaren Netzwerk- oder 5xx-Ausgang nur denselben Schlüssel mit exakt demselben Body wiederverwenden; niemals mit neuem Schlüssel oder geändertem Body fortfahren.
Fehlerbehandlung
Die API gibt konsistente Fehlerantworten in standardisiertem Format zurück:
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Nicht genügend Guthaben für diese Bestellung",
"details": {
"required": 0.05,
"available": 0.02
}
}
}
Häufige Fehlercodes und empfohlene Behandlung:
| Code | HTTP-Status | Bedeutung | Empfohlene Behandlung |
|---|---|---|---|
UNAUTHORIZED |
401 | Ungültiger oder fehlender API-Token | Token prüfen, kein Retry |
INSUFFICIENT_BALANCE |
409 | Zu wenig Guthaben | Guthaben aufladen, dann neu versuchen |
SERVICE_UNAVAILABLE |
503 | API temporär nicht verfügbar | Bezahlten Create als unklar behandeln und zuerst abgleichen |
NOT_FOUND |
404 | Order-ID existiert nicht | Order-ID prüfen, kein Retry |
CONFLICT |
409 | Anfrage widerspricht dem aktuellen Zustand | Aktuellen Snapshot neu lesen |
RATE_LIMIT_EXCEEDED |
429 | Zu viele Anfragen | Retry-After-Header auslesen, warten |
INTERNAL_ERROR |
500 | Interner Serverfehler | Bezahlten Create als unklar behandeln und abgleichen |
Behandle den HTTP-Status und den strukturierten Fehlercode gemeinsam. Ein 429 verlangt begrenztes Warten, ein Konflikt verlangt einen neuen Snapshot, und ein Netzwerk- oder 5xx-Ergebnis eines bezahlten Create bleibt unklar: gleiche Idempotency-Key-/Body-Kombination abgleichen, statt blind eine neue Bestellung zu erzeugen.
Integration in CI/CD-Pipelines
Für Entwickler, die Registrierungs-Flows in automatisierten Tests abdecken, ist die API-Integration in CI/CD-Pipelines besonders wertvoll. Damit können echte SMS-Verifizierungen als Teil des Test-Suites ausgeführt werden — nicht mit Mock-Daten, sondern mit real zugestellten Codes.
GitHub Actions Beispiel:
name: E2E Tests mit SMS-Verifizierung
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Node.js einrichten
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Abhängigkeiten installieren
run: npm ci
- name: Playwright installieren
run: npx playwright install --with-deps
- name: E2E-Tests ausführen
env:
SMSCODE_API_TOKEN: ${{ secrets.SMSCODE_API_TOKEN }}
BASE_URL: ${{ vars.TEST_BASE_URL }}
run: npx playwright test
Der API-Token wird als verschlüsseltes Secret in GitHub Actions hinterlegt — niemals als Klartext in workflow.yml oder im Repository.
GitLab CI Beispiel:
e2e_with_sms:
stage: test
image: mcr.microsoft.com/playwright:v1.41.0-jammy
script:
- npm ci
- npx playwright test
variables:
SMSCODE_API_TOKEN: $SMSCODE_API_TOKEN
only:
- main
- merge_requests
Playwright-Testbeispiel (TypeScript):
import crypto from 'node:crypto';
import { Agent, fetch } from 'undici';
import { test, expect } from '@playwright/test';
const orderDispatcher = new Agent({ connectTimeout: 5_000 });
const orderTransport = () => ({
dispatcher: orderDispatcher,
signal: AbortSignal.timeout(30_000)
});
async function getSmsMessage(catalogProductId: number): Promise<{ number: string; message: string }> {
const token = process.env.SMSCODE_API_TOKEN!;
const baseUrl = 'https://api.smscode.gg/v1';
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const requestBody = { catalog_product_id: catalogProductId, quantity: 1 };
const idempotencyKey = crypto.randomUUID();
const orderRes = await fetch(`${baseUrl}/orders/create`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(requestBody),
...orderTransport()
});
const order = await orderRes.json();
const { id, phone_number } = order.data.orders[0];
// `phone_number` ist optional/nullable bis zur Zuweisung. Eine einzige
// gebundene Lesung mit derselben id; niemals ein zweites bezahltes create.
let number = phone_number;
if (!(typeof number === 'string' && number.trim())) {
const currentRes = await fetch(`${baseUrl}/orders/${id}`, {
headers,
...orderTransport()
});
const current = await currentRes.json();
number = current.success ? current.data.phone_number : null;
}
if (!(typeof number === 'string' && number.trim())) {
return { kind: 'pending_assignment', orderId: id };
}
let lastSeenRevision = -1;
// OTP_RECEIVED bleibt ein Lifecycle-Zustand; die SMS wird revisionsbasiert verarbeitet.
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 5000));
const statusRes = await fetch(`${baseUrl}/orders/${id}`, {
headers,
...orderTransport()
});
const status = await statusRes.json();
const {
status: lifecycleStatus,
sms_revision: revision,
otp_message: message
} = status.data;
if (
Number.isInteger(revision) &&
revision > lastSeenRevision &&
typeof message === 'string' &&
message.trim()
) {
lastSeenRevision = revision;
return { number, message };
}
if (['COMPLETED', 'CANCELED', 'EXPIRED'].includes(lifecycleStatus)) {
throw new Error(`Bestellung beendet ohne neue SMS: ${lifecycleStatus}`);
}
}
throw new Error('SMS-Code nicht empfangen');
}
test('Registrierung mit SMS-Verifizierung', async ({ page }) => {
const ergebnis = await getSmsMessage(88);
// `getSmsMessage` liefert eine unterschiedene Union: eine zulässige nicht
// zugewiesene Nummer ist KEIN Fehler. Ohne diese Verzweigung würde das
// Destructuring den Auftrag in einen TypeError verwandeln und die bezahlte
// `orderId` verlieren, statt sie zu melden.
test.skip(
ergebnis.kind === 'pending_assignment',
`pending_assignment: Order ${ergebnis.orderId} hat noch keine Nummer`
);
const { number, message } = ergebnis;
const code = message.match(/\b\d{4,8}\b/)?.[0];
if (!code) throw new Error('Kein OTP in otp_message gefunden');
await page.goto('/register');
await page.fill('[name="phone"]', number);
await page.click('button[type="submit"]');
// Warte auf SMS-Eingabefeld
await page.waitForSelector('[autocomplete="one-time-code"]');
await page.fill('[autocomplete="one-time-code"]', code);
await page.click('[data-testid="verify-button"]');
await expect(page).toHaveURL('/dashboard');
});
Rate Limits
Fixieren Sie keine numerische Quote im Client. Begrenzen Sie die Parallelität in Ihrer Anwendung und
behandeln Sie 429 über Retry-After.
Falls Rate Limits greifen, antwortet die API mit 429 Too Many Requests und einem Retry-After-Header:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
Content-Type: application/json
{"success": false, "error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Zu viele Anfragen"}}
Die korrekte Behandlung mit einer absoluten Gesamtfrist:
import { Agent } from 'undici';
const getDispatcher = new Agent({ connectTimeout: 5_000 });
const getTransport = (remainingMs) => ({
dispatcher: getDispatcher,
signal: AbortSignal.timeout(Math.min(30_000, remainingMs))
});
async function apiGet(url, attempt = 0, deadline = Date.now() + 90_000) {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
throw new Error('Polling-Gesamtfrist erreicht');
}
const res = await fetch(url, { method: 'GET', headers, ...getTransport(remainingMs) });
if (res.status === 429) {
if (attempt >= 2) throw new Error('Bounded polling retry exhausted');
const parsed = Number(res.headers.get('Retry-After'));
const retryAfter = Number.isInteger(parsed) && parsed > 0
? Math.min(parsed, 60)
: 5;
const remaining = Math.max(0, deadline - Date.now());
if (remaining === 0) throw new Error('Polling-Gesamtfrist erreicht');
await sleep(Math.min(retryAfter * 1000, remaining));
if (Date.now() >= deadline) throw new Error('Polling-Gesamtfrist erreicht');
return apiGet(url, attempt + 1, deadline); // Nur für idempotente Leseanfragen
}
return res.json();
}
Die Obergrenze von 60 Sekunden, die Gesamtfrist von 90 Sekunden und das vor jedem GET auf den kleineren Wert aus 30 Sekunden und dem positiven Restbudget begrenzte Transportlimit sind lokale Schutzregeln dieses Beispielclients, keine von der API garantierte Quote, Zustellzeit oder SLA.
Sicherheits-Best-Practices für die API
Token-Zugriff: Ein Konto hat einen aktuellen API-Token. Wenn mehrere Anwendungen ihn verwenden, teilen sie dieselben Kontorechte und dasselbe Guthaben. Isoliere Umgebungen deshalb über getrennte Konten oder eigene Backend-Grenzen, statt mehrere Tokens pro Konto vorauszusetzen.
Token-Rotation: Regeneriere den aktuellen API-Token nach einem Sicherheitsvorfall oder wenn Zugriffsberechtigte ausscheiden. Die Regeneration ersetzt den bisherigen Token; aktualisiere danach alle autorisierten Server gemeinsam.
Logging mit Bedacht: Logge API-Antworten niemals vollständig in persistenten, dauerhaften Logs. SMS-Codes sind einmalige Sicherheitscodes — ein Log davon ist ein Sicherheitsrisiko. Logge Order-IDs und Status, aber nicht den otp_code-Wert selbst.
HTTPS immer: Alle API-Anfragen müssen über HTTPS laufen. Die SMSCode API ist ausschließlich über HTTPS erreichbar und lehnt HTTP-Verbindungen ab.
Token nie im Frontend: API-Token dürfen niemals in Browser-JavaScript, React-Apps, mobilen Apps oder anderen client-seitigen Anwendungen verwendet werden. Der Token hat Vollzugang zum Konto und würde in Client-Code für jeden Nutzer sichtbar sein. Immer einen Backend-Proxy verwenden:
Browser → eigenes Backend (mit Token) → SMSCode API
Guthaben-Monitoring: Bei automatisierten Prozessen regelmäßig den Kontostand prüfen und Alerts einrichten, bevor das Guthaben auf null fällt. Eine leere Balance führt zu Fehlern in Produktionsprozessen:
async function checkBalance(minimumBalance = 100000) {
const res = await fetch(`${BASE_URL}/balance`, { headers });
const data = await res.json();
const balance = data.data.balance;
if (balance < minimumBalance) {
// Alert senden (z. B. via Slack, PagerDuty, E-Mail)
throw new Error(`Niedriger Kontostand: Rp ${balance} < Rp ${minimumBalance}`);
}
return balance;
}
Praktische Anwendungsfälle
Automatisierte E2E-Tests: Registrierungs-Flows mit echter SMS-Verifizierung in Playwright-, Cypress- oder Selenium-Tests abdecken. Damit wird der komplette Registrierungsweg — inklusive SMS-Bestätigung — ohne manuelle Eingriffe und ohne Mocking reproduzierbar getestet.
Konto-Provisioning: Bei der automatisierten Einrichtung neuer Unternehmenskonten auf Plattformen wie eBay, LinkedIn oder anderen B2B-Diensten. Der gesamte Registrierungsflow — Formular ausfüllen, Nummer eingeben, Code abfragen, Konto aktivieren — läuft automatisch ab.
Multi-Konto-Management: Verschiedene Konten auf Plattformen mit jeweils eigenen Nummern ausstatten, ohne jede Nummer manuell zu buchen. Besonders nützlich bei der Verwaltung vieler Konten für verschiedene Kunden.
Interne Entwicklertools: Eigene interne Portale bauen, die Mitarbeitern die Nummernbeschaffung über ein zentrales Interface ermöglichen, ohne dass jeder Mitarbeiter direkten Zugang zum SMSCode-Dashboard braucht. Die SMSCode API wird dabei vom Backend des internen Tools angesprochen.
Load-Tests und Performance-Tests: Große Mengen Registrierungen simulieren, um Load-Verhalten der eigenen Plattform zu testen. Jede simulierte Registrierung braucht eine eigene Nummer — die API liefert diese programmatisch.
Monitoring und Alerting: Regelmäßige synthetische Checks, die den eigenen Registrierungsflow inklusive SMS testen, und bei Problemen Alerts auslösen. Damit werden Ausfälle im SMS-Versand der eigenen Plattform frühzeitig erkannt.
Erste Schritte
- Kostenloses Konto erstellen
- Guthaben aufladen und im aktuellen Katalog ein aktives, preisgünstiges Produkt für begrenzte Tests wählen (siehe Zahlungsmethoden)
- API-Token im Dashboard generieren
- Ersten API-Call mit cURL testen:
curl -H "Authorization: Bearer TOKEN" https://api.smscode.gg/v1/balance - Einfaches Skript in der bevorzugten Sprache schreiben (die Beispiele oben als Ausgangspunkt nutzen)
- In die eigene Anwendung oder CI/CD-Pipeline integrieren
Die vollständige API-Referenz mit allen Endpunkten, Parametern, Antwortformaten und Fehlercodes ist in der Entwickler-Dokumentation zu finden.
Weitere Anleitungen
- Was ist eine virtuelle Nummer?
- Deutsche Nummer bekommen
- Amerikanische Nummer bekommen
- Mehrere eBay-Konten anlegen
- API-Dokumentation
FAQ
Brauche ich Programmierkenntnisse, um die SMSCode API zu nutzen?
Grundlegende Kenntnisse in einer Programmiersprache (JavaScript, Python, PHP, Ruby, Go etc.) sind hilfreich, aber nicht zwingend erforderlich für einfachere Anwendungsfälle. Shell-Skripte mit cURL reichen für viele Automationsszenarien aus. Die Beispiele in diesem Guide sind so aufgebaut, dass sie auch ohne tiefe Programmierkenntnisse angepasst werden können. Für komplexe Integrationen in bestehende Systeme sind Entwicklerkenntnisse empfehlenswert.
Gibt es einen Sandbox- oder Testmodus?
Aktuell gibt es keinen separaten Sandbox-Modus. Paid Creates verwenden die reale API und das reale Guthaben. Für Tests ein aktives, preisgünstiges Katalogprodukt wählen, die Anzahl begrenzen und ein mehrdeutiges Ergebnis mit demselben Schlüssel und demselben Body abgleichen, bevor eine weitere Bestellung erstellt wird.
Welche Programmiersprachen werden unterstützt?
Da es sich um eine Standard-REST-API mit JSON handelt, ist sie mit jeder Programmiersprache nutzbar, die HTTP-Anfragen senden kann — etwa JavaScript/TypeScript, Python, PHP, Ruby, Go, Java, C#, Rust, Swift oder Kotlin. Dieser Guide enthält Beispiele für mehrere HTTP-Clients.
Wie sicher ist der API-Token?
Der API-Token gewährt Zugriff auf die öffentlichen API-Funktionen des Kontos und ist wie ein Passwort zu behandeln. Er gehört ausschließlich auf den Server, niemals in Browser-Code oder öffentliche Repositories. Bei Verdacht auf Kompromittierung den aktuellen Token im Dashboard regenerieren und alle autorisierten Server aktualisieren.
Was passiert bei einem Netzwerkfehler während einer Bestellung?
Nach einem Netzwerkfehler ist unbekannt, ob die Buchung angelegt wurde. Wiederhole denselben bezahlten Create nur begrenzt mit exakt demselben Idempotency-Key und demselben JSON-Body. Bewahre Request-Zeit, Key und Body für die Abgleichung auf und starte keine alternative Bestellung, solange das Ergebnis unklar ist.
Kann ich die API für mehrere Projekte gleichzeitig nutzen?
Ja. Mehrere Anwendungen können den aktuellen Token verwenden, greifen dann aber auf dasselbe Konto, Guthaben und dieselben Ressourcen zu. Für echte Sicherheits- oder Kostenisolation getrennte Konten beziehungsweise eigene Backend-Zugriffsgrenzen verwenden; ein einzelnes Konto stellt nicht mehrere parallele API-Tokens bereit.
Was ist der Unterschied zwischen der API und dem Dashboard?
Das Dashboard ist die grafische Benutzeroberfläche für manuelle, gelegentliche Nutzung — man klickt, kauft und liest Codes manuell. Die API ist die programmatische Schnittstelle für automatisierte Nutzung — sie wird von Skripten, Anwendungen und CI/CD-Systemen angesteuert. Beide greifen auf dasselbe Konto und dasselbe Guthaben zu. Das Dashboard eignet sich für Einzelverwendung und Tests; die API für Automatisierung, Skalierung und Integrationen.
Wie gehe ich mit dem Fall um, dass eine Nummer von der Zielplattform abgelehnt wird?
Wenn die Zielplattform eine Nummer ablehnt, lies zuerst den aktuellen Snapshot. Storniere nur bei can_cancel=true und bestätige refund_amount sowie new_balance aus der erfolgreichen Antwort. Erst danach darf eine neue logische Bestellung mit frisch gewähltem aktivem Katalogprodukt, neuem Body und neuem Idempotency-Key beginnen. Nach einem mehrdeutigen Create darf kein Länder- oder Produkt-Failover erfolgen.