# Samy – Dokumentation > Samy ist eine Handy-App (iOS und Android), die Nachrichten aus selbst gebauter Software als Push aufs Handy bringt. Ein Tool schickt mit einem HTTP-Aufruf eine Nachricht an Samy. Nachrichten können Knöpfe haben, deren Tipp per Webhook an das Tool zurückgemeldet wird. API-Adresse: https://api-production-e537a.up.railway.app. Die Verbindung zu einem Tool heißt „Packlet“ und hat einen eigenen Schlüssel. --- ## Überblick Was Samy macht und wie dein Tool, Samy und dein Handy zusammenspielen. Samy bringt Software, die du selbst gebaut hast, aufs Handy. Dein Tool schickt eine Nachricht, Samy zeigt sie als Push, und über Knöpfe in der Nachricht bedienst du dein Tool von unterwegs. ### Die drei Bausteine - **Packlet:** die Verbindung zwischen einem deiner Tools und Samy. Jedes Packlet hat einen eigenen Kanal in der App und einen eigenen Schlüssel. - **Nachricht:** Dein Tool schickt sie mit einem einzigen HTTP-Aufruf. Samy speichert sie, zeigt sie im Kanal und schickt einen Push. - **Knopf:** Eine Nachricht kann bis zu drei Knöpfe haben. Ein Knopf öffnet einen Link oder meldet den Tipp per Webhook an dein Tool zurück. Dein Code läuft dabei weiter bei dir: auf dem Rechner, einem Server, einem Raspberry Pi oder als Cronjob. ### Wo du weiterliest | Du willst … | Seite | |---|---| | die erste Nachricht schicken | [Schnellstart](https://samy.sinio.io/docs/schnellstart.md) | | deinen Coding-Assistenten die Anbindung schreiben lassen | [Für KI-Assistenten](https://samy.sinio.io/docs/ki.md) | | alle Felder einer Nachricht kennen | [Nachricht senden](https://samy.sinio.io/docs/senden.md) | | dein Tool vom Handy aus bedienen | [Knöpfe](https://samy.sinio.io/docs/knoepfe.md) und [Webhook](https://samy.sinio.io/docs/webhook.md) | | einen Fehler verstehen | [Antworten und Fehler](https://samy.sinio.io/docs/antworten.md) | ### Kosten Die App, Push-Nachrichten und Knöpfe sind kostenlos. --- ## Schnellstart In drei Schritten zur ersten Nachricht auf dem Handy. ### 1. Packlet anlegen In der App auf **Packlet verbinden** tippen, Name und Avatar wählen. Die Webhook-URL ist optional und lässt sich später nachtragen. ### 2. Schlüssel ablegen Samy zeigt den Schlüssel **einmal** an. Lege ihn als Umgebungsvariable ab, nicht im Code: ```bash export SAMY_KEY="samy_…" ``` ### 3. Erste Nachricht schicken ```bash curl https://api-production-e537a.up.railway.app/v1/push \ -H "Authorization: Bearer $SAMY_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Hallo","text":"Mein Tool ist verbunden"}' ``` Die App wartet live auf diesen Aufruf und zeigt „Verbunden!“, sobald er ankommt. Die Antwort ist `{"message_id": "…"}`. ### Wie es weitergeht - Den Aufruf in dein Tool einbauen: [Beispiele](https://samy.sinio.io/docs/beispiele.md) für Python und Node.js. - Den Aufruf von einer KI einbauen lassen: [Für KI-Assistenten](https://samy.sinio.io/docs/ki.md). - Knöpfe ergänzen, um dein Tool vom Handy zu bedienen: [Knöpfe](https://samy.sinio.io/docs/knoepfe.md). --- ## Für KI-Assistenten Die Doku als reiner Text, damit dein Coding-Assistent die Anbindung für dich schreibt. Die gesamte Dokumentation gibt es als reinen Text. So kann ein Coding-Assistent wie Claude Code oder Cursor sie lesen und die Anbindung für dich schreiben. ### Adressen für die KI | Adresse | Inhalt | |---|---| | [/llms-full.txt](https://samy.sinio.io/llms-full.txt) | Die komplette Dokumentation in einer Datei. Im Zweifel diese. | | [/llms.txt](https://samy.sinio.io/llms.txt) | Kurzes Inhaltsverzeichnis mit Links auf alle Seiten als Markdown. | | `/docs/.md` | Jede einzelne Seite als Markdown, zum Beispiel [/docs/senden.md](https://samy.sinio.io/docs/senden.md). | ### Prompt zum Kopieren Gib deinem Assistenten diesen Auftrag und ersetze den Teil in eckigen Klammern: ```text Lies zuerst die Samy-Dokumentation unter https://samy.sinio.io/llms-full.txt. Baue dann in dieses Projekt eine Anbindung an Samy ein: - Eine Funktion, die eine Push-Nachricht über POST https://api-production-e537a.up.railway.app/v1/push schickt. - Der Schlüssel kommt aus der Umgebungsvariable SAMY_KEY und steht nie im Code. - Fehler (400, 401, 429) werden sichtbar gemeldet und nicht verschluckt. - [Beschreibe hier, wann eine Nachricht kommen soll und welche Knöpfe sie hat.] Wenn Knöpfe ohne url vorkommen: Baue zusätzlich einen Webhook-Endpunkt, der die Signatur im Header X-Samy-Signature prüft und auf button_id reagiert. ``` ### Das Wichtigste in Kürze Für Assistenten, die nur diese Seite lesen: - **Senden:** `POST https://api-production-e537a.up.railway.app/v1/push`, Header `Authorization: Bearer ` und `Content-Type: application/json`. - **Body:** `text` ist Pflicht (bis 10.000 Zeichen). Optional: `title` (bis 80), `url`, `image`, `priority` (`normal`, `high`, `silent`), `buttons` (bis 3). - **Knopf:** `{"id": "…", "label": "…"}` meldet den Tipp per Webhook. Mit zusätzlichem `"url"` öffnet er einen Link. - **Erfolg:** Status 200 mit `{"message_id": "…"}`. - **Fehler:** 400 `invalid_body`, 401 `missing_key` oder `invalid_key`, 429 `rate_limited` (60 Aufrufe pro Minute und Schlüssel). - **Webhook:** `POST` an die Webhook-URL des Packlets mit `message_id`, `packlet_id`, `button_id`, `pressed_at`. Signatur: `X-Samy-Signature: sha256=`. Die Einzelheiten stehen auf [Nachricht senden](https://samy.sinio.io/docs/senden.md), [Knöpfe](https://samy.sinio.io/docs/knoepfe.md) und [Webhook](https://samy.sinio.io/docs/webhook.md). --- ## Nachricht senden Der eine Aufruf, mit dem dein Tool eine Nachricht schickt, und alle Felder dazu. ### Aufruf ```http POST https://api-production-e537a.up.railway.app/v1/push Authorization: Bearer Content-Type: application/json ``` Der Schlüssel bestimmt, in welchem Kanal die Nachricht landet. Eine Kanal-Angabe im Body gibt es nicht. ### Felder | Feld | Typ | Pflicht | Bedeutung | |---|---|---|---| | `text` | Text, 1 bis 10.000 Zeichen | ja | Die Nachricht. Der Push zeigt den Anfang, die App den ganzen Text. Einfache [Formatierung](https://samy.sinio.io/docs/formatierung.md) ist erlaubt. | | `title` | Text, bis 80 Zeichen | nein | Überschrift. Ohne Titel steht nur der Name des Packlets im Push. | | `url` | http(s)-Adresse, bis 2.048 Zeichen | nein | Link, der in der Nachricht angezeigt wird. | | `image` | http(s)-Adresse, bis 2.048 Zeichen | nein | Vorschaubild. | | `buttons` | Liste, bis 3 Einträge | nein | Knöpfe unter der Nachricht, siehe [Knöpfe](https://samy.sinio.io/docs/knoepfe.md). | | `priority` | `normal`, `high` oder `silent` | nein | Standard ist `normal`, siehe [Push, Ton und Ruhezeit](https://samy.sinio.io/docs/push.md). | Leere Texte (`""`) und `null` bei optionalen Feldern zählen als nicht angegeben. ### Beispiel ```bash curl https://api-production-e537a.up.railway.app/v1/push \ -H "Authorization: Bearer $SAMY_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Neue Bestellung #1042", "text": "2× Kaffeebecher, 89,00 €", "url": "https://shop.example.com/orders/1042", "priority": "normal" }' ``` ### Antwort ```json { "message_id": "5b0c1a52-…" } ``` Status 200 heißt: Die Nachricht ist gespeichert und erscheint im Kanal. Das gilt auch, wenn der Push einmal nicht zugestellt werden kann. Alle anderen Antworten stehen unter [Antworten und Fehler](https://samy.sinio.io/docs/antworten.md). --- ## Formatierung Überschriften, Listen und Fettdruck im Text einer Nachricht. Im Feld `text` ist einfache Formatierung erlaubt. Die App stellt sie dar. Im Push auf dem Sperrbildschirm werden die Zeichen entfernt. ### Was geht | Schreibweise | Ergebnis | |---|---| | `## Überschrift` | Überschrift | | `- Punkt` oder `• Punkt` | Liste | | `1. Punkt` | nummerierte Liste | | `**fett**` | Fettdruck | | Leerzeile | neuer Absatz | Anderes Markdown wie Links, Tabellen oder Code wird nicht umgewandelt und erscheint als normaler Text. ### Beispiel ```text ## Tagesbericht Heute lief alles **ohne Fehler**. - 18 Bestellungen - 1.284 € Umsatz 1. Versand vorbereiten 2. Lager prüfen ``` In JSON schreibst du Zeilenumbrüche als `\n`, eine Leerzeile also als `\n\n`. --- ## Knöpfe Mit Knöpfen öffnest du Links oder bedienst dein Tool direkt vom Handy. Eine Nachricht kann bis zu drei Knöpfe haben. Du gibst sie im Feld `buttons` an. ### Zwei Arten - **Link-Knopf** (mit `url`): öffnet die Adresse auf dem Handy. - **Aktions-Knopf** (ohne `url`): Samy meldet den Tipp an die Webhook-URL deines Packlets. So bedienst du dein Tool vom Handy aus. Dafür muss beim Packlet eine [Webhook-URL](https://samy.sinio.io/docs/webhook.md) eingetragen sein. ### Felder eines Knopfs | Feld | Typ | Pflicht | Bedeutung | |---|---|---|---| | `id` | Text, 1 bis 64 Zeichen | ja | Deine Kennung für den Knopf. Pro Nachricht eindeutig. Sie kommt im Webhook als `button_id` zurück. | | `label` | Text, 1 bis 40 Zeichen | ja | Beschriftung. | | `url` | http(s)-Adresse | nein | Macht aus dem Knopf einen Link-Knopf. | ### Beispiel ```bash curl https://api-production-e537a.up.railway.app/v1/push \ -H "Authorization: Bearer $SAMY_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Neue Bestellung #1042", "text": "2× Kaffeebecher, 89,00 €", "buttons": [ {"id": "label", "label": "Versandlabel drucken"}, {"id": "open", "label": "Details", "url": "https://shop.example.com/orders/1042"} ] }' ``` Der erste Knopf ist ein Aktions-Knopf, der zweite ein Link-Knopf. Nach dem Tipp zeigt die Nachricht in der App, welcher Knopf gedrückt wurde. --- ## Push, Ton und Ruhezeit Wann eine Nachricht als Push erscheint, wann mit Ton und wann sie still bleibt. Ob eine Nachricht als Push erscheint, entscheiden zwei Dinge: die Einstellungen des Packlets in der App und das Feld `priority` der Nachricht. ### Priorität | `priority` | Verhalten | |---|---| | `normal` | Push mit Ton, sofern Ton für das Packlet an ist. In der Ruhezeit kein Push. Das ist der Standard. | | `high` | Kommt auch in der Ruhezeit durch. Für alles, was nicht warten kann. | | `silent` | Push ohne Ton. In der Ruhezeit kein Push. | ### Einstellungen in der App Pro Packlet stellst du in der App ein: - **Push an/aus** und **Kanal stummschalten:** Ist Push aus oder der Kanal stumm, kommt kein Push, auch nicht bei `high`. - **Ton an/aus:** Ohne Ton kommen Pushes still an. - **Ruhezeit:** In diesem Zeitraum kommen nur Nachrichten mit `high` als Push. Die Nachricht steht in jedem Fall im Kanal. Nur der Push entfällt. ### Was im Push steht - Titel: der Name des Packlets, dazu der `title` der Nachricht, falls angegeben. - Text: der Anfang von `text` ohne Formatierungszeichen. --- ## Webhook So erfährt dein Tool, welcher Knopf gedrückt wurde, und prüft, dass die Anfrage echt ist. Über den Webhook erfährt dein Tool, dass du in der App auf einen Aktions-Knopf getippt hast. ### Einrichten Die Webhook-URL trägst du beim Anlegen des Packlets oder später in seinen Einstellungen ein. - Sie muss mit `https://` beginnen und öffentlich erreichbar sein. - Zugangsdaten in der Adresse (`https://name:passwort@…`) sind nicht erlaubt. ### Die Anfrage Bei einem Tipp schickt Samy einen `POST` mit diesem JSON-Body: ```json { "message_id": "5b0c1a52-…", "packlet_id": "9d2f7c10-…", "button_id": "label", "pressed_at": "2026-11-09T18:00:00Z" } ``` | Feld | Bedeutung | |---|---| | `message_id` | Die Nachricht, zu der der Knopf gehört. Derselbe Wert wie in der Antwort beim Senden. | | `packlet_id` | Das Packlet. | | `button_id` | Die `id`, die du dem Knopf gegeben hast. | | `pressed_at` | Zeitpunkt des Tipps, ISO 8601 in UTC. | | Header | Wert | |---|---| | `Content-Type` | `application/json` | | `X-Samy-Event` | `button.pressed` | | `X-Samy-Signature` | `sha256=` | | `User-Agent` | `Samy-Webhook/1.0` | ### Antworten und Wiederholungen - Antworte innerhalb von 10 Sekunden mit einem Status 2xx. Der Inhalt der Antwort wird nicht gelesen. - Bei Zeitüberschreitung, Status `429` oder einem Status ab 500 versucht Samy es bis zu fünfmal erneut, mit wachsendem Abstand. - Andere Fehler (zum Beispiel 400 oder 404) werden nicht wiederholt. - Weiterleitungen folgt Samy nicht. Wegen der Wiederholungen kann dieselbe Anfrage mehrfach ankommen. Merke dir `message_id` zusammen mit `button_id`, wenn eine Aktion nur einmal laufen darf. ### Signatur prüfen Mit der Signatur stellst du sicher, dass die Anfrage wirklich von Samy kommt. - Algorithmus: HMAC-SHA256 über den **unveränderten** Body (die rohen Bytes, nicht das neu erzeugte JSON). - Geheimnis: der SHA-256-Hash deines Schlüssels als Hex-Text in Kleinbuchstaben. - Format des Headers: `sha256=` gefolgt vom HMAC als Hex-Text. ```python import hashlib, hmac, os SAMY_KEY = os.environ["SAMY_KEY"] def is_from_samy(raw_body: bytes, signature: str) -> bool: secret = hashlib.sha256(SAMY_KEY.encode()).hexdigest() expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or "") ``` ```javascript import { createHash, createHmac, timingSafeEqual } from "node:crypto"; export function isFromSamy(rawBody, signature = "") { const secret = createHash("sha256").update(process.env.SAMY_KEY).digest("hex"); const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b); } ``` Erzeugst du in der App einen neuen Schlüssel, ändert sich auch das Geheimnis für die Signatur. --- ## Antworten und Fehler Was Samy auf einen Aufruf antwortet und was die Fehlercodes bedeuten. Alle Antworten von `POST /v1/push` sind JSON. ### Übersicht | Status | `error` | Bedeutung | Was tun | |---|---|---|---| | 200 | | Gespeichert. Antwort: `{"message_id": "…"}` | | | 400 | `invalid_body` | Ungültige Daten. | `details` lesen und den Body korrigieren. Nicht wiederholen. | | 401 | `missing_key` | Der Header `Authorization` fehlt. | Header `Authorization: Bearer ` setzen. | | 401 | `invalid_key` | Schlüssel falsch, neu erzeugt oder Packlet entfernt. | Aktuellen Schlüssel aus der App eintragen. | | 429 | `rate_limited` | Zu viele Aufrufe. | Kurz warten und erneut senden. | ### Aufbau eines Fehlers ```json { "error": "invalid_body", "message": "Ungültige Daten.", "details": [{ "path": "text", "message": "text darf nicht leer sein" }] } ``` - `error` ist eine feste Kennung, auf die dein Code prüfen kann. - `message` ist ein lesbarer Satz auf Deutsch. - `details` gibt es nur bei `invalid_body`. Jeder Eintrag nennt das Feld (`path`) und den Grund. ### Rate-Limit Erlaubt sind 60 Aufrufe pro Minute und Schlüssel. Das Limit ist reiner Missbrauchsschutz. ### Protokoll In den Einstellungen jedes Packlets zeigt die App die letzten Aufrufe mit Status, auch die Webhook-Aufrufe an dein Tool. Dort siehst du zuerst nach, wenn etwas nicht ankommt. --- ## Beispiele Fertige Bausteine für Python, Node.js, einen Webhook-Empfänger und die Shell. Fertige Bausteine zum Kopieren. Alle lesen den Schlüssel aus der Umgebungsvariable `SAMY_KEY`. ### Python ```python import os, requests SAMY_KEY = os.environ["SAMY_KEY"] def samy(text, title=None, url=None, buttons=None, priority="normal"): res = requests.post( "https://api-production-e537a.up.railway.app/v1/push", headers={"Authorization": f"Bearer {SAMY_KEY}"}, json={"title": title, "text": text, "url": url, "buttons": buttons or [], "priority": priority}, timeout=10, ) res.raise_for_status() return res.json()["message_id"] samy("2× Kaffeebecher, 89,00 €", title="Neue Bestellung #1042", buttons=[{"id": "label", "label": "Versandlabel drucken"}]) ``` ### Node.js ```javascript export async function samy(message) { const res = await fetch("https://api-production-e537a.up.railway.app/v1/push", { method: "POST", headers: { Authorization: `Bearer ${process.env.SAMY_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(message), }); if (!res.ok) throw new Error(`Samy antwortet mit ${res.status}: ${await res.text()}`); return (await res.json()).message_id; } await samy({ title: "Deploy fehlgeschlagen", text: "Build 318 ist abgebrochen.", priority: "high" }); ``` ### Webhook-Empfänger (Python, Flask) Reagiert auf einen Aktions-Knopf und prüft die [Signatur](https://samy.sinio.io/docs/webhook.md). ```python import hashlib, hmac, os from flask import Flask, request app = Flask(__name__) SECRET = hashlib.sha256(os.environ["SAMY_KEY"].encode()).hexdigest().encode() @app.post("/samy") def samy_webhook(): expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("X-Samy-Signature", "")): return "", 401 event = request.get_json() if event["button_id"] == "label": print("Versandlabel drucken für Nachricht", event["message_id"]) return "", 204 ``` ### Shell ```bash curl https://api-production-e537a.up.railway.app/v1/push \ -H "Authorization: Bearer $SAMY_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Backup","text":"Backup von heute Nacht ist fertig.","priority":"silent"}' ``` --- ## Sicherheit Wie Samy mit deinem Schlüssel umgeht und was du selbst beachten solltest. ### Der Schlüssel - Format: `samy_` gefolgt von 32 Zeichen. - Samy zeigt ihn nur einmal, direkt nach dem Erzeugen. Danach ist er in der App maskiert. - Gespeichert wird nur sein SHA-256-Hash. Den Klartext kennt Samy nach dem Erzeugen nicht mehr. ### Was du beachten solltest - Lege den Schlüssel in einer Umgebungsvariable ab, nicht im Code und nicht im Repository. - Nutze pro Tool ein eigenes Packlet. So lässt sich ein einzelner Schlüssel tauschen, ohne dass andere Tools ausfallen. - Prüfe bei Webhooks immer die [Signatur](https://samy.sinio.io/docs/webhook.md), bevor dein Tool etwas ausführt. ### Wenn ein Schlüssel verloren geht In den Einstellungen des Packlets erzeugst du einen neuen Schlüssel. Der alte ist sofort ungültig. Entfernst du ein Packlet, ist sein Schlüssel ebenfalls sofort ungültig. ### Datenschutz Server und Datenbank stehen in der EU. Dein Konto löschst du in der App, zusammen mit allen Daten.