SMSCode API Guide für Entwickler: Virtuelle Nummern programmatisch nutzen

SMSCode API Guide für Entwickler: Virtuelle Nummern programmatisch nutzen

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

  1. Kostenloses Konto erstellen
  2. Guthaben aufladen und im aktuellen Katalog ein aktives, preisgünstiges Produkt für begrenzte Tests wählen (siehe Zahlungsmethoden)
  3. API-Token im Dashboard generieren
  4. Ersten API-Call mit cURL testen: curl -H "Authorization: Bearer TOKEN" https://api.smscode.gg/v1/balance
  5. Einfaches Skript in der bevorzugten Sprache schreiben (die Beispiele oben als Ausgangspunkt nutzen)
  6. 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


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.

Bereit, SMSCode auszuprobieren?

Erstellen Sie ein Konto und erhalten Sie Ihre erste virtuelle Nummer in unter zwei Minuten.

Jetzt starten →