Korat

Einen Bot für den eigenen Betrieb bauen

Diese Seite ist so geschrieben, dass ihr auch folgen kann, wer nicht programmiert; die technischen Einzelheiten für Entwickler stehen unten — nutz das Inhaltsverzeichnis rechts zum Springen.

Was ein Bot ist und wofür er taugt

Ein Bot ist eine weitere Art „Nutzerkonto“ auf Korat, hinter dem niemand tippt — dein eigenes Programm schickt eine Anfrage, und in dem Chat, in den der Bot eingeladen wurde, erscheint eine Nachricht. Echte Einsätze:

  • Neue Bestellungen in einen Chat melden — dein Bestellsystem bekommt eine neue Bestellung ⇒ der Bot schreibt sie direkt in den Kundenchat oder in den Raum des Teams
  • Warnung bei niedrigem Bestand — der Bestand fällt unter eine von dir gesetzte Schwelle ⇒ der Bot schreibt eine Warnung in den Raum des Teams
  • Meldungen zum Zustand von Servern und Systemen — dein eigenes Prüfskript ist mit einem Durchlauf fertig ⇒ der Bot meldet das Ergebnis in eine Gruppe, die deine IT beobachtet

Was ein Bot nicht kann (echte Grenzen aus der Datenbank, keine vorübergehenden Lücken):

  • Ein Bot kann Nachrichten in einem Raum nicht lesen — er sendet nur, es sei denn, du richtest selbst einen eingehenden Webhook ein (siehe die technische Referenz unten) — und selbst mit eingerichtetem Webhook wird heute nichts zugestellt, weil der Taktgeber, der die Warteschlange leeren muss, in der Produktion noch nicht eingeplant ist.
  • Ein Bot kann sich nicht selbst in einen Raum einladen — das muss immer jemand tun, der schon in diesem Raum ist (oder ihn verwalten darf).
  • Ein Bot kann höchstens 2 gültige Token zugleich haben, und ein Konto besitzt höchstens 5 Bots.

Loslegen — 4 Schritte

  1. 1Einen Bot anlegen. Gib ihm einen Namen und einen Benutzernamen (der Benutzername muss auf bot enden, etwa orderbot) — soll er einem Betrieb statt nur dir gehören, wähl den Betrieb schon beim Anlegen.
  2. 2Das Token kopieren. Es wird dir genau einmal gezeigt, direkt nach dem Anlegen — kopier es sofort an einen sicheren Ort. Nach dem Schließen dieses Bildschirms gibt es keinen Weg mehr, den echten Wert zu sehen (du müsstest stattdessen ein neues ausstellen).
  3. 3Den Bot in einen Raum einladen. Wähl den Chat, den Gruppenraum oder den Teamraum, in den der Bot schreiben soll — du musst selbst schon darin sein (oder ihn verwalten dürfen), um einzuladen.
  4. 4Die erste Nachricht senden. Nimm das Token aus Schritt 2 und ruf die Bot-API auf — ein lauffähiges Beispiel steht im nächsten Abschnitt.

Die Schritte 1–3 passieren in der Korat-App

Das Menü liegt unter Einstellungen › Konto › „Meine Bots“ — dort legst du einen Bot an, stellst Token aus, tauschst oder widerrufst sie und lädst ihn in einen Chat, Gruppen- oder Teamraum ein, alles ohne eine Zeile Code. Dieser Bildschirm kommt mit der nächsten Fassung der Korat-App — die Fassung, die du heute laden kannst, hat ihn noch nicht. Siehst du in den Einstellungen kein „Meine Bots“, warte auf ein Update der App. Bis dahin erledigst du die Schritte 1–3, indem du die Supabase-RPCs direkt aufrufst (siehe technische Referenz unten).

🔒 Ein Token ist das Passwort des Bots

Wer dieses Token hat, kann sofort als der Bot senden — schick ein Token nie über Chat, als Bildschirmfoto oder per unverschlüsselter E-Mail. Bewahr es nur auf deinem eigenen Server oder in deinem Geheimnisspeicher auf. Kein Korat-Personal fragt dich je nach einem Token, weder per Chat noch per E-Mail oder Telefon — bittet dich jemand darum und gibt sich als Korat-Team aus, geh von Betrug aus und melde es sofort über Kontakt. Ist ein Token abhandengekommen, widerruf es sofort in der App (das lässt sich nicht rückgängig machen — du musst ein neues ausstellen).

Beispiel — die erste Nachricht senden

Es gibt einen einzigen Endpunkt, POST https://koratland.com/api/bot/send. Leg das Token in einen Header und sag, was in welchen Raum geschrieben werden soll:

Mit curl (für alle, die mit einem Terminal umgehen)

curl -X POST "https://koratland.com/api/bot/send" \
  -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "Eine neue Bestellung ist eingegangen." }'

Ersetz kbot_12_xxxxxxxxxxxxxxxxxxxx durch das echte Token des Bots und 9931 durch die Kennung des Raums, in den du den Bot eingeladen hast (die Raumkennung siehst du am Link des Chats in der App oder in der Raumliste im Bildschirm „Meine Bots“).

Ohne Programmieren (Google Apps Script)

Hast du keinen eigenen Server, ist Google Apps Script mit einem Google-Konto kostenlos — geh auf script.google.com → Neues Projekt → das hier einfügen → auf „Ausführen“ klicken. Beim ersten Lauf wird nach der Erlaubnis für Internetzugriff gefragt (die kannst du bedenkenlos geben — es ist dein eigenes Skript):

function sendKoratMessage() {
  var url = "https://koratland.com/api/bot/send";
  var payload = {
    kind: "chat",
    chat_id: 9931,               // durch die Kennung deines Raums ersetzen
    text: "Eine neue Bestellung ist eingegangen."  // durch deine Nachricht ersetzen
  };
  var options = {
    method: "post",
    contentType: "application/json",
    headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" },  // durch das Token deines Bots ersetzen
    payload: JSON.stringify(payload)
  };
  var res = UrlFetchApp.fetch(url, options);
  Logger.log(res.getContentText());  // Ergebnis im Ausführungsprotokoll ansehen
}

Klick einmal auf „Ausführen“, um es sofort zu testen. Damit es von selbst zündet — etwa bei jeder neuen Zeile in einer Google-Tabelle, in der du Bestellungen festhältst — nimm die Trigger von Apps Script und ruf diese Funktion bei onEdit oder im Zeittakt alle paar Minuten auf.

Derselbe Weg geht mit jedem Automatisierungswerkzeug, das schon eine Aktion „HTTP-Anfrage“ oder „Webhook“ hat, etwa n8n, Make oder Zapier — richte ein POST an dieselbe Adresse ein, mit dem Header X-Bot-Token und demselben JSON-Rumpf wie oben.

Häufige Probleme

Das siehst du (reason)Was es heißtWie du es behebst
not_invitedDer Bot wurde nie in diesen Raum eingeladen (oder er war einmal drin und wurde später entfernt).Lad den Bot über „Meine Bots“ oder bot_invite erneut in diesen Raum ein.
invite_staleWer den Bot in diesen Raum eingeladen hat, hat dort sein Recht verloren (die Gruppe verlassen, herabgestuft, das Recht zur Verwaltung des Teamraums verloren, oder der Raum wurde geschlossen) ⇒ der Bot verstummt von selbst — niemand hat ihn hinausgeworfen.Lass jemanden, der in diesem Raum derzeit noch das Recht hat, den Bot erneut einladen — das Recht wird immer neu aus der jüngsten einladenden Person gerechnet.
conversation_not_openDieser Raum ist das Kundenpostfach eines Betriebs, und die Kundschaft hat nie zuerst geschrieben — die Seite des Betriebs (samt Bot) kann das Gespräch nicht eröffnen.Warte, bis die Kundschaft zuerst schreibt, und lass den Bot danach antworten.
rate_limitedDer Bot sendet schneller als seine eingestellte Grenze (je Minute oder je Tag).Warte die in retry_after_sec genannte Zahl von Sekunden und versuch es erneut — brauchst du wirklich eine höhere Grenze, wende dich über Kontakt an das Team.
blockedDie Person in diesem Chat hat diesen Bot gesperrt.Auf deiner Seite gibt es nichts zu beheben — nimm nötigenfalls einen anderen Chat oder Bot.
Ein Bot, der bisher schrieb, verstummt plötzlichDie häufigste Ursache ist invite_stale oben — selten liegt es am Bot selbst.Prüf, ob die Person, die den Bot eingeladen hat, noch im Raum ist und noch das Recht hat.

Die vollständige Tabelle der Fehlercodes steht unten in der technischen Referenz.

Technische Referenz (für Entwickler)

Alles ab hier richtet sich an alle, die eine eigene Anbindung an die Bot-API bauen — jeder Parameter, die vollständige Fehlertabelle und der eingehende Webhook.

Die Schritte 1–3 direkt per RPC (ohne App)

Hat deine App-Fassung den Bildschirm „Meine Bots“ noch nicht, oder willst du das automatisieren, rufst du die Supabase-RPCs direkt auf, angemeldet mit dem Zugangstoken der eingeloggten Person (nicht mit einem Bot-Token), über PostgREST — drei Schritte:

1. Einen Bot anlegen — bot_create

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_create" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "p_name": "Notify bot",
    "p_username": "notifybot",
    "p_business_id": null,
    "p_about": "Meldungen zum Bestellstatus"
  }'

p_username muss auf a–z 0–9 _ passen, 4–31 Zeichen lang sein und auf bot enden (Regex ^[a-z][a-z0-9_]{2,29}bot$). Übergib p_business_id, damit es ein Bot eines Betriebs wird — die aufrufende Person muss Mitglied dieses Betriebs sein und die Fähigkeit bot.manage haben. Bei Erfolg ist token in der Antwort das einzige Mal, dass der echte Wert je zurückkommt; die Datenbank behält nur dessen sha256-Hash. Geht er verloren, musst du tauschen (bot_rotate_token) — nachlesen lässt er sich nicht.

{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }

2. Den Bot in einen Chat einladen — bot_invite

Die aufrufende Person muss wirklich in diesem Chat sein (oder als Personal des Betriebs zum Kundenpostfach dieses Betriebs gehören) und diesen Bot verwalten dürfen (can_manage_bot). p_target ist chats.id.

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_invite" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "p_bot": 12, "p_kind": "chat", "p_target": 9931 }'

2a. Den Bot in einen Gruppen- oder Teamraum einladen — eine strengere Hürde als ein Chat

p_kind: "community_room" oder "business_room" mit p_target auf eine community_rooms.id bzw. business_rooms.id funktioniert heute wirklich — aber die einladende Person muss eine Rechteprüfung bestehen, die dem entspricht, was dieser Raum wirklich bedeutet, nicht bloß Mitgliedschaft:

  • Gruppenraum — die einladende Person muss nach den Regeln des Raums (read_role/post_role) dort wirklich schreiben dürfen. In einem gewöhnlichen Chatraum darf jedes Mitglied einladen. In einem Ankündigungs- oder Verwaltungsraum, der auf Admins beschränkt ist, nur die Inhaberschaft oder Admins der Gemeinschaft.
  • Teamraum eines Betriebs — die einladende Person muss die bestehenden Hürden des Raums nehmen (Lese- und Schreibfähigkeit, Zielgruppenregel) und immer zusätzlich die Fähigkeit teamroom.manage haben — einen Bot in den internen Raum eines Unternehmens zu holen gilt als „den Raum verwalten“, nicht bloß als „darin schreiben“.

In beiden Fällen muss die aufrufende Person zudem immer diesen Bot verwalten dürfen (can_manage_bot) — ein gemeinsamer Raum ist kein Ort, an dem beliebige Leute fremde Bots zum Reden hineinziehen.

🔴 invite_stale — das Wichtigste, bevor du das nutzt

Das Recht der einladenden Person wird nicht nur beim Einladen geprüftbot_send prüft die Regeln des Raums bei jedem einzelnen Senden erneut, im Namen der ursprünglich einladenden Person. Verlässt diese Person die Gruppe, wird sie herabgestuft, verliert sie teamroom.manage oder wird der Raum geschlossen, bekommt die allernächste Nachricht des Bots sofort reason: "invite_stale" — der Bot verstummt von selbst, ohne dass ihn jemand hinausgeworfen hätte. Kein Hintergrundlauf, kein Trigger, der zünden müsste — es ist reine Neuberechnung der Rechte.

Eine Nachricht senden — POST /api/bot/send

Zwei Wege, das Token mitzugeben — nimm einen:

# Weg 1 — Authorization: Bearer
curl -X POST "https://koratland.com/api/bot/send" \
  -H "Authorization: Bearer kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-9931-shipped" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "Deine Bestellung ist unterwegs." }'
# Weg 2 — X-Bot-Token
curl -X POST "https://koratland.com/api/bot/send" \
  -H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "chat", "chat_id": 9931, "text": "Deine Bestellung ist unterwegs." }'

Ganz ohne Token gibt es eine 401:

{ "ok": false, "reason": "bad_token", "detail": "ส่งโทเคนใน Authorization: Bearer … หรือ X-Bot-Token เท่านั้น" }

(Die Fehlertexte des Workers sind heute thailändisch, ganz gleich, von welcher Sprachfassung der Dokumentation du kommst — verzweige im Programm über reason und den HTTP-Status, nicht über detail.)

Felder im Rumpf

FeldTypPflichtBedeutung
kindstringNein (Vorgabe "chat")Entscheidet über die Art des Ziels. Drei echte Werte: "chat" · "community_room" · "business_room". Alles andere bekommt reason: "bad_target" direkt aus der Datenbank (es gibt kein stilles Zurückfallen auf "chat" — das war ein echter Fehler und ist behoben).
chat_idinteger > 0Ja (oder target_id)Die Kennung des Raums, in den der Bot bereits eingeladen wurde — chats.id bei kind: "chat", community_rooms.id bei "community_room", business_rooms.id bei "business_room" (das Feld im Rumpf heißt unabhängig von kind immer chat_id/target_id).
textstringJaDer Nachrichtentext. Darf nicht leer sein. Die Länge begrenzt der Registrierungseintrag text_max_message (ein lebender, anpassbarer Wert, keine fest verdrahtete Zahl — siehe das Feld max bei einem Fehler too_long).
client_keystringNeinSchlüssel gegen Doppelsendung, Alternative zum Header Idempotency-Key. 8–128 Zeichen, nur A–Z a–z 0–9 _ . : -.

Privater Chat gegenüber Betriebschat — welches Feld entscheidet

kind entscheidet über die Art der Zieltabelle (Chat / Gruppenraum / Teamraum eines Betriebs). Ein privater Chat zu zweit und das Kundenpostfach eines Betriebs unterscheiden sich überhaupt nicht über kind — beide sind Zeilen derselben Tabelle chats, und der Unterschied liegt darin, ob chats.business_id gesetzt ist (gesetzt = Postfach eines Betriebs). Diese Spalte lesen auch die Rechteprüfung beim Einladen, die Sperrprüfung und die Regel, dass ein Betrieb der Kundschaft nicht zuerst schreiben darf (conversation_not_open). Die übergebene chat_id muss also schon der richtige Raum sein — die API selbst kennt keinen eigenen Parameter für „privat oder Betrieb“.

Idempotency-Key und Sendegrenzen

Schick entweder den Standard-Header Idempotency-Key oder ein Feld client_key im Rumpf — nimm eines; schickst du beides, müssen sie übereinstimmen, sonst bekommst du 422 idempotency_key_conflict. Schickst du dieselbe Anfrage mit demselben Schlüssel erneut (gleicher Chat, gleicher Schlüssel), kommt die ursprüngliche 200-Antwort mit "duplicate": true zurück — es entsteht keine neue Nachricht.

Zwei Schichten von Grenzen: je IP (120 Anfragen pro Minute, vom Worker selbst durchgesetzt) und je Bot (voreingestellt 20 pro Minute und 1000 pro Tag, je Bot, von einer Verwaltung anpassbar). Beide antworten mit 429 und einem Header Retry-After in Sekunden (je IP immer 60 · je Bot und Minute 60 · je Bot und Tag 3600).

Nachrichten empfangen — Webhook

Schreibt jemand in einen Chat, in den der Bot schon eingeladen wurde (und ist die sendende Person nicht das Konto des Bots selbst — kein Echo auf sich selbst), schickt Korat das per POST an die von dir eingerichtete Adresse. Setz sie per RPC, genauso wie du ein Token holst — mit der Sitzung einer angemeldeten Person, die can_manage_bot für diesen Bot hat:

curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_set_webhook" \
  -H "apikey: <SUPABASE_ANON_KEY>" \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "p_bot": 12, "p_url": "https://dein-server.example.com/korat-webhook" }'
{ "ok": true, "secret": "a1b2c3…", "note": "you see this secret once — keep it to verify signatures yourself" }

Die Adresse muss https:// sein und darf kein interner, Loopback- oder Link-Local-Host sein (localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 — das deckt den Metadaten-Endpunkt jedes Cloud-Anbieters ab) — abgelehnt schon beim Setzen und vor jeder einzelnen Zustellung mit einer echten DNS-Auflösung erneut geprüft, falls DNS später woanders hinzeigt. secret erscheint nur einmal, beim Setzen oder Tauschen — bot_rotate_webhook_secret(p_bot) tauscht es jederzeit, bot_disable_webhook(p_bot) und bot_enable_webhook(p_bot) schalten es von Hand aus und ein, und bot_webhook_status(p_bot) liest den Zustand (nie das Geheimnis).

In der Produktion wird noch nichts zugestellt

Die Zustellwarteschlange (bot_webhook_deliveries) arbeitet richtig und ist in der Datenbank bewiesen, und das Geheimnis der Route (BOT_WEBHOOK_DISPATCH_SECRET) hat die Inhaberschaft gesetzt, aber der Teil, der die Warteschlange wirklich leert (pg_cron, das den Worker jede Minute aufruft), ist in der Produktion noch nicht eingeplant — du kannst einen Webhook setzen, und Nachrichten stauen sich an, doch dein Endpunkt bekommt nichts, bis der pg_cron-Takt eingeplant ist.

Was dein Endpunkt bekommt

POST /korat-webhook HTTP/1.1
Content-Type: application/json
X-Korat-Signature: sha256=<hex>
X-Korat-Timestamp: 1750000000000

{
  "event": "message.created",
  "chat_id": 9931,
  "message_id": 88213,
  "text": "Gibt es das auch in Rot?",
  "time": "14:02",
  "sender": { "id": 4821, "username": "jamie_reed", "name": "Jamie Reed" }
}

Das ist der gesamte Inhalt — keine Telefonnummer, keine E-Mail-Adresse und niemandes Token sind je enthalten. Der Bot sieht von Raum, Nachricht und sendender Person genau so viel, wie er zum Antworten braucht.

Prüf die Signatur, bevor du der Anfrage traust

X-Korat-Signature ist HMAC-SHA256(secret, "<timestamp>." + rawBody), hexadezimal kodiert. Node.js:

const crypto = require("crypto");
function verify(rawBody, signatureHeader, timestampHeader, secret) {
  const age = Date.now() - Number(timestampHeader);
  if (!(age >= 0 && age < 5 * 60_000)) return false; // Wiedereinspielungen älter als 5 Minuten ablehnen
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestampHeader}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Antworte innerhalb von 8 Sekunden mit 2xx, damit es als zugestellt gilt. Alles andere — auch eine Weiterleitung 3xx, der Korat nie folgt — oder gar keine Antwort gilt als Fehlschlag und geht zurück in die Warteschlange.

Neue Versuche, wenn dein Endpunkt ausfällt

Ein Fehlschlag wartet exponentiell länger (1, 2, 4, 8, 16, 32, 64, 128 Minuten), gedeckelt auf 8 Versuche je Nachricht; danach wird diese eine Nachricht aufgegeben. Fällt der Endpunkt 15-mal hintereinander aus (über Nachrichten hinweg), wird der Webhook automatisch abgeschaltet und die Inhaberschaft des Bots in der App benachrichtigt — er feuert nie ewig gegen einen toten Endpunkt. Ruf bot_enable_webhook auf, um ihn wieder einzuschalten, sobald das Ziel repariert ist.

Vollständige Tabelle der Fehlercodes

Jede Fehlerantwort hat {"ok": false, "reason": "…"} — verzweige über reason, nicht nur über den HTTP-Status.

HTTPreasonQuelleBedeutung / Abhilfe
401bad_tokenWorker / DBKein Token geschickt, oder das Token ist falsch, widerrufen oder der Bot ist abgeschaltet — bewusst eine einzige Antwort für all das, damit Rateversuche kein Signal bekommen.
403not_invitedDBDer Bot wurde nie in diesen Raum eingeladen oder wurde entfernt. Ruf zuerst bot_invite auf.
403blockedDBDie Person im Raum hat diesen Bot gesperrt (nur in Räumen, die kein Kundenpostfach sind).
403account_suspendedDBEine Seite des Chats ist gesperrt — eine Rechtefrage, keine fehlerhafte Anfrage.
404no_such_chatDBchat_id gibt es nicht.
404no_such_endpointWorkerFalscher Pfad — es gibt nur POST /api/bot/send.
405method_not_allowedWorkerEine andere Methode als POST benutzt.
409conversation_not_openDBDas ist ein Kundenpostfach, und die Kundschaft hat nie zuerst geschrieben — die Seite des Betriebs (samt Bot) kann das Gespräch nicht eröffnen.
409chat_closedDBEin Dating-Match-Raum, der für neue Nachrichten bereits geschlossen ist.
422empty_textDBtext ist nach dem Trimmen leer.
422too_longDBDie Nachricht überschreitet die Längengrenze — siehe den beigefügten Wert max.
422bad_targetDBkind ist keines von "chat"/"community_room"/"business_room".
404no_such_roomDBchat_id zeigt auf keinen echten Gruppen- oder Teamraum (der Raum wurde gelöscht).
409room_archivedDBDer Raum ist geschlossen — niemand kann dort schreiben, Mensch wie Bot.
403invite_staleDBDas Recht der einladenden Person wurde neu geprüft und trägt nicht mehr — sie hat die Gruppe verlassen, wurde herabgestuft, hat teamroom.manage verloren, oder der Raum wurde geschlossen. In detail steht der Untergrund (not_a_member/room_role/not_teamroom_manager) — behoben wird es, indem jemand mit noch gültigem Recht den Bot erneut in diesen Raum einlädt (bot_invite), nicht durch Warten.
422bad_jsonWorkerDer Anfragerumpf ist kein gültiges JSON.
422bad_chat_idWorkerchat_id/target_id muss eine positive ganze Zahl sein.
422bad_textWorkertext muss eine Zeichenkette sein.
422idempotency_key_conflictWorkerSowohl Idempotency-Key als auch client_key geschickt, und sie widersprechen sich — schick nur eines.
422bad_client_keyWorkerDer Schlüssel gegen Doppelsendung passt nicht zur erlaubten Form (8–128 Zeichen, A–Z a–z 0–9 _ . : -).
429rate_limitedWorker / DBÜber der Grenze — sieh in den Header Retry-After und die Felder window/retry_after_sec.
502database_errorWorkerDie Datenbank hat geantwortet, aber nicht mit 2xx — siehe die beigefügten pg_code/detail, die Meldung von Postgres selbst.
503service_key_not_configuredWorkerAuf dem Server ist kein Dienstschlüssel eingerichtet — ein Problem auf Korat-Seite, nicht bei der aufrufenden Seite.
504database_unreachableWorkerDie Datenbank war nicht erreichbar oder hat das Zeitlimit überschritten. Prüf safe_to_retry: ein neuer Versuch ist nur sicher, wenn du einen client_key bzw. Idempotency-Key mitgeschickt hast.

Antwort bei Erfolg

{ "ok": true, "duplicate": false, "message_id": 88213 }

duplicate: true heißt, dass genau diese Anfrage schon einmal geschickt wurde (über client_key/Idempotency-Key erkannt) — es entstand keine neue Nachricht, es ist aber trotzdem ein Erfolg (200), weil das gewünschte Ergebnis bereits eingetreten ist.