ANLEGEN VERSENDET NICHTS. `POST …/campaigns` legt einen ENTWURF (`DRAFT`) an: Kanal, Liste, Vorlage, bei E-Mail Betreff und Vorschautext (leer heisst: die der Vorlage). `scheduledAt` im Entwurf ist nur eine Vormerkung. Gesendet oder eingeplant wird ausschliesslich mit `POST …/campaigns/{id}/send`.
DREI WÄNDE VOR DEM VERSAND. Erstens `marketing:write` UND die ausdrücklich vergebene Handlung `action:marketing.send` — ohne sie antwortet der Wächter mit 403 `forbidden_action`, auch einem Schlüssel mit vollem Schreibrecht. Zweitens ist `Idempotency-Key` PFLICHT: eine Wiederholung nach einem Netzabbruch bekommt die gespeicherte Antwort und sendet nie ein zweites Mal. Drittens eine Bremse von 20 Sendungen je Stunde und Betrieb, gemeinsam gezählt mit dem Dashboard (429 mit `scope: "marketing.send"`).
WAS BEIM SENDEN PASSIERT. Der Inhalt (Vorlage, Betreff, Vorschautext, Tracking-Modus) wird EINGEFROREN; spätere Änderungen an der Vorlage erreichen diese Kampagne nicht mehr. Ohne `scheduledAt` geht sie auf `SENDING` und der Versand beginnt sofort; mit einem Zeitpunkt in der Zukunft (höchstens 365 Tage) auf `SCHEDULED`. Die Empfänger werden erst dann bestimmt: Mitglieder der Liste mit bestätigter Einwilligung für den Kanal, ohne Einträge der Sperrliste. Jede Mail trägt einen Abmeldelink (auch als One-Click-Kopfzeile), jede SMS „Abmelden: <Link>".
FACHLICHE ABLEHNUNGEN SIND `conflict` MIT `reason`. Fehlt Liste oder Vorlage, ist die Vorlage leer, fehlt der Betreff, ist der SMS-Versand nicht eingerichtet, ist die SMS mit Abmeldelink länger als vier Teile, das E-Mail-Kontingent des Monats aufgebraucht oder beim Sofortversand einer SMS-Kampagne das SMS-Guthaben leer (ohne automatische Aufladung), antwortet `…/send` mit 409 und dem Marketing-Code in `reason` (z. B. `marketing-campaign-template-empty`, `marketing-sms-not-ready`, `marketing-email-quota-exhausted`, `marketing-sms-no-credit`). Einplanen prüft Kontingent und Guthaben nicht — bis zum Termin kann der Monat wechseln bzw. aufgeladen werden. Ein Zeitpunkt in der Vergangenheit ist 400 am Feld `scheduledAt`. Der Prüfmodus (`X-TacticTable-Dry-Run: 1`) läuft alle diese Prüfungen durch, sendet nichts und liefert zusätzlich `estimate` (Mitglieder, erreichbar, Kontingent, geschätzte SMS-Kosten in Cent).
STATUS: `DRAFT` Entwurf, `SCHEDULED` eingeplant, `SENDING` im Versand, `PAUSED` angehalten (von Hand im Dashboard oder durch die Engine — `pauseReason` sagt warum: `manual`, `quota_exceeded`, `email_not_configured`, `sms_not_ready`, `sms_no_credit`, `plan_inactive` oder `restaurant_inactive` für einen archivierten bzw. sich schliessenden Betrieb), `SENT` fertig, `CANCELED` abgebrochen, `FAILED` gescheitert. Ändern geht in `DRAFT`, `SCHEDULED` und `PAUSED`; ändert sich bei einer geplanten Kampagne Inhalt oder Zeitpunkt, wird sie wieder Entwurf und muss neu gesendet werden. Nach dem Versandstart sind Liste und Kanal fest. Löschen geht in jedem Status ausser `SENDING` und entfernt Kampagne und Auswertung. Das Protokoll bereits übergebener Nachrichten bleibt stehen: Abmeldelinks, One-Click-Abmeldung und Beschwerdemeldungen aus zugestellten Mails und SMS wirken weiter.
ABBRECHEN (`…/cancel`) braucht keine Handlung — es hält Werbung an, statt sie auszulösen. Noch nicht übergebene Nachrichten werden übersprungen; was schon beim Anbieter ist, lässt sich nicht zurückholen. Pausieren und Fortsetzen gibt es in v1 bewusst nicht: Fortsetzen löst wieder Versand aus und bleibt dem Dashboard vorbehalten.
RATEN SIND ANTEILE VON 0 BIS 1, NICHT PROZENT. `openRate` und `clickRate` beziehen sich auf die MESSBAREN Empfänger (`trackedCount` — nur wer der Auswertung zugestimmt hat, wenn der Tracking-Modus „Nur mit Einwilligung" gilt), `deliveryRate` und `bounceRate` auf `sentCount`. Gezählt werden eindeutige, menschliche Öffnungen und Klicks; Vorabrufe von Sicherheits-Scannern und Apple Mail Privacy Protection zählen nicht. SMS haben keine Zustellbestätigung: dort sind `openRate`, `deliveryRate` und `bounceRate` `null`. `null` heisst immer „kein Nenner", nie 0 %.
DAS EMPFÄNGERPROTOKOLL (`…/recipients`) ist eine Liste von Menschen: den Namen gibt es nur mit `guests:read`, die volle Adresse nur zusätzlich mit `action:guests.personal` — sonst maskiert (`a…@example.at`). Mit Namen zählt jede Seite gegen das Gästekontingent. Wer nur Zahlen braucht, liest `…/stats`: Zeitverlauf (die ersten 72 Stunden stündlich, danach täglich), Links mit Klicks und Programme — ohne eine einzige Person.