Gästekartei lesen, anlegen und pflegen

Die Stammdaten der Gäste: exakte Suche statt Auskunftei, drei Sichtbarkeitsstufen, Merkmale additiv ändern und der Newsletter-Zustand als eine einzige verlässliche Frage.

Für Entwickler

Voraussetzungen

  • Recht `guests:read` zum Lesen, `guests:write` zum Anlegen und Ändern
  • Handlung `action:guests.personal` für `notes`, `isBlocked`, `blockReason`, `noShowCount`
  • Plan-Merkmal „Gäste / CRM"

ES GIBT KEINE UNSCHARFE SUCHE. `email` und `phone` treffen ausschliesslich VOLLSTÄNDIG und normalisiert; es gibt keinen Parameter `search` und kein `contains`. Der Grund: eine Teilstringsuche über Kontaktdaten macht aus der Schnittstelle ein Abfragesystem — „kennt dieses Haus diese Person, ist sie gesperrt, wie oft ist sie nicht erschienen". Ein Fehlschlag ist eine LEERE Liste, nie ein anderer Status und nie eine andere Form; sonst verriete schon die Antwort, dass es die Nummer gibt.

DREI SICHTBARKEITSSTUFEN. Mit `guests:read` sehen Sie Stammdaten, Merkmale, Zähler und den Newsletter-Zustand. Mit der zusätzlichen Handlung `action:guests.personal` kommt im EINZELABRUF `personal` dazu: `isBlocked`, `blockReason`, `noShowCount` und `notes`. In einer LISTE steht `personal` immer auf `null` — auch mit der Handlung, denn auf einem Bildschirm im Gastraum stünde sonst „zahlt schlecht" neben einem Namen. Und dann gibt es Felder, die niemand bekommt: die Newsletter-Token (wer sie hätte, trüge fremde Gäste aus dem Verteiler aus) und die Anzahlungs- und Gebührenregeln (ein Endpunkt, der zu einer Mailadresse einen Anzahlungsbetrag zurückgibt, wäre eine Auskunftei).

`personal: null` HEISST NICHT „KEINE NOTIZ". Es heisst „dem Schlüssel fehlt die Handlung". Der Unterschied gehört in die Antwort, damit niemand auf einer Abwesenheit aufbaut, die eine Rechtefrage ist.

DER NEWSLETTER-ZUSTAND IST EINE EINZIGE FRAGE: `newsletter.subscribed`. Er ist abgeleitet aus `optIn && unsubscribedAt === null` und wird mitgeliefert, weil sonst jeder Aufrufer die Verknüpfung selbst bildet — und wer sie falsch bildet, schreibt Menschen an, die sich ausgetragen haben. Über die API lässt sich nur eine ABMELDUNG melden (`newsletterOptIn: false`); eine Anmeldung entsteht ausschliesslich im Double-Opt-in des Betriebs. Dürfte ein Schlüssel hier `true` setzen, wäre jede Einwilligung im Verteiler eine Behauptung des Fremdsystems, und der Wirt haftet für die Werbemail.

MERKMALE (`tags`) LIEGEN ALS EINE LISTE IN EINER SPALTE. `tags` ERSETZT sie vollständig — genau daran stirbt ein Stammgastprogramm, das seine eigene Stufe zurückschreibt und dabei „Allergiker" und „Fensterplatz" des Hauses löscht. Deshalb gibt es `addTags` und `removeTags`, die additiv auf dem gelesenen Stand arbeiten. Beide Wege zu mischen ist ein Fehler und keine Auslegungsfrage: `tags` zusammen mit `addTags` ergibt 400.

`undefined` IST NICHT `null`. Ein fehlendes Feld in einem `PATCH` bleibt unverändert, ein ausdrückliches `null` leert. Ohne diese Trennung löscht ein Newsletter-Werkzeug, das nur eine Abmeldung meldet, dabei Telefonnummer und Firma des Gastes — der Aufrufer merkt es nie, der Wirt Monate später.

ZÄHLER UND BETRÄGE SIND ABGELEITET UND NICHT SETZBAR. `totalVisits`, `cancelCount`, `noShowCount`, `averagePartySize`, `lastVisit` und `totalSpentCents` entstehen aus Reservierungen und Statuswechseln. Wer sie mitschickt, bekommt 400 `read_only_field` mit der Begründung — nicht „unbekanntes Feld". `totalSpentCents` ist wie jeder Geldbetrag der API ein GANZZAHLIGER CENT-Wert; der Name trägt die Einheit, damit niemand teilt.

DER ABZUG DER GANZEN KARTEI IST ERLAUBT — die Übergabe an ein Newsletter-Werkzeug IST ein Vollabzug. Er ist aber gedeckelt (`limit` höchstens 200, Zeiger statt Seitenzahl, 120 zeilenliefernde Aufrufe je Stunde und Schlüssel) und beziffert: jede Antwort schreibt ihre Zeilenzahl mit, und daran hängt die Warnung an den Inhaber. Gleichen Sie inkrementell über `updatedSince` ab, statt jedes Mal alles zu lesen.

Die Routen

GET/api/v1/guests

Die Gästekartei seitenweise lesen — für den Abgleich mit einem Newsletter-Werkzeug, einem CRM oder der Buchhaltung.

Rechte

guests:read

Plan-Merkmal guests — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Abfrageparameter

NameTypBedeutung
limitinteger 1–200Vorgabe: 50Zeilen je Seite. Mehr als 200 liefert kein Aufruf, auch nicht auf Wunsch.
cursorundurchsichtiger Zeiger`pagination.nextCursor` der vorigen Antwort, unverändert weitergereicht.
updatedSinceISO-8601 mit ZoneNur Gäste, die sich seit diesem Zeitpunkt geändert haben. Der Filter, der aus einem Stundenkontingent ein Nichtproblem macht.
emailE-MailEXAKTER Treffer auf die vollständige Adresse. Kleinschreibung wird angeglichen, ein Teilstring trifft nie.
phoneRufnummerEXAKTER Treffer. Die Nummer wird wie beim Schreiben nach E.164 normalisiert; eine unvollständige Nummer ergibt 400, keine Trefferliste.
tagstring, 1–40 ZeichenGenau dieses Merkmal. Anführungszeichen und Rückstriche sind nicht erlaubt, weil das Merkmal samt seiner Klammern gesucht wird — sonst träfe „VIP" auch „VIP-Kunde".
viptrue | falseNur Gäste mit bzw. ohne VIP-Kennzeichnung.
newslettertrue | false`true` liefert AUSSCHLIESSLICH bestätigte und nicht ausgetragene Abonnenten — die einzige Menge, an die geschrieben werden darf. `false` liefert alle übrigen.

Mögliche Fehler

  • validationUnbekannter Parameter (ein stillschweigend ignoriertes `updated_since` wäre der teuerste Tippfehler dieser API), unlesbare Rufnummer, unlesbarer Cursor.
  • forbiddenDem Schlüssel fehlt `guests:read`.
  • plan_upgrade_requiredDer Plan des Betriebs enthält das Gästemodul nicht.
  • rate_limitedMinutenkontingent oder das Stundenkontingent für zeilenliefernde Gästeaufrufe (120 je Stunde und Schlüssel) ausgeschöpft.
  • Umschlag dieses Bereichs: `{ "data": [...], "pagination": { "limit", "nextCursor", "hasMore" }, "meta": { "restaurantId", "timezone" } }`.
  • Sortiert AUFSTEIGEND nach `updatedAt`, dann `id`. Eine Zeile, die während des Blätterns geändert wird, wandert ans Ende und wird in diesem Durchlauf noch gelesen.
  • `personal` ist in jeder Listenzeile `null` — auch mit der Handlung `guests.personal`.

POST/api/v1/guests

Einen Gast anlegen — etwa beim Import aus einem Altsystem.

Rechte

guests:write

Plan-Merkmal guests — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Kopfzeilen

NameTypBedeutung
Idempotency-Keystring bis 255 ZeichenFreiwillig, aber richtig: ohne ihn legt ein Netzwiederholversuch denselben Gast zweimal an — sofern keine Mailadresse dabei ist, die das auffängt.

Felder im Rumpf

NameTypBedeutung
namePflichtstring, 1–200 ZeichenDer Name des Gastes. Das einzige Pflichtfeld.
emailE-Mail oder nullKontaktadresse. Innerhalb eines Betriebs eindeutig — ein zweiter Gast mit derselben Adresse ist 409.
phoneRufnummer oder nullWird nach E.164 normalisiert gespeichert.
companystring bis 200 Zeichen oder nullFirma — für Geschäftsessen und Rechnungen.
tagsListe von bis zu 50 ZeichenkettenMerkmale des Hauses („Allergiker", „Fensterplatz"). Doppelte fallen weg.
isVIPbooleanVorgabe: falseKennzeichnung als VIP; steuert die Hervorhebung im Dashboard.
notesstring bis 5000 Zeichen oder nullInterne Notiz. Verlangt `action:guests.personal` — wer sie schreiben darf, muss sie auch lesen dürfen.
isBlockedbooleanSperrt den Gast für die Online-Buchung. Verlangt `action:guests.personal`.
blockReasonstring bis 1000 Zeichen oder nullBegründung der Sperre. Verlangt `action:guests.personal`; ohne Sperre wird sie beim Entsperren gelöscht.

Mögliche Fehler

  • validationSchema verletzt oder unbekanntes Feld im Rumpf.
  • read_only_fieldDer Rumpf enthält ein abgeleitetes Feld (`totalVisits`, `totalSpent`, `noShowCount`, `privacyConsentAt`, `newsletterConfirmedAt`, ein Token …). Die Meldung nennt je Feld den Grund.
  • forbidden_action`notes`, `isBlocked` oder `blockReason` ohne die Handlung `action:guests.personal`.
  • conflictZu dieser Mailadresse gibt es in diesem Betrieb bereits einen Gast. Suchen Sie ihn mit `GET /api/v1/guests?email=` und ändern Sie ihn.
  • forbiddenDem Schlüssel fehlt `guests:write`.
  • Antwortet mit 201 und `{ "data": { … }, "meta": { … } }`.
  • `source` setzt der Server auf `IMPORT` — über diese API angelegte Gäste tragen immer diesen Wert. `ADMIN` wäre eine Lüge (es war niemand im Dashboard).

GET/api/v1/guests/{id}

Einen Gast vollständig lesen, samt `personal` (mit Handlung) und ETag für ein späteres Schreiben.

Rechte

guests:read

Plan-Merkmal guests — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Pfad

NameTypBedeutung
{id}PflichtUUIDKennung des Gastes. Eine fremde Kennung ergibt 404 mit demselben Wortlaut wie eine erfundene — jede Abweichung wäre eine Auskunft.

Mögliche Fehler

  • not_foundZu dieser Kennung gibt es in diesem Betrieb keinen Gast.
  • forbiddenDem Schlüssel fehlt `guests:read`.
  • Liefert `ETag` in der Form `"g-1789412460123"` (stark, ohne `W/`). Aufheben und beim nächsten `PATCH` als `If-Match` zurückschicken.
  • Zählt gegen das Minutenkontingent, aber NICHT gegen das Stundenkontingent für zeilenliefernde Gästeaufrufe.

PATCH/api/v1/guests/{id}

Stammdaten, Merkmale und den Newsletter-Zustand eines Gastes ändern.

Rechte

guests:write

Plan-Merkmal guests — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Pfad

NameTypBedeutung
{id}PflichtUUIDKennung des Gastes.

Kopfzeilen

NameTypBedeutung
If-MatchETag-WertSchreibt nur, wenn niemand zwischen Ihrem Lesen und diesem Schreiben geschrieben hat. Sonst 412 mit dem aktuellen Wert.

Felder im Rumpf

NameTypBedeutung
namestring, 1–200 ZeichenNeuer Name.
emailE-Mail oder nullNeue Adresse; `null` leert sie. Eine bereits vergebene Adresse ist 409.
phoneRufnummer oder nullNeue Rufnummer; `null` leert sie.
companystring bis 200 Zeichen oder nullFirma setzen oder leeren.
isVIPbooleanVIP-Kennzeichnung setzen oder aufheben.
tagsListe von bis zu 50 ZeichenkettenERSETZT die gesamte Liste. Nicht mit `addTags`/`removeTags` mischbar.
addTagsListe von bis zu 50 ZeichenkettenErgänzt Merkmale auf dem gelesenen Stand — der Weg für ein Stammgastprogramm, das nur seine eigene Stufe pflegt.
removeTagsListe von bis zu 50 ZeichenkettenEntfernt genau diese Merkmale und lässt alle anderen stehen.
notesstring bis 5000 Zeichen oder nullInterne Notiz. Verlangt `action:guests.personal`.
isBlockedbooleanSperre setzen oder aufheben. Beim Entsperren fällt `blockReason` automatisch weg — ein Grund ohne Sperre ist eine Notiz über einen Menschen, die niemand mehr verantwortet.
blockReasonstring bis 1000 Zeichen oder nullBegründung der Sperre. Verlangt `action:guests.personal`.
newsletterOptInnur der Wert falseMeldet eine ABMELDUNG zurück und setzt dabei den Abmeldezeitpunkt, falls noch keiner gesetzt ist. `true` wird abgewiesen: eine Anmeldung entsteht ausschliesslich im Double-Opt-in des Betriebs.

Mögliche Fehler

  • nothing_to_writeDer Rumpf enthält kein änderbares Feld. Ein Aufruf, der nichts ändert, wird nicht als Erfolg beantwortet — sonst hält ein fehlerhafter Abgleich sich für erfolgreich.
  • validationSchema verletzt, `tags` zusammen mit `addTags`/`removeTags`, oder `newsletterOptIn: true`.
  • read_only_fieldEin abgeleitetes Feld im Rumpf.
  • forbidden_actionPersönliche Felder ohne `action:guests.personal`.
  • conflictDie neue Mailadresse gehört bereits einem anderen Gast dieses Betriebs.
  • precondition_failed`If-Match` passt nicht mehr.
  • not_foundZu dieser Kennung gibt es in diesem Betrieb keinen Gast.
  • Kein `DELETE`. Ein Löschbegehren läuft über `POST /api/v1/guests/{id}/anonymize` — eine gelöschte Gastzeile reisst Löcher in Berichte und Besuchszählung.

Codebeispiele

curl — Abonnenten seit dem letzten Lauf
curl
curl -sS -G https://tactictable.com/api/v1/guests \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \  --data-urlencode "newsletter=true" \  --data-urlencode "updatedSince=2026-09-13T00:00:00Z" \  --data-urlencode "limit=200"
`newsletter=true` liefert ausschliesslich bestätigte und nicht ausgetragene Abonnenten — die einzige Menge, an die geschrieben werden darf. Zusammen mit `updatedSince` ist das der ganze Abgleich.
Antwort 200 — GET /api/v1/guests
JSON
{  "data": [    {      "id": "fb69190b-e35f-5a2e-be8a-b7c272782903",      "name": "Anna Berger",      "email": "anna.berger@example.at",      "phone": "+4366412345678",      "company": null,      "tags": ["Stammgast", "Fensterplatz"],      "isVIP": true,      "source": "WIDGET",      "totalVisits": 12,      "lastVisit": "2026-08-29T18:00:00.000Z",      "cancelCount": 1,      "totalSpentCents": 0,      "averagePartySize": 3,      "privacyConsentAt": "2026-02-18T10:22:41.000Z",      "newsletter": {        "subscribed": true,        "optIn": true,        "confirmedAt": "2026-02-18T10:24:02.000Z",        "unsubscribedAt": null      },      "createdAt": "2026-02-18T10:22:41.000Z",      "updatedAt": "2026-09-14T07:12:44.019Z",      "personal": null    }  ],  "pagination": {    "limit": 200,    "nextCursor": "MjAyNi0wOS0xNFQwNzoxMjo0NC4wMTlafGZiNjkxOTBi",    "hasMore": true  },  "meta": {    "restaurantId": "35122c8e-d842-5b49-814c-0e8f2aaed157",    "timezone": "Europe/Vienna"  }}
`personal` ist in JEDER Listenzeile `null` — auch mit der Handlung. `totalSpentCents` steht hier auf 0, weil der Umsatz erst mit einer Kassenanbindung entsteht; er ist ganzzahliger Cent, nie Euro.
Antwort 200 — GET /api/v1/guests/{id} mit `guests.personal`
JSON
{  "data": {    "id": "fb69190b-e35f-5a2e-be8a-b7c272782903",    "name": "Anna Berger",    "email": "anna.berger@example.at",    "phone": "+4366412345678",    "company": "Berger Consulting GmbH",    "tags": ["Stammgast", "Fensterplatz"],    "isVIP": true,    "source": "WIDGET",    "totalVisits": 12,    "lastVisit": "2026-08-29T18:00:00.000Z",    "cancelCount": 1,    "totalSpentCents": 0,    "averagePartySize": 3,    "privacyConsentAt": "2026-02-18T10:22:41.000Z",    "newsletter": {      "subscribed": true,      "optIn": true,      "confirmedAt": "2026-02-18T10:24:02.000Z",      "unsubscribedAt": null    },    "createdAt": "2026-02-18T10:22:41.000Z",    "updatedAt": "2026-09-14T07:12:44.019Z",    "personal": {      "isBlocked": false,      "blockReason": null,      "noShowCount": 0,      "notes": "Allergie: Sellerie"    }  },  "meta": {    "restaurantId": "35122c8e-d842-5b49-814c-0e8f2aaed157",    "timezone": "Europe/Vienna"  }}
`source` sagt, wie der Gast entstanden ist: `WIDGET` (Buchungsstrecke), `ADMIN` (im Dashboard angelegt), `WAITLIST`, `NEWSLETTER`, `WALKIN`, `AI` (Telefon- oder Chatassistent), `IMPORT` (über diese API oder einen Import).
curl — Merkmal ergänzen, ohne die anderen zu löschen
curl
curl -sS -X PATCH \  https://tactictable.com/api/v1/guests/fb69190b-e35f-5a2e-be8a-b7c272782903 \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \  -H "Content-Type: application/json" \  -d '{"addTags": ["Gold"], "removeTags": ["Silber"]}'
Das ist der Weg für ein Stammgastprogramm. `{"tags": ["Gold"]}` hätte „Stammgast" und „Fensterplatz" gelöscht — die Merkmale des Hauses, die niemand wiederherstellt.
Antwort 400 — beide Wege gemischt
JSON
{  "error": "validation",  "message": "Der Rumpf ist nicht gueltig.",  "issues": [    {      "path": "tags",      "code": "custom",      "message": "`tags` ersetzt die gesamte Liste und laesst sich nicht mit `addTags`/`removeTags` mischen. Nehmen Sie entweder das eine oder das andere."    }  ],  "docs": "https://tactictable.com/dokumentation/api/fehler/validation",  "requestId": "req_8f31c0a94d2b47e6ba05"}
Kein Vorrang, keine Auslegung: entweder ersetzen oder ergänzen. Welches von beiden gemeint war, kann die API nicht wissen — und Raten kostet Merkmale.
TypeScript — Abgleich mit einem Newsletter-Werkzeug
TypeScript
interface Gast {    id: string    name: string    email: string | null    tags: string[]    isVIP: boolean    newsletter: {        subscribed: boolean        optIn: boolean        confirmedAt: string | null        unsubscribedAt: string | null    }    updatedAt: string} interface GaesteSeite {    data: Gast[]    pagination: { limit: number; nextCursor: string | null; hasMore: boolean }    meta: { restaurantId: string; timezone: string }} export async function abonnentenSeit(token: string, seit: string): Promise<Gast[]> {    const gesammelt: Gast[] = []    let cursor: string | null = null     for (let seite = 0; seite < 500; seite++) {        const p = new URLSearchParams({ newsletter: 'true', updatedSince: seit, limit: '200' })        if (cursor) p.set('cursor', cursor)         const antwort = await fetch('https://tactictable.com/api/v1/guests?' + p, {            headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },        })        if (antwort.status === 429) {            throw new Error('Stundenkontingent erschoepft — spaeter erneut, inkrementell.')        }        if (!antwort.ok) throw new Error('HTTP ' + antwort.status)         const s = (await antwort.json()) as GaesteSeite        gesammelt.push(...s.data)         if (!s.pagination.nextCursor) return gesammelt        cursor = s.pagination.nextCursor    }     throw new Error('Zu viele Seiten — Zeitfenster verkleinern.')} /** Nur diese Menge darf angeschrieben werden. Nie selbst zusammenrechnen. */export function darfAngeschriebenWerden(g: Gast): boolean {    return g.newsletter.subscribed && g.email !== null}
`newsletter.subscribed` ist bereits die richtige Verknüpfung. Wer stattdessen `optIn` allein prüft, schreibt Menschen an, die sich ausgetragen haben — und der Wirt haftet dafür.
Python — Abmeldung zurückmelden
Python
import json import requests  def abmeldung_melden(sitzung: requests.Session, gast_id: str) -> dict:    """Meldet eine im Fremdwerkzeug erfolgte Abmeldung zurueck."""    antwort = sitzung.patch(        "https://tactictable.com/api/v1/guests/" + gast_id,        data=json.dumps({"newsletterOptIn": False}),        headers={"Content-Type": "application/json"},        timeout=20,    )     rumpf = antwort.json()    if antwort.status_code != 200:        raise RuntimeError("{}: {}".format(rumpf["error"], rumpf["message"]))     return rumpf["data"]  # Eine ANMELDUNG laesst sich so nicht setzen: newsletterOptIn akzeptiert nur# False. Die Einwilligung entsteht im Double-Opt-in des Betriebs — alles# andere waere eine Behauptung des Fremdsystems, fuer die der Wirt haftet.
Der Abmeldezeitpunkt wird dabei gesetzt, wenn noch keiner steht. Ein bereits gesetzter bleibt: die erste Abmeldung ist die, die im Nachweis zählt.
PHP — Gast anlegen und den Konflikt vertragen
PHP
<?php function gast_anlegen(string $token, array $daten): array{    $ch = curl_init('https://tactictable.com/api/v1/guests');    curl_setopt_array($ch, [        CURLOPT_RETURNTRANSFER => true,        CURLOPT_CUSTOMREQUEST => 'POST',        CURLOPT_HTTPHEADER => [            'Authorization: Bearer ' . $token,            'Content-Type: application/json',            'Idempotency-Key: ' . bin2hex(random_bytes(16)),        ],        CURLOPT_POSTFIELDS => json_encode($daten, JSON_THROW_ON_ERROR),        CURLOPT_TIMEOUT => 30,    ]);     $rumpf = curl_exec($ch);    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);    curl_close($ch);     $json = json_decode($rumpf, true, 512, JSON_THROW_ON_ERROR);     if ($status === 409) {        // Die Mailadresse gibt es schon. Suchen statt ein zweites Mal anlegen.        return ['bereits_vorhanden' => true, 'email' => $daten['email'] ?? null];    }     if ($status !== 201) {        throw new RuntimeException($json['error'] . ': ' . $json['message']);    }     return $json['data'];}
Bei einem Import ist der 409 der häufigste Ausgang, nicht der seltenste. Suchen Sie den bestehenden Gast mit `GET /api/v1/guests?email=` und ändern Sie ihn, statt eine zweite Zeile anzulegen.
Ruby — einen Gast über die Mailadresse finden
Ruby
require 'json'require 'net/http' def gast_zu_mail(token, mail)  ziel = URI('https://tactictable.com/api/v1/guests')  ziel.query = URI.encode_www_form(email: mail, limit: 1)   anfrage = Net::HTTP::Get.new(ziel)  anfrage['Authorization'] = 'Bearer ' + token   antwort = Net::HTTP.start(ziel.hostname, ziel.port, use_ssl: true) do |http|    http.request(anfrage)  end   raise 'HTTP ' + antwort.code unless antwort.code == '200'   # Kein Treffer ist eine LEERE Liste — nie ein 404. Die Antwortform verraet  # damit nicht, ob es die Adresse in diesem Haus gibt.  JSON.parse(antwort.body)['data'].firstend
Der Filter trifft die vollständige Adresse, nie einen Teil davon. Wer eine gekaufte Adressliste durchprobiert, bekommt lauter leere Listen und lernt daraus nichts.