RiftVoiceLabAPI v1

RiftVoiceLab · Öffentlicher Zugang

Ein Schlüssel, ein Regal.

Text hinein, MP3 heraus — mit genau den Stimmen, die deinem Schlüssel zugeteilt sind. Ein Endpunkt zum Auflisten, einer zum Sprechen. Mehr braucht es nicht.

Basishttps://…
AnmeldungAuthorization: Bearer rvl_…
FormatJSON hinein · audio/mpeg heraus

Schnellstart

Drei Schritte. Der erste passiert nicht hier, sondern in der Werkbank.

  1. Schlüssel holen. Werkbank → Reiter API → Name vergeben, Stimmen anhaken, Schlüssel ausstellen. Der Schlüssel steht genau einmal auf dem Bildschirm. Danach liegt bei uns nur noch sein Abdruck — verloren heißt neu ausstellen.
  2. Stimmen nachsehen. GET /api/v1/voices zeigt dein Regal und die Kennungen darin.
  3. Sprechen lassen. POST /api/v1/speech mit text und voice.
# 1) Regal ansehen
curl -s https://HOST/api/v1/voices \
  -H "Authorization: Bearer $RIFT_KEY"

# 2) Sprechen lassen — die MP3 landet direkt in der Datei
curl -s https://HOST/api/v1/speech \
  -H "Authorization: Bearer $RIFT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Guten Abend. Der Ton steht.","voice":"VOICE_ID"}' \
  -o ansage.mp3
const KEY = process.env.RIFT_KEY;          // niemals im Frontend
const BASE = "https://HOST";

// 1) Regal ansehen
const { data: voices } = await fetch(`${BASE}/api/v1/voices`, {
  headers: { authorization: `Bearer ${KEY}` },
}).then((r) => r.json());

// 2) Sprechen lassen
const res = await fetch(`${BASE}/api/v1/speech`, {
  method: "POST",
  headers: {
    authorization: `Bearer ${KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    text: "Guten Abend. Der Ton steht.",
    voice: voices[0].id,
  }),
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const mp3 = Buffer.from(await res.arrayBuffer());
console.log("Rest heute:", res.headers.get("x-rift-quota-remaining"));
import os, requests

KEY  = os.environ["RIFT_KEY"]              # niemals im Repository
BASE = "https://HOST"
head = {"Authorization": f"Bearer {KEY}"}

# 1) Regal ansehen
voices = requests.get(f"{BASE}/api/v1/voices", headers=head).json()["data"]

# 2) Sprechen lassen
res = requests.post(
    f"{BASE}/api/v1/speech",
    headers=head,
    json={"text": "Guten Abend. Der Ton steht.", "voice": voices[0]["id"]},
    timeout=90,
)
res.raise_for_status()

with open("ansage.mp3", "wb") as f:
    f.write(res.content)

print("Rest heute:", res.headers["X-Rift-Quota-Remaining"])
<?php
$key  = getenv('RIFT_KEY');
$base = 'https://HOST';

$ch = curl_init("$base/api/v1/speech");
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => [
    "Authorization: Bearer $key",
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'text'  => 'Guten Abend. Der Ton steht.',
    'voice' => 'VOICE_ID',
  ]),
]);

$mp3    = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) { throw new RuntimeException($mp3); }
file_put_contents('ansage.mp3', $mp3);

Authentifizierung

Jede Anfrage außer /health braucht deinen Schlüssel. Zwei gleichwertige Wege:

Authorization: Bearer rvl_7fQ2x9aBc…
X-API-Key: rvl_7fQ2x9aBc…

Ein Schlüssel beginnt immer mit rvl_. Was danach kommt, sind 32 Zufallsbytes — geraten wird da nichts.

Einmal sichtbar

Beim Ausstellen. Gespeichert wird nur der SHA-256 des Schlüssels; niemand — auch die Werkbank nicht — kann ihn später wieder anzeigen.

Sofort widerrufbar

Stilllegen hält ihn an und lässt ihn stehen, Widerrufen löscht ihn, Neu ausstellen tauscht das Geheimnis und behält alle Einstellungen.

Keine Kekse

Eine Sitzung der Web-App öffnet die API nicht, und ein API-Schlüssel öffnet die Web-App nicht. Die beiden Wege teilen sich nur Engine und Stimmen.

Ein Schlüssel gehört auf einen Server. Wer ihn in Frontend-Code, ein Mobil-Bundle oder ein öffentliches Repository legt, verschenkt sein Kontingent. Bei Verdacht: Neu ausstellen — das kostet nichts und dauert eine Sekunde.

Stimmen & Zuteilung

Ein Schlüssel bekommt in der Werkbank entweder alle Stimmen oder eine ausdrückliche Liste. Diese Zuteilung ist die ganze Berechtigungslogik — es gibt keine zweite Ebene.

  • Alle Stimmen: auch die, die später dazukommen. Praktisch für eigene Dienste.
  • Auswahl: genau die angehakten. Neue Stimmen sind für diesen Schlüssel nicht dabei.
  • Was nicht zugeteilt ist, existiert nicht. Eine fremde Kennung beantwortet /speech mit 404 voice_not_found — derselben Antwort wie für eine Kennung, die es nirgends gibt. Aus der Antwort lässt sich das fremde Regal nicht abtasten.
  • Die interne Referenz bleibt drinnen. Nach draußen geht nur die kurze Kennung aus /voices, nie die Kennung, mit der die Engine arbeitet.
  • Über der Zuteilung steht der Riegel. Jede Stimme trägt in der Werkbank den Schalter öffentlich verwendbar. Steht er auf aus, ist die Stimme für den offenen Zugang nicht vorhanden — auch dann nicht, wenn ein Schlüssel sie ausdrücklich zugeteilt bekommen hat. Sie verschwindet aus /voices und /speech antwortet 404 voice_not_found.

Modelle

Zwei Wege, dieselbe Stimme. Welchen du nimmst, entscheidet das Feld model bei /speech.

ModellWas es tutZeichen je Zeichen
RiftVoiceV1 Spricht den Text so, wie er dasteht. Das ist der Standard und der Weg, den es immer gab. 1
RiftVoiceV2 Liest den Text vorher mit, setzt Betonung, Pausen und Stimmung selbst und spricht ihn dann — realistischer als jedes verfügbare Modell. Du schickst denselben Text wie bei V1 — um den Rest kümmert sich der Dienst. 2
Rift 2.1 Überragende Tonqualität. Dieselbe Regie wie bei V2, aber sparsam dosiert: höchstens eine Marke je zwei Sätze, und Pausen direkt neben einem Satzzeichen werden wieder entfernt — dort macht die Engine die Pause ohnehin, und beides zusammen klingt wie ein Versprecher. Dazu die Ausgabe mit 192 kbit/s statt 128. Eine Aufnahme, die zu kurz für den Text ist, wird nicht ausgeliefert, sondern neu genommen. Braucht keinen zweiten Durchgang am Text und läuft damit nicht länger als V2. Kurzformen: 2.1, rift2.1. 2,5

"v1" und "v2" sind als Kurzform ebenfalls erlaubt. Ohne model gilt RiftVoiceV1 — bestehende Aufrufe ändern sich also nicht. Ein Name, den es nicht gibt, ergibt 404 model_not_found; still ersetzt wird nie.

Was die höheren Modelle kosten. Bei RiftVoiceV2 zählt jedes Zeichen doppelt, bei Rift 2.1 zweieinhalbfach gegen das Tageskontingent. 100 Zeichen Text sind 200 Zeichen Kontingent. Was wirklich gebucht wurde, steht in jeder Antwort: im Kopf X-Rift-Billed-Chars und im Feld billedCharacters. Bleibt die Betonung einmal aus, wird der Aufschlag zurückgebucht und der Lauf kostet den einfachen Satz.

Grenzen & Kontingent

GrenzeWertWas passiert beim Überschreiten
Zeichen je Anfrage1000400 text_too_long
Zeichen je Tagpro Schlüssel gesetzt429 quota_exhausted
Anfragen je Minute60429 rate_limited mit Retry-After
gleichzeitig2409 concurrent_limit
Größe des Rumpfs64 kB413 payload_too_large

Abgerechnet wird die Länge des bereinigten Textes, nicht die der MP3 — bei RiftVoiceV2 mal zwei, Rift 2.1 mal zweieinhalb, siehe Modelle. Der Tageszähler springt um Mitternacht in Berlin zurück; wann genau, steht in jeder Antwort unter quota.resetsAt beziehungsweise im Kopf X-Rift-Quota-Reset.

Fehlschläge kosten nichts. Gebucht wird vor der Synthese, und wenn die nicht durchläuft, wird zurückgebucht. Ein 502 geht also nicht von deinem Kontingent ab.
GET

/api/v1/voices

Alle Stimmen, die dieser Schlüssel benutzen darf. Ohne Parameter, ohne Seitenzahlen.

Antwort 200

{
  "object": "list",
  "count": 2,
  "data": [
    { "id": "8kq2m1x9", "name": "Erzähler Nord", "tone": "warm, ruhig", "language": "DE" },
    { "id": "p4v7t0zc", "name": "Ansage Klar",   "tone": "sachlich",    "language": "DE" }
  ]
}
FeldTypBedeutung
idstringDas, was bei /speech ins Feld voice gehört.
namestringAnzeigename aus der Werkbank.
tonestringCharakterbeschreibung, kann leer sein.
languagestringSprachkürzel, kann leer sein.
GET

/api/v1/voices/{voiceId}

Eine einzelne Stimme nachschlagen — nützlich, um eine gespeicherte Kennung zu prüfen, bevor ein Batch losläuft.

Antwort 200

{ "id": "8kq2m1x9", "name": "Erzähler Nord", "tone": "warm, ruhig", "language": "DE" }

Nicht zugeteilt oder nicht vorhanden ergibt beides 404 voice_not_found.

GET

/api/v1/models

Welche Modelle es gibt und was sie kosten. Ohne Parameter.

Antwort 200

{
  "object": "list",
  "count": 3,
  "data": [
    {
      "id": "RiftVoiceV1",
      "alias": "v1",
      "name": "RiftVoiceV1",
      "characterCost": 1,
      "description": "Spricht den Text so, wie er dasteht. Ein Zeichen zählt als ein Zeichen.",
      "available": true
    },
    {
      "id": "RiftVoiceV2",
      "alias": "v2",
      "name": "RiftVoiceV2",
      "characterCost": 2,
      "description": "Liest den Text vorher mit, setzt Betonung, Pausen und Stimmung selbst und spricht ihn dann — realistischer als jedes verfügbare Modell. Ein Zeichen zählt als zwei Zeichen.",
      "available": true
    },
    {
      "id": "Rift 2.1",
      "alias": "r21",
      "name": "Rift 2.1",
      "characterCost": 2.5,
      "description": "Setzt Betonung sparsam statt überall, nimmt doppelte Pausen wieder heraus und gibt mit 192 kbit/s aus statt 128. Eine zu kurz geratene Aufnahme lässt es nicht durch, sondern nimmt sie neu. Ein Zeichen zählt als zweieinhalb Zeichen.",
      "available": true
    }
  ]
}
FeldTypBedeutung
idstringDas, was bei /speech ins Feld model gehört.
aliasstringKurzform, die dort ebenfalls gilt.
characterCostintegerWie viele Zeichen ein Zeichen vom Kontingent nimmt.
availablebooleanSteht hier false, ist das Modell gerade nicht abrufbar.
POST

/api/v1/speech

Der eigentliche Endpunkt. Erwartet JSON, liefert standardmäßig rohe MP3-Bytes.

Rumpf

FeldTypPflichtBedeutung
textstringjaWas gesprochen werden soll. Höchstens 1000 Zeichen nach dem Bereinigen.
voicestringjaKennung aus /voices. voice_id geht auch.
modelstringnein"RiftVoiceV1" (Standard), "RiftVoiceV2" oder "Rift 2.1"; "v1", "v2" und "2.1" gehen auch. Siehe Modelle.
formatstringneinDerzeit nur "mp3".
encodingstringnein"binary" (Standard) liefert audio/mpeg, "base64" liefert JSON.

Anfrage

{
  "text": "Guten Abend. Der Ton steht.",
  "voice": "8kq2m1x9",
  "model": "RiftVoiceV2"
}

Antwort 200 · audio/mpeg

Der Rumpf ist die MP3. Alles Zählbare steht in den Köpfen:

X-Rift-Request-Id:       rk9f2c1a4b7d0e3
X-Rift-Voice:            8kq2m1x9
X-Rift-Model:            RiftVoiceV2
X-Rift-Chars:            27
X-Rift-Billed-Chars:     54
X-Rift-Bytes:            41984
X-Rift-Ms:               1840
X-Rift-Quota-Remaining:  4973
X-Rift-Quota-Reset:      2026-09-04T00:00:00.000Z

Antwort mit "encoding": "base64"

{
  "object": "speech",
  "requestId": "rk9f2c1a4b7d0e3",
  "model": "RiftVoiceV2",
  "format": "mp3",
  "encoding": "base64",
  "audio": "SUQzBAAAAAAA…",
  "bytes": 41984,
  "characters": 27,
  "billedCharacters": 54,
  "durationMs": 1840,
  "voice": { "id": "8kq2m1x9", "name": "Erzähler Nord", "tone": "warm, ruhig", "language": "DE" },
  "quota": {
    "unlimited": false,
    "limit": 5000,
    "used": 27,
    "remaining": 4973,
    "day": "2026-09-03",
    "resetsAt": "2026-09-04T00:00:00.000Z"
  }
}

Base64 macht die Antwort rund ein Drittel größer. Für Warteschlangen und Webhooks bequem, für Durchsatz nicht.

Zeitbudget. Eine Synthese darf bis zu 55 Sekunden brauchen. Setz den Zeitablauf deines Clients auf mindestens 90 Sekunden, sonst brichst du Läufe ab, die noch gekommen wären.
GET

/api/v1/me

Zustand des Schlüssels: Kontingent, Grenzen, Umfang des Regals. Gut für ein Startprotokoll oder ein Dashboard.

Antwort 200

{
  "key": {
    "id": "km0q7c1f2a9b4d",
    "name": "Hörbuch-Pipeline",
    "hint": "rvl_7fQ2x9aB…4kQz",
    "active": true,
    "createdAt": "2026-09-01T09:12:44.201Z",
    "lastUsedAt": "2026-09-03T16:02:11.884Z",
    "expiresAt": null
  },
  "quota":  { "unlimited": false, "limit": 5000, "used": 27, "remaining": 4973,
              "day": "2026-09-03", "resetsAt": "2026-09-04T00:00:00.000Z" },
  "limits": { "maxTextLength": 1000, "requestsPerMinute": 60, "concurrentRequests": 2 },
  "voices": { "scope": "selected", "count": 2 },
  "totals": { "calls": 143, "characters": 38122 }
}

hint ist der Wiedererkennungswert des Schlüssels, nicht der Schlüssel.

GET

/api/v1/health

ohne Schlüssel

Sagt, ob der Dienst antwortet. Sonst nichts — kein Zustand, keine Zahlen.

{ "status": "ok", "time": "2026-09-03T16:02:11.884Z", "version": "1.0.0" }

Fehler

Immer JSON, immer dieselbe Form. Prüfe error.code, nicht den Wortlaut — Texte dürfen sich ändern, Codes nicht.

{
  "error": {
    "type": "rate_limit_error",
    "code": "quota_exhausted",
    "message": "Das Tageskontingent dieses Schluessels ist aufgebraucht.",
    "quota": { "limit": 5000, "used": 5000, "remaining": 0, "resetsAt": "2026-09-04T00:00:00.000Z" },
    "requestId": "rk9f2c1a4b7d0e3",
    "docs": "/api"
  }
}
HTTPcodeBedeutung & nächster Schritt
400text_requiredtext fehlt oder ist nach dem Bereinigen leer.
400text_too_longText kürzen oder in Abschnitte teilen.
400voice_requiredvoice fehlt.
400invalid_requestRumpf ist kein gültiges JSON-Objekt, oder ein Wert kennt diese Option nicht.
401missing_keyKopf fehlt ganz.
401invalid_keyUnbekannt oder widerrufen. Nicht wiederholen — neu ausstellen.
403key_disabledIn der Werkbank stillgelegt.
403key_expiredAblaufdatum überschritten.
404voice_not_foundNicht zugeteilt, nicht öffentlich oder nicht vorhanden. /voices neu lesen.
404model_not_foundUnbekanntes Modell. Erlaubt sind RiftVoiceV1, RiftVoiceV2 und Rift 2.1.
404not_foundPfad tippfehlerverdächtig.
409concurrent_limitZu viele Läufe gleichzeitig. Kurz warten, dann erneut.
429rate_limitedRetry-After abwarten.
429quota_exhaustedTageskontingent leer. resetsAt abwarten oder in der Werkbank erhöhen.
502synthesis_failedNicht durchgelaufen, nichts abgebucht. Wiederholen ist sinnvoll.
502voice_unavailableDiese Stimme antwortet gerade nicht. Andere versuchen.
503service_unavailableVorübergehend. Mit wachsendem Abstand wiederholen.
503maintenanceWartung — der ganze Zugang ist zu. Siehe Wartung.
Wiederholen mit Verstand. 429, 502 und 503 lohnen einen zweiten Versuch — mit Abstand, der sich verdoppelt. 4xx außer 429 lohnen keinen: die Anfrage bleibt falsch, egal wie oft sie kommt.

Zu jeder Antwort gehört eine requestId. Wenn du etwas gemeldet haben möchtest, nenn sie — damit findet sich der Lauf, ohne dass jemand deinen Schlüssel braucht.

Wartung

Wird die Werkstatt geschlossen, ist der Zugang vollständig zu — jeder Pfad unter /api, auch /health und der Bauplan. Es gibt in diesem Zustand keinen Endpunkt, der noch synthetisiert.

{
  "error": {
    "type": "api_error",
    "code": "maintenance",
    "message": "Wartung — der Zugang ist voruebergehend geschlossen.",
    "detail": "Kurze Wartung — gleich wieder da.",
    "retryAfter": 900,
    "requestId": "rk9f2c1a4b7d0e3",
    "docs": "/api"
  }
}
  • Status 503, Code maintenance. Behandle das wie service_unavailable: warten, dann erneut.
  • Retry-After beachten, wenn ein Ende angekündigt ist. Steht dort nichts, ist das Ende offen — dann mit wachsendem Abstand nachfragen.
  • Preflight bleibt beantwortet. Ein Browser bekommt darum eine ehrliche 503 statt eines nichtssagenden CORS-Fehlers.
  • Nichts wird abgebucht. Eine abgewiesene Anfrage kostet kein Kontingent.

Antwortköpfe

KopfWoInhalt
X-Rift-Request-IdüberallKennung dieses Laufs.
X-Rift-Voice/speechBenutzte Stimmkennung.
X-Rift-Model/speechModell, das gesprochen hat.
X-Rift-Chars/speechZeichen im Text.
X-Rift-Billed-Chars/speechTatsächlich gebuchte Zeichen — bei RiftVoiceV2 das Doppelte, bei Rift 2.1 das Zweieinhalbfache (aufgerundet).
X-Rift-Bytes/speechGröße der MP3.
X-Rift-Ms/speechDauer in Millisekunden.
X-Rift-Quota-Remaining/speechRest heute, oder unlimited.
X-Rift-Quota-Reset/speechWann der Zähler zurückspringt.
Retry-After429Sekunden bis zum nächsten Versuch.

Aus dem Browser sind diese Köpfe lesbar — sie stehen in Access-Control-Expose-Headers. Cookies gehen dagegen nie mit: die API kennt keine Sitzung.

Schlüssel sicher halten

  • Nur auf dem Server. Umgebungsvariable oder Geheimnisverwaltung — nicht ins Repository, nicht ins Bundle, nicht in ein Log.
  • Ein Schlüssel je Zweck. Getrennt für Produktion, Test und jeden Partner. Dann trifft ein Widerruf genau eine Sache.
  • So wenig Regal wie nötig. Ein Schlüssel für eine Stimme kann auch nur diese eine.
  • Kontingent als Bremse. Das Tageslimit ist die Obergrenze des Schadens, wenn ein Schlüssel doch einmal abhandenkommt.
  • Ablaufdatum für Gäste. Wer nur drei Wochen etwas bauen soll, braucht keinen Schlüssel, der ewig gilt.
  • Bei Verdacht neu ausstellen. Einstellungen und Zuteilung bleiben, nur das Geheimnis ist ein anderes.

Konsole

Echte Anfragen an diese Instanz. Der Schlüssel bleibt in diesem Tab und wird nirgendwo gespeichert — ein Neuladen leert das Feld.

Die Konsole ruft dieselben Endpunkte auf wie dein Code. Was hier funktioniert, funktioniert dort.