Dokumentation
Schnellstart. Basis-URL ist https://draht.edv.sg, alle Antworten sind
JSON, alle Zeitangaben UTC im ISO-8601-Format.
Anmeldung
Jede Anfrage trägt deinen API-Schlüssel im Authorization-Kopf.
Schlüssel beginnen mit draht_live_ (Produktion) oder
draht_test_ (Testkanal).
Authorization: Bearer draht_live_…
Der Schlüssel wird beim Anlegen einmal im Klartext angezeigt und danach nur noch als Prüfsumme gespeichert. Verloren heisst neu erzeugen.
Schlüssel erzeugst und sperrst du in deinem Konto — dort meldest du dich ohne Passwort an, mit einem Link per E-Mail. Alles, was hier beschrieben ist, geht auch von Hand über diese Seite.
1. Kanal anlegen
curl -X POST https://draht.edv.sg/v1/channels \
-H "Authorization: Bearer draht_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"Werkstatt St. Gallen"}'
{
"channel_id": "ch_8fa2c1d40b7e",
"name": "Werkstatt St. Gallen",
"status": "qr_pending",
"created_at": "2026-08-20T16:04:11Z"
}
2. Nummer verknüpfen
Der Kanal steht auf qr_pending. Hol den QR-Code ab und scanne ihn in
WhatsApp unter Einstellungen → Verknüpfte Geräte → Gerät verknüpfen.
# PNG (Vorgabe)
curl https://draht.edv.sg/v1/channels/ch_8fa2c1d40b7e/qr \
-H "Authorization: Bearer draht_live_…" > qr.png
# oder den rohen Verknüpfungs-Link, wenn du selbst zeichnen willst
curl "https://draht.edv.sg/v1/channels/ch_8fa2c1d40b7e/qr?format=raw" \
-H "Authorization: Bearer draht_live_…"
Der Code ist rund 60 Sekunden gültig und wird danach automatisch erneuert — hol
ihn bei Ablauf einfach neu ab. Auch ein Kanal, der lange ungescannt lag und auf
disconnected gefallen ist, kommt damit zurück: der erneute Abruf
startet die Sitzung neu und braucht dann ein paar Sekunden länger. Sobald das
Handy den Code gescannt hat, wechselt der Kanal auf connected.
3. Status abfragen
curl https://draht.edv.sg/v1/channels/ch_8fa2c1d40b7e \
-H "Authorization: Bearer draht_live_…"
| Status | Bedeutung |
|---|---|
qr_pending | Kanal angelegt, wartet auf das Scannen des QR-Codes. |
connected | Verknüpft und sendebereit. |
disconnected | Verbindung unterbrochen — meist, weil das Gerät zu lange offline war oder die Verknüpfung im Handy entfernt wurde. Eingehende Nachrichten kommen in dieser Zeit nicht an, und WhatsApp stellt sie später nicht nach; ausgehende bleiben in der Warteschlange. Wir schicken dir eine Mail, sobald das passiert — höchstens einmal pro Stunde und Kanal, damit eine flatternde Verbindung dein Postfach nicht füllt — und eine kurze Bestätigung, sobald der Kanal wieder verbunden ist. |
banned | Die Nummer ist mit hoher Wahrscheinlichkeit von WhatsApp gesperrt: drei Verknüpfungsversuche in Folge sind gescheitert, bei einer Nummer, die vorher verbunden war. Gezählt werden Versuche, nicht Abfragen — den Zustand nachzusehen zählt nie mit, so oft du willst. Der Kanal nimmt keine Aufträge mehr an. Wir melden uns in diesem Fall bei dir. (WhatsApp meldet eine Sperre nicht als eigenen Zustand — darum die Bedingung aus Wiederholung und Vorgeschichte statt einer Behauptung.) |
4. Nachricht senden
curl -X POST https://draht.edv.sg/v1/messages \
-H "Authorization: Bearer draht_live_…" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_8fa2c1d40b7e",
"to": "41791234567",
"type": "text",
"text": "Ihr Fahrzeug ist bereit zur Abholung."
}'
{
"message_id": "msg_2b91ee7c",
"status": "queued",
"queued_at": "2026-08-20T16:07:52Z"
}
Die Empfängernummer wird im internationalen Format ohne + und ohne
Leerzeichen erwartet: 41791234567.
Bilder, Dokumente, Sprachnachrichten
Neben text kennt Draht image, video,
voice und document. Die Datei kommt als
https-Adresse; wir laden sie beim Versand von dort.
curl -X POST https://draht.edv.sg/v1/messages \
-H "Authorization: Bearer draht_live_…" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_8fa2c1d40b7e",
"to": "41791234567",
"type": "document",
"media_url": "https://deine-seite.ch/rechnung-2026-001.pdf",
"filename": "Rechnung 2026-001.pdf",
"caption": "Ihre Rechnung"
}'
filename und caption sind freiwillig; ohne
filename nehmen wir den letzten Teil der Adresse. Eine
Sprachnachricht braucht Opus in einem Ogg-Behälter
(.ogg) — alles andere kommt beim Empfänger als Datei an, nicht als
Sprachnachricht. Für voice gibt es keine Bildunterschrift.
Wir nehmen keine Dateien entgegen und lagern keine. Es gibt bewusst keinen Upload und kein base64: Draht speichert keine Inhalte, und eine hochgeladene Datei wäre genau das. Die Adresse muss zum Sendezeitpunkt erreichbar sein — ein signierter Link mit kurzer Gültigkeit reicht.
Nutzung abfragen
Wo steht der Kanal heute im Tagesbudget? Das musst du nicht aus einer
429-Antwort erraten:
curl https://draht.edv.sg/v1/channels/ch_8fa2c1d40b7e/usage \
-H "Authorization: Bearer draht_live_…"
{
"today": { "sent": 42, "counted": 18.5, "received": 31,
"new_contacts": 4, "remaining": 281.5 },
"queued": 2,
"limits": { "per_day": 300, "per_minute": 15, "min_gap_seconds": 12,
"warmup_day": 9, "warmup_complete": false },
"history": [ … ]
}
sent ist die Zahl der Nachrichten, counted die
gewichtete Summe — an der hängt die Bremse. Antworten in einem Chat, den
die Gegenseite eröffnet hat, zählen nur zu einem Viertel.
5. Webhooks empfangen
Eingehende Nachrichten und Statuswechsel schickt Draht als POST an
deine URL. Jede Zustellung ist signiert.
POST /dein/endpunkt
X-Draht-Event: message.received
X-Draht-Timestamp: 1755705*** (Unix-Sekunden)
X-Draht-Signature: sha256=4f2b…
{
"event": "message.received",
"channel_id": "ch_8fa2c1d40b7e",
"from": "41791234567",
"type": "text",
"text": "Passt, ich komme morgen vorbei.",
"message_id": "3EB0C767D0…",
"media_url": null,
"filename": null,
"received_at": "2026-08-20T16:12:03Z"
}
message_id ist die Kennung aus WhatsApp. Nutze sie zur
Doppelerkennung: bleibt deine Antwort aus, stellt Draht
dieselbe Nachricht bis zu fünfmal erneut zu.
Bilder, Sprachnachrichten, Dokumente
Vorgabe ist aus. Normalerweise siehst du bei einer Datei nur
"type": "image" und "text": null — Draht speichert
keine Inhalte, und dazu gehören Dateien.
type nimmt einen dieser Werte an:
text, image, video,
ptt (Sprachnachricht), audio (Tondatei, die keine
Sprachnachricht ist), document, sticker,
contact, location, poll,
album, group_invite, interactive,
reaction. Kennt Draht eine Art nicht, meldet es
text.
Bis 0.11.0 stand hier immer "text", auch bei
einem Bild oder einem PDF. Wer darauf ausgewertet hat, bekommt ab 0.11.1
die richtige Art — das ist eine Verhaltensänderung, wenn auch die Behebung
eines Fehlers.
Auf Wunsch schalten wir die Durchreichung für einen Kanal ein. Dann trägt der
Webhook zusätzlich media_url und filename, und unter
dieser Adresse holst du die Datei mit deinem API-Schlüssel ab:
GET /v1/messages/msg_1a2b/medien
Authorization: Bearer draht_live_…
→ 200, die Datei im ursprünglichen Format
Hol sie sofort ab. Draht legt die Datei nicht ab, sondern
reicht sie in dem Moment aus dem Kanal durch. Nach 24 Stunden
ist sie fort und die Adresse antwortet 410 media_gone. Wer ein
Archiv will, holt beim Eintreffen des Webhooks ab, nicht beim Anschauen.
Chatverlauf
Vorgabe ist aus, und zwar nicht als Bequemlichkeit: die
Engine hält von sich aus nichts vor. Ohne diesen Schalter ist der
Verlauf nicht bloss leer, er ist gar nicht abfragbar — dieselbe Antwort
kommt auch für die Kontaktliste und für die Auflösung einer
@lid auf eine Telefonnummer.
Eingeschaltet legt der Kanal den Chatverlauf in seinem Sitzungsband ab.
Das ist der eine Punkt, an dem die Zusage «Draht speichert keine Inhalte»
für einen Kanal aufgehoben wird, darum: nur auf Wunsch, je Kanal,
und nie im Vorbeigehen. Am Kanal steht unter
chat_history.requested, ob er gewünscht ist — gewünscht,
nicht wirksam: dazwischen liegt ein QR-Scan.
Beim Anlegen mitgeben, dann sitzt er von Anfang an richtig:
POST /v1/channels {"name":"…","chat_history":true}, optional
dazu "chat_history_full_sync":true für rund ein Jahr statt drei
Monate.
Der Schalter muss vor dem QR-Scan sitzen. Die Einstellung gehört zur Sitzung; an einer bereits verknüpften Nummer lässt sie sich nicht mehr gefahrlos ändern. Nachträglich einschalten heisst deshalb: Verknüpfung verwerfen und neu scannen. Die Aufwärmkurve fängt dabei nicht von vorn an. Reichweite ohne Vollabgleich rund drei Monate, mit rund ein Jahr.
GET /v1/channels/ch_1a2b/chats?limit=100
Authorization: Bearer draht_live_…
→ 200 {"channel_id":"ch_1a2b","count":37,"chats":[…]}
→ 409 history_disabled, wenn der Kanal ihn nicht hat
→ 409 history_needs_relink, wenn er eingetragen ist, aber die
laufende Verknüpfung ihn nicht kennt — dann neu verknüpfen
GET /v1/channels/ch_1a2b/chats/[email protected]/messages?limit=100
GET /v1/channels/ch_1a2b/lids/272790636785785@lid
→ 200 {"lid":"272790636785785@lid","phone_number":"41791234567"}
→ phone_number: null, wenn die Engine keine Zuordnung hat
Kanal umbenennen
Nur der Name. Er ist keine Zierde: er steht in der Oberfläche und in jeder Betriebsmeldung, die wir dir schicken. Nummer, Chatverlauf und die Aufwärmphase bleiben davon unberührt.
PATCH /v1/channels/ch_1a2b
Authorization: Bearer draht_live_…
Content-Type: application/json
{"name": "Werkstatt"}
→ 200 der Kanal, wie bei GET /v1/channels
→ 400 invalid_request, wenn "name" fehlt oder über 60 Zeichen hat
Übersicht: Stand je Chat
Je Chat in einem Aufruf: wer ist dran, seit wann, steht eine Frage offen,
was war das Letzte. Die Bewertung ist kanalneutral — sie
liest nur Absender und Zeit, keine Wortlisten. Damit gilt sie für einen
Handwerksbetrieb und einen Verein gleich. Braucht den eingeschalteten
Chatverlauf. Gruppen sind dabei; gruppen=0 lässt sie weg,
auftragsstand=1 legt den Leitstand (unten) je Chat dazu.
GET /v1/channels/ch_1a2b/uebersicht?limit=40
Authorization: Bearer draht_live_…
→ 200 {"channel_id":"ch_1a2b","channel_name":"…","phone_number":"41791234567",
"count":24,"offen":3, // "offen" = wie viele auf dich warten
"chats":[{
"chat_id":"[email protected]","name":"…","gruppe":false,
"stand":{
"dran":"du", // du | gegenseite | niemand
"unbeantwortet":2, // Serie am Ende, von derselben Seite
"wartet_seit":"2026-09-07T08:12:00Z", // die ERSTE unbeantwortete
"wartet_tage":2,"frage_offen":true,"ruht":false,
"nachrichten":34,"von_mir":22,"von_gegenseite":12,
"letzte":{"von_mir":false,"text":"…","wann":"…","typ":null},
"satz":"Du bist dran: 2 Nachrichten ohne Antwort, seit 2 Tagen. …"
}}]}
Sortiert nach Dringlichkeit: offene Frage an dich, dann stille Bringschuld,
dann Warten auf die Gegenseite, ruhende Chats zuletzt. Ein einzelner Chat,
dessen Verlauf die Engine nicht hergibt, kommt mit stand: null
und error — er kippt die Übersicht nicht.
wartet_seit ist die erste unbeantwortete Nachricht,
nicht die letzte: wer dreimal nachfragt, wartet seit der ersten Frage.
Die Oberfläche dazu ist /chats.html, hinter der Anmeldung ohne Passwort und nach Nummer getrennt. Von dort geht nichts automatisch raus — jede Nachricht braucht einen Klick.
Leitstand: Auftragsstand je Chat
Dasselbe je Chat, aber mit der Brille eines Handwerksauftrags: welche Angaben für einen Termin fehlen (Adresse, Stockwerk, Lift …), ob es ein grosser Auftrag ist, was als Nächstes zu tun ist. Braucht den eingeschalteten Chatverlauf. Gruppen sind ausgenommen — dort kommen keine Aufträge herein. Für einen Kanal, über den keine Aufträge laufen, ist die Übersicht oben die richtige Sicht: die Wortlisten hier würden dort Dinge behaupten, die nicht gemeint sind.
GET /v1/channels/ch_1a2b/leitstand?limit=30
Authorization: Bearer draht_live_…
→ 200 {"channel_id":"ch_1a2b","count":12,"chats":[{
"chat_id":"[email protected]","name":"…","phone_number":"41791234567",
"stand":{
"felder":{"was":true,"adresse":true,"stockwerk":false,…},
"fehlt":["stockwerk","lift"], // ohne diese kein Termin
"gross":false,"gross_signale":[], // gross → du übernimmst
"wartet_auf_kunden":false,
"naechster_schritt":{"art":"nachfragen","text":"Es fehlt noch: …"}
}}]}
naechster_schritt.art ist einer von fünf Werten, nach
Dringlichkeit sortiert zurückgegeben: uebernehmen (grosser
Auftrag), nachfragen, terminieren,
warten (deine letzten zwei Nachrichten sind unbeantwortet),
ignorieren. Die Bewertung ist regelbasiert und damit für
denselben Verlauf immer gleich — sie kommt nicht von einem Modell.
Eine eigene Oberfläche hat der Leitstand nicht mehr;
/leitstand.html leitet auf /chats.html
um. Dort ist er über auftragsstand=1 erreichbar.
An eine @lid antworten
Ein eingehender Webhook nennt die Gegenstelle zweimal: from
trägt nur die Ziffern, from_jid die vollständige Kennung.
Antworte mit from_jid. WhatsApp adressiert
neuere Gegenstellen als <zahl>@lid; gibst du nur die
Ziffern zurück, wird daraus <zahl>@c.us — eine Nummer,
die es nicht gibt, und die Nachricht ist unzustellbar. Enthält
to ein @, bleibt die Art unangetastet.
Sprachnachrichten als Text
Vorgabe ist aus. Eingeschaltet wandelt Draht eine eingehende Sprachnachricht in Text, sobald sie ankommt — nicht auf Abruf, und das hat einen zwingenden Grund: die Audiodatei lebt nur 24 Stunden im Kanal. Wer zwei Tage später transkribieren will, findet nichts mehr.
Der Text steht danach im Webhook message.transcribed und im
Verlauf unter transcript. Setzt die Medien-Durchreichung
voraus — ohne sie lädt der Kanal die Datei gar nicht erst herunter. Am
Kanal steht beides unter voice_transcription:
requested ist der Schalter, effective sagt, ob er
tatsächlich wirkt.
Zwei Dinge, die dazugehören: ein Transkript ist Nachrichteninhalt und wird gespeichert — das ist dieselbe Ausnahme wie beim Chatverlauf. Und die Audiodatei verlässt für die Umwandlung den Server. Wer das für einen Kanal einschaltet, braucht es in seiner Auftragsbearbeitung stehen.
// Webhook, sobald die Umwandlung fertig ist
{"event":"message.transcribed",
"data":{"channel_id":"ch_1a2b","message_id":"msg_1a2b",
"text":"Hoi Jonas, ich bin am Samstag um 10 Uhr dabei…",
"duration_seconds":28}}
Doppelt senden verhindern
Nach einem Zeitüberschreiten weisst du nicht, ob Draht die Nachricht
angenommen hat. Wiederholst du sie, wird sie zweimal zugestellt. Gib darum
einen idempotency_key mit — 8 bis 200 Zeichen, frei gewählt.
Kommt derselbe Schlüssel am selben Kanal erneut, antwortet Draht mit
200 und idempotent_replay: true und stellt
nichts erneut zu.
Der Schlüssel muss stabil sein, sonst hilft er nicht: bilde ihn aus Empfänger, Text und einem Zeitfenster, nicht zufällig. Ein Wiederholungsversuch braucht denselben Schlüssel wie der erste Versuch.
POST /v1/messages
{"channel_id":"ch_1a2b","to":"41791234567","text":"…",
"idempotency_key":"abgabe-2026-09-04-kunde-17"}
→ 202 {"message_id":"msg_1a2b","status":"queued"}
→ 200 {"message_id":"msg_1a2b","status":"queued",
"idempotent_replay":true} beim zweiten Mal
Gruppen
Senden in Gruppen ist gesperrt. Eine Nachricht dort sehen
alle Mitglieder, und eine Verwechslung ist nicht zurückzunehmen — ein an den
falschen Chat gerichteter Preis steht vor zwanzig Leuten. Eine
Gruppen-Kennung (…@g.us) gibt 400 group_not_allowed.
Wer es braucht, setzt "allow_group": true je Aufruf.
Signatur prüfen
Die Signatur ist ein HMAC-SHA256 über <timestamp>.<roher Body>
mit deinem Webhook-Geheimnis. Prüfe sie vor dem Verarbeiten und
vergleiche zeitkonstant. Verwirf Zustellungen, deren Zeitstempel mehr als fünf
Minuten abweicht — das schliesst Wiedereinspielungen aus.
// Node
import crypto from "node:crypto";
function pruefen(rohBody, kopf, zeitstempel, geheimnis) {
const erwartet = "sha256=" + crypto
.createHmac("sha256", geheimnis)
.update(zeitstempel + "." + rohBody)
.digest("hex");
const a = Buffer.from(kopf), b = Buffer.from(erwartet);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Antworte innerhalb von zehn Sekunden mit 2xx. Andernfalls wiederholen
wir die Zustellung mit wachsendem Abstand — nach 1 min, 5 min, 30 min, 2 h und 6 h.
Danach gilt das Ereignis als unzustellbar und wird im Kundenkonto vermerkt.
Durchsatzgrenzen
Wird eine Grenze erreicht, antwortet die API mit 429 und einem
Retry-After-Kopf in Sekunden. Die Nachricht wurde dann
nicht angenommen — schick sie nach Ablauf erneut.
HTTP/1.1 429 Too Many Requests
Retry-After: 34
X-Draht-Limit-Reset: 2026-08-20T16:15:00Z
{
"error": "rate_limited",
"message": "Tagesgrenze der Aufwärmphase erreicht (Tag 3 von 14).",
"retry_after": 34
}
Aufwärmkurve
Eine frisch verknüpfte Nummer, die sofort hunderte Nachrichten verschickt, ist das auffälligste Muster überhaupt. Neue Kanäle starten deshalb gedrosselt und steigern sich automatisch. Das ist die Standardkurve:
| Zeitraum | Nachrichten pro Tag | Pro Minute |
|---|---|---|
| Tag 1–2 | 20 | 5 |
| Tag 3–4 | 50 | 8 |
| Tag 5–7 | 120 | 10 |
| Tag 8–14 | 300 | 15 |
| ab Tag 15 | 1'000 | 20 |
Antworten auf eingehende Nachrichten zählen milder als Erstkontakte — ein Chat, den die Gegenseite begonnen hat, ist unverdächtig. Braucht dein Anwendungsfall dauerhaft mehr, sprich mit uns: Wir heben die Grenze an, wenn das Versandmuster es trägt. Nicht angehoben wird sie für Werbung an Empfängerlisten — siehe Nutzungsordnung.
Fehlercodes
| HTTP | error | Bedeutung |
|---|---|---|
| 400 | invalid_request | Ein Pflichtfeld fehlt oder eine Adresse ist keine https-Adresse. |
| 400 | unsupported_type | type ist keiner von text, image, video, voice, document. |
| 401 | no_key | Der Authorization-Kopf fehlt. |
| 401 | invalid_key | Schlüssel unbekannt, falsch oder gesperrt. |
| 402 | subscription_required | Testphase abgelaufen oder Zahlung offen. |
| 403 | account_suspended | Konto gesperrt. |
| 404 | channel_not_found | Kanal existiert nicht oder gehört nicht zu deinem Konto. |
| 409 | channel_not_connected | Kanal ist qr_pending, disconnected oder banned. |
| 400 | group_not_allowed | Empfänger ist eine Gruppe; mit allow_group freischalten. |
| 409 | history_disabled | Für diesen Kanal ist der Chatverlauf nicht eingeschaltet. |
| 409 | history_needs_relink | Chatverlauf ist eingetragen, aber die laufende Verknüpfung kennt ihn nicht — neu verknüpfen. |
| 422 | invalid_recipient | Nummer ist ungültig oder nicht bei WhatsApp registriert. |
| 429 | rate_limited | Durchsatzgrenze erreicht, siehe Retry-After. |
| 451 | blocked_by_policy | Versandmuster verstösst gegen die Nutzungsordnung. |
| 503 | capacity_reached | Auf diesem Server ist kein weiterer Kanal mehr frei. Wir melden uns. |
| 503 | qr_unavailable | Der QR-Code ist noch nicht bereit — in ein paar Sekunden erneut abrufen. |
Im Aufbau. Diese Dokumentation beschreibt die Schnittstelle, wie sie gebaut wird. Bis zur Freigabe können sich Details ändern; Änderungen kündigen wir an. Fragen an [email protected].