Draht von edv.sg

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_…"
StatusBedeutung
qr_pendingKanal angelegt, wartet auf das Scannen des QR-Codes.
connectedVerknüpft und sendebereit.
disconnectedVerbindung 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.
bannedDie 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:

ZeitraumNachrichten pro TagPro Minute
Tag 1–2205
Tag 3–4508
Tag 5–712010
Tag 8–1430015
ab Tag 151'00020

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

HTTPerrorBedeutung
400invalid_requestEin Pflichtfeld fehlt oder eine Adresse ist keine https-Adresse.
400unsupported_typetype ist keiner von text, image, video, voice, document.
401no_keyDer Authorization-Kopf fehlt.
401invalid_keySchlüssel unbekannt, falsch oder gesperrt.
402subscription_requiredTestphase abgelaufen oder Zahlung offen.
403account_suspendedKonto gesperrt.
404channel_not_foundKanal existiert nicht oder gehört nicht zu deinem Konto.
409channel_not_connectedKanal ist qr_pending, disconnected oder banned.
400group_not_allowedEmpfänger ist eine Gruppe; mit allow_group freischalten.
409history_disabledFür diesen Kanal ist der Chatverlauf nicht eingeschaltet.
409history_needs_relinkChatverlauf ist eingetragen, aber die laufende Verknüpfung kennt ihn nicht — neu verknüpfen.
422invalid_recipientNummer ist ungültig oder nicht bei WhatsApp registriert.
429rate_limitedDurchsatzgrenze erreicht, siehe Retry-After.
451blocked_by_policyVersandmuster verstösst gegen die Nutzungsordnung.
503capacity_reachedAuf diesem Server ist kein weiterer Kanal mehr frei. Wir melden uns.
503qr_unavailableDer 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].