{"openapi":"3.1.0","info":{"title":"Öffentliche API von QR Suisse — v1","version":"1.1.1","description":"Öffentliche REST-API von QR Suisse, verfügbar mit den Plänen Pro und Entreprise.\nDokumentiert die 8 Endpoints unter `/api/v1/*`, in ihrer Version 1.1.1. Innerhalb der v1\nsind Änderungen kompatible Ergänzungen, abgesehen von einigen Verhaltensänderungen, die im\n«Versionsverlauf» aufgeführt sind.\n\n## Authentifizierung\n\nHeader `Authorization: Bearer qrs_live_<apiKeyId>.<secret>` — eine einzige Zeichenkette,\ndie Sie unverändert aus dem Anzeigefenster im Dashboard kopieren (beim Erstellen oder\nRotieren des Schlüssels) und direkt in den Header einfügen. **Es handelt sich nicht um ein\nJWT**: Der Teil vor dem ersten `.` (nach dem festen Präfix `qrs_live_`) ist die Kennung des\nSchlüssels (direkte Suche, nie geheim), der Teil nach dem `.` ist das opake\nGeheimnis mit hoher Entropie, das auf dem Server in konstanter Zeit verglichen wird. Ein\nfehlender oder fehlerhafter Header, ein widerrufener oder unbekannter Schlüssel, ein\neingefrorener Schlüssel (siehe unten) oder ein falsches Geheimnis ergeben **alle** denselben\nFehler `401 unauthorized` — keiner dieser Fälle wird in der Antwort unterschieden (Schutz\nvor Enumeration). Jede `401`-Antwort trägt denselben Header\n`WWW-Authenticate: Bearer realm=\"qrsuisse-api\"` (RFC 6750), ohne Angabe zum Grund der\nAblehnung.\n\nDer Name des Schemas, `Bearer`, wird ohne Beachtung der Gross- und Kleinschreibung erkannt (RFC\n9110); darauf folgen ein einzelnes Leerzeichen und dann der Schlüssel. Beim Schlüssel selbst\nwird die Gross- und Kleinschreibung beachtet: Senden Sie ihn genau so, wie er kopiert wurde.\n\n**Nur HTTPS; ein über HTTP gesendeter Schlüssel muss rotiert werden.** Die API antwortet nur\nüber HTTPS. Eine über HTTP (`http://`) gesendete Anfrage wird weder umgeleitet noch\nverarbeitet: Sie erhält `403 https_required`, noch bevor der Schlüssel geprüft wird. Der\nSchlüssel wurde dann bereits unverschlüsselt über das Netz übertragen: Betrachten Sie ihn als\noffengelegt und rotieren Sie ihn im Dashboard (siehe «Rotation und Widerruf»).\n\nDer Zugang zur API insgesamt ist eine Funktion der **Pläne Pro und Entreprise** und wird bei\njeder Anfrage geprüft (nie zwischengespeichert). Jeder Plan erlaubt eine Höchstzahl\ngleichzeitig aktiver Schlüssel: **1 mit dem Pro-Plan, 10 mit dem Entreprise-Plan**. Senkt\nein Planwechsel diese Grenze, werden die überzähligen Schlüssel (die neuesten)\n**eingefroren** — weder widerrufen noch gelöscht — und sind automatisch wieder nutzbar,\nsobald wieder ein Plan mit mehr Schlüsseln aktiv ist. Endet ein Pro- oder\nEntreprise-Abonnement und wechselt das Konto zurück zum Gratis-Plan, werden **alle** seine\nSchlüssel eingefroren: Sie funktionieren sofort nicht mehr, auch nicht zum Lesen, und ergeben\n`401 unauthorized`. `403 plan_required` betrifft nur einen nicht eingefrorenen Schlüssel\neines Kontos, das keinen Pro- oder Entreprise-Plan mehr hat (Grenzfall, ausserhalb\ndes normalen Endes eines Abonnements).\n\n## Einen Schlüssel erhalten\n\nEin API-Schlüssel kann nur im Dashboard von QR Suisse erstellt werden, und zwar von einem\nMitglied eines Pro- oder Entreprise-Kontos mit der Berechtigung «Verwalter der API-Schlüssel»\n(oder Administrator); dieses Mitglied wählt auch die Rechte des Schlüssels. Es gibt kein OAuth,\nkeine automatische Client-Registrierung und keinen API-Endpoint, der einen Schlüssel ausgibt:\nEin Agent oder ein Tool kann sich keinen Schlüssel selbst beschaffen, dieses Mitglied stellt ihn\nbereit. Ein Agent ohne Schlüssel bittet daher den Benutzer, einen Schlüssel mit den nötigen\nRechten erstellen zu lassen und ihn über die Konfiguration des Agenten bereitzustellen. Der\nvollständige Schlüssel wird nur einmal angezeigt, beim Erstellen oder Rotieren: QR Suisse\nspeichert ihn nicht im Klartext und kann ihn nicht erneut senden.\n\n## Ausgesetztes Konto oder Konto, das auf die Löschung wartet\n\nWurde das Konto, dem der Schlüssel gehört, von QR Suisse **ausgesetzt** oder wartet es wegen\nInaktivität **auf die Löschung**, ergibt jede Anfrage mit einem gültigen Schlüssel\n`403 account_disabled`, **auch beim Lesen**. Die Meldung gibt an, welcher der beiden\nZustände vorliegt, nie den Grund der Aussetzung. Diese Prüfung erfolgt nach der Prüfung des\nSchlüssels und vor der Prüfung des Plans und der Rechte: Ein ungültiger, widerrufener oder\neingefrorener Schlüssel bleibt bei `401 unauthorized`. Der Schlüssel wird weder widerrufen\nnoch eingefroren: Ein neuer Schlüssel nützt nichts, der Inhaber des Kontos muss selbst\nhandeln. Wird die Aussetzung aufgehoben, funktioniert der Schlüssel automatisch wieder, ohne\nRotation. Das Warten auf die Löschung betrifft nur Konten mit dem Gratis-Plan: Nach der\nReaktivierung bleibt das Konto beim Gratis-Plan, und es gelten die üblichen Regeln\n(eingefrorener Schlüssel, also `401 unauthorized`, oder `403 plan_required`).\n\n## Atomare Rechte eines Schlüssels\n\nEin Schlüssel trägt eine Teilmenge der folgenden 5 atomaren Rechte: `QR_CREATE`, `QR_READ`,\n`QR_UPDATE`, `QR_DELETE`, `STATS_READ`. Jede Operation gibt das Recht an, das sie erfordert\n(«Erforderliches Recht») — es wird **nicht** über OAuth2-Scopes abgebildet (diese API kennt ein\neinziges Bearer-Authentifizierungsschema, keinen Standard-Scope-Mechanismus). Ein Aufruf mit\neinem authentifizierten Schlüssel ohne das erforderliche Recht ergibt `403 forbidden`.\n\n## Mandantentrennung\n\nJeder Schlüssel wirkt nur auf die Ressourcen des Kontos, das ihn ausgestellt hat. Eine\nfehlende Ressource ODER eine Ressource eines anderen Kontos ergibt **immer**\n`404 not_found` — nie `403`: Die API bestätigt nie, dass eine Ressource eines anderen Kontos\nexistiert.\n\n## Rotation und Widerruf\n\nDas **Rotieren** eines Schlüssels im Dashboard erzeugt ein neues Geheimnis und macht das alte im\nselben Moment ungültig: Ein Schlüssel hat nie zwei gültige Geheimnisse gleichzeitig. Nur das\nGeheimnis ändert sich: Der Schlüssel behält seine Kennung (den Teil vor dem `.`), seinen Namen\nund seine Rechte; solange das Tool, das ihn verwendet, den neuen Wert nicht erhalten hat, werden\nseine Aufrufe mit `401 unauthorized` abgelehnt. Das **Widerrufen** macht den Schlüssel sofort\nund endgültig unbrauchbar (`401 unauthorized`): Er kann weder reaktiviert noch rotiert werden.\nQR Suisse kann auch alle Schlüssel eines Kontos widerrufen, aktive wie eingefrorene, auf Wunsch\neines Administrators des Kontos oder wenn ein Schlüssel offengelegt oder ohne Zustimmung des\nInhabers verwendet worden sein könnte; die Administratoren des Kontos werden per E-Mail\ninformiert. Weder die Rotation noch der Widerruf ändern oder löschen einen QR-Code.\n\n## Schutz des Schlüssels\n\n- Behandeln Sie den vollständigen Schlüssel wie ein Passwort: Bewahren Sie ihn in einem\n  Secrets-Manager oder einer Umgebungsvariable auf, nie in versioniertem Code, einer geteilten\n  Datei, einem Ticket oder einem Screenshot.\n- Fügen Sie den Schlüssel nie in eine geteilte oder gespeicherte Unterhaltung ein, auch nicht\n  mit einem KI-Assistenten. Ein Agent erhält den Schlüssel über seine Konfiguration\n  (Umgebungsvariable, Secrets-Manager) und bittet den Benutzer nie, ihn in die Unterhaltung\n  einzugeben.\n- Senden Sie ihn nur im Header `Authorization`, über HTTPS, an `https://app.qrsuisse.ch/api/v1`\n  — nie in einer URL. Protokollieren Sie diesen Header nicht.\n- Geben Sie einem Schlüssel nur die nötigen Rechte: zum Beispiel `QR_READ` und `STATS_READ` (die\n  Voreinstellung «KI-Assistent» im Dashboard) für einen Agenten, der nur nachschlägt. Verwenden\n  Sie mit dem Entreprise-Plan einen Schlüssel pro Tool, damit Sie einen widerrufen können, ohne\n  die anderen zu unterbrechen.\n- Könnte der Schlüssel offengelegt worden sein, rotieren oder widerrufen Sie ihn sofort im\n  Dashboard; ist das Konto im Gratis-Plan, schreiben Sie an contact@qrsuisse.ch von der\n  E-Mail-Adresse eines Administrators des Kontos aus. Ein Agent, der vermutet, dass sein\n  Schlüssel offengelegt worden sein könnte, meldet dies dem Inhaber des Kontos.\n\n## Ratenbegrenzung (Rate Limiting)\n\nDrei unabhängige Limits:\n- **Allgemein** — 30 Anfragen / 60 Sekunden pro API-Schlüssel, gilt für **jede**\n  authentifizierte Anfrage an die 8 Endpoints.\n- **Erstellung** — 8 Anfragen / 60 Sekunden pro API-Schlüssel, gilt **zusätzlich** zum\n  allgemeinen Limit, nur für `POST /qr-codes`.\n- **PNG-Bild** — 10 Erzeugungen / 60 Sekunden **pro Konto** (von allen Schlüsseln des\n  Kontos geteilt), gilt **zusätzlich** zum allgemeinen Limit, nur für\n  `GET /qr-codes/{id}/image` im PNG-Format. Weder das SVG-Format noch eine `304`-Antwort\n  werden angerechnet.\n\nEine Überschreitung ergibt `429 rate_limited` mit einem Header `Retry-After` (Sekunden). Die\nLimits eines Schlüssels werden angerechnet, sobald Schlüssel und Geheimnis erkannt\nsind, auch wenn die Anfrage danach abgelehnt wird (`403 account_disabled`, `plan_required`\noder `forbidden`): Ist ein Limit ausgeschöpft, wird `429 rate_limited` zurückgegeben,\nvor diesen Ablehnungen.\n\nMit `401 unauthorized` abgelehnte Anfragen sind zusätzlich pro IP-Adresse begrenzt:\n60 Ablehnungen / 60 Sekunden pro IPv4-Adresse oder pro IPv6-Netz /64 und 300 Ablehnungen /\n60 Sekunden pro IPv6-Netz /48. Darüber hinaus gibt die API bis zum Ende des Zeitfensters\n`429 rate_limited` zurück (mit `Retry-After`), ohne den Schlüssel zu prüfen, auch wenn er\ngültig ist.\n\n## Kommerzielles Erstellungskontingent\n\n`POST /qr-codes` unterliegt zusätzlich einem monatlichen Erstellungskontingent:\n**5 Erstellungen pro UTC-Kalendermonat mit dem Pro-Plan, 100 mit dem Entreprise-Plan**,\ndanach dem Guthaben der gekauften API-Erstellungspakete (Einmalkauf, nach dem monatlichen\nKontingent verbraucht, vom ältesten zum neuesten). Jede erfolgreiche Erstellung wird\ngezählt, auch wenn der QR-Code danach gelöscht wird: Eine Löschung gibt keine Erstellung\nzurück. Das monatliche Kontingent ist nicht\nübertragbar und wird am 1. jedes Monats um 00:00 UTC zurückgesetzt. Sind das monatliche\nKontingent und alle verfügbaren Pakete aufgebraucht, ergibt die Erstellung\n`429 quota_exceeded` mit dem Datum, an dem das monatliche Kontingent zurückgesetzt wird\n(`resetAt`, ISO 8601) — harte Sperre, keine nutzungsabhängige Abrechnung in v1.\n\n`GET /usage` liefert jederzeit den Stand: Erstellungen des Monats, monatliches Kontingent,\nverbleibende Guthaben in den Paketen und Datum der Zurücksetzung.\n\n## Gemeinsames Fehlerformat\n\nJeder Fehler dieser API hat dieselbe Form `{ \"error\": \"<code>\", \"message\": \"<text>\",\n...allfällige Details }`. 14 stabile Codes: Ein bestehender Code ändert sich nie; eine\nNebenversion kann neue hinzufügen, nur für eine neue und optionale Funktion (siehe\n«Versionsverlauf»). Die Codes: `unauthorized` (401),\n`plan_required` (403), `forbidden` (403), `account_disabled` (403), `https_required` (403),\n`not_found` (404), `method_not_allowed` (405), `validation_error` (400), `quota_exceeded`\n(429), `rate_limited` (429), und für `POST /qr-codes` mit dem Header `Idempotency-Key`:\n`idempotency_key_mismatch` (422), `idempotency_in_progress` (409), `resource_deleted` (410),\nund für `POST /qr-codes` und `PATCH /qr-codes/{id}`: `payload_too_large` (413), Request-Body\ngrösser als 64 KiB.\nDer Text von `message` ist eine Lesehilfe, auf Französisch wie in den Beispielen dieser\nSpezifikation, und er kann sich ohne Vorankündigung ändern. Stützen Sie Ihren Code auf den\nCode `error`, nie auf `message`.\nEin URL-Pfad mit einer ungültigen `%`-Sequenz (z. B. `%zz`) wird vor jeder\nAuthentifizierung mit `400 validation_error` abgelehnt. Ein unbekannter Pfad unter\n`/api/v1/` ergibt `404 not_found`. Eine HTTP-Methode, die ein bestehender Endpunkt nicht\nunterstützt, ergibt `405 method_not_allowed`, mit einem Header `Allow`, der die akzeptierten\nMethoden auflistet. Diese beiden Antworten sind mit und ohne API-Schlüssel dieselben. Eine\nAnfrage über HTTP erhält `403 https_required` vor all diesen Prüfungen (siehe\n«Authentifizierung»).\n\n## Öffentlicher Index\n\n`GET /api/v1` liefert ohne Authentifizierung ein statisches JSON-Dokument: Name und Version\nder API sowie Links zu ihrer OpenAPI-Spezifikation, ihrem Katalog (RFC 9727), ihrer\nDokumentation und ihrer Anleitung zur Authentifizierung. Die Spezifikation ist unter\n`https://www.qrsuisse.ch/openapi.json` (und `/openapi.yaml`) veröffentlicht, standardmässig\nauf Englisch; `?lang=de`, `fr` oder `it` liefert die übersetzte Fassung. Der Index ist auf\n60 Anfragen / 60 Sekunden pro IPv4-Adresse oder pro IPv6-Netz /64 begrenzt, und auf 300\nAnfragen / 60 Sekunden pro IPv6-Netz /48 (darüber `429 rate_limited`).\n\nDie Anleitung zur Authentifizierung ist auf Englisch unter `https://www.qrsuisse.ch/auth.md`\nveröffentlicht.\n\n## Bewährte Vorgehensweisen\n\n- Senden Sie bei jeder Erstellung einen `Idempotency-Key` mit (ein neuer Zufallswert, UUID v4\n  empfohlen, keine Personendaten) und senden Sie denselben Wert erneut, wenn die Anfrage an\n  einer Zeitüberschreitung oder einem Verbindungsabbruch scheitert (siehe `POST /qr-codes`).\n- Warten Sie bei `429 rate_limited` oder `409 idempotency_in_progress` die in `Retry-After`\n  angegebene Zeit ab, bevor Sie es erneut versuchen; versuchen Sie es bei\n  `429 quota_exceeded` nicht vor `resetAt` oder dem Kauf eines Pakets erneut (der Stand lässt\n  sich mit `GET /usage` abrufen).\n- Um eine gefilterte oder sortierte Liste seitenweise abzurufen, senden Sie mit `cursor`\n  dieselben Filter- und Sortierparameter erneut; nur `limit` darf sich ändern.\n- Kodieren Sie in einem Datum mit Versatz (`createdFrom`, `createdBefore`) das `+` als `%2B`,\n  oder verwenden Sie `Z`.\n- Senden Sie das `ETag` eines Bildes in `If-None-Match` zurück, um es nicht erneut\n  herunterzuladen, und bevorzugen Sie SVG für den Druck.\n- Behandeln Sie Fehler nach ihrem Code `error`, nie nach `message`.\n\n## Versionsverlauf\n\nDie Version steht in `info.version` dieser Spezifikation. Innerhalb der v1 fügt eine\nNebenversion nur optionale Funktionen hinzu: Eine für die v1.0 geschriebene Integration\nfunktioniert ohne Änderung weiter, vorbehaltlich der «Verhaltensänderungen» unten. Eine\nKorrekturversion (zum Beispiel 1.1.1) behebt einen Fehler, ohne neue Funktion; sie kann eine\nAnfrage ablehnen, die dieser Dokumentation nicht entsprach. Eine inkompatible Änderung würde\nals v2 veröffentlicht, unter einer anderen Adresse.\n\n### 1.1.1 — Oktober 2026\n\nKorrekturversion, ohne neue Funktion.\n\nVerhaltensänderungen:\n- Ein Inhalt von `POST /qr-codes` oder `PATCH /qr-codes/{id}`, der kein gültiges UTF-8 ist\n  (zum Beispiel in ISO-8859-1 kodierter Text), wird mit `400 validation_error` abgelehnt.\n  Bisher wurden die ungültigen Bytes durch «�» ersetzt und gespeichert.\n- Zwei gleichzeitige `DELETE` auf denselben QR-Code konnten noch `500` antworten, und ein\n  `PATCH` auf einen QR-Code, der gleichzeitig gelöscht wird, konnte `200` antworten, ohne\n  etwas zu speichern, oder `500`: Die Anfrage, die den QR-Code bereits gelöscht vorfindet,\n  antwortet jetzt `404 not_found`, wie in 1.1.0 angekündigt.\n\n### 1.1.0 — Oktober 2026\n\nNeu:\n- **Archivieren und reaktivieren** eines QR-Codes mit `PATCH /qr-codes/{id}` und dem Feld\n  `active` (`false` archiviert, `true` reaktiviert), ohne eine Erstellung zu verbrauchen.\n  Recht `QR_UPDATE`.\n- **Liste filtern, durchsuchen und sortieren** mit `GET /qr-codes`: `active`, `type`, `q`\n  (Bezeichnung, Ziel und Ende der öffentlichen URL), `createdFrom`, `createdBefore`, `sort`.\n  Für die nächste Seite senden Sie dieselben Parameter mit `cursor` erneut.\n- **Erstellungskontingent abrufen** mit `GET /usage`: Erstellungen des Monats, monatliches\n  Kontingent, verbleibende Paketguthaben, Datum der Zurücksetzung. Recht `QR_CREATE`.\n- **Erstellung ohne Duplikat wiederholen** mit dem optionalen Header `Idempotency-Key` bei\n  `POST /qr-codes` (UUID v4 empfohlen, keine Personendaten, 24 Stunden). Drei neue\n  Fehlercodes, nur mit diesem Header zurückgegeben: `422 idempotency_key_mismatch`,\n  `409 idempotency_in_progress`, `410 resource_deleted`.\n\nVerhaltensänderungen:\n- `GET /qr-codes` ohne Parameter sortiert nach Erstellungsdatum absteigend, dann nach `id`\n  (vorher: nur nach `id` absteigend); die Reihenfolge ist in der Praxis dieselbe.\n- `nextCursor` ist jetzt ein opaker Wert. Ein ungültiger Cursor oder ein Cursor,\n  der mit anderen Parametern oder von einem anderen Konto erhalten wurde, wird mit\n  `400 validation_error` abgelehnt. Der frühere Cursor der v1.0 (die `id` des letzten\n  Elements) bleibt während der ganzen v1 gültig, für die Liste ohne Filter und Sortierung.\n- Die Parameter `active`, `type`, `q`, `createdFrom`, `createdBefore` und `sort` der Liste,\n  bisher ignoriert, werden jetzt angewendet und geprüft: Ein ungültiger, leerer oder\n  wiederholter Wert wird mit `400 validation_error` abgelehnt.\n- Ein Feld `active` im Inhalt von `PATCH /qr-codes/{id}`, bisher ignoriert, wird jetzt\n  angewendet.\n- Ein Request-Body grösser als 64 KiB bei `POST /qr-codes` oder `PATCH /qr-codes/{id}` wird\n  mit `413 payload_too_large` abgelehnt (neuer Code; ein normaler Inhalt kommt nicht in die\n  Nähe dieser Grenze).\n- Eine Bezeichnung mit einem Steuerzeichen (Nullzeichen, Tabulator, Zeilenumbruch …) wird mit\n  `400 validation_error` abgelehnt.\n- Alle Antworten tragen `Cache-Control: no-store`, ausser dem Bild eines QR-Codes\n  (`private, no-cache` mit `ETag`).\n- Ein `PATCH` oder `DELETE` auf einen QR-Code, der gleichzeitig von einer anderen Anfrage\n  gelöscht wird, antwortet `404 not_found` statt `500`.\n- Eine fehlerhafte Unicode-Zeichenkette (ein einzelnes Surrogat, zum Beispiel `\\ud800`) im\n  Inhalt von `POST /qr-codes` oder `PATCH /qr-codes/{id}` wird mit `400 validation_error`\n  abgelehnt statt mit einem Fehler `500`.\n\n### 1.0.0 — September 2026\n\nErste öffentliche Version: Erstellen, Abrufen, Auflisten, Ändern und Löschen von QR-Codes\n(Typen `URL` und `VCARD`), Bild als PNG oder SVG, Scan-Statistiken.\n","contact":{"name":"QR Suisse"}},"servers":[{"url":"https://app.qrsuisse.ch/api/v1","description":"Produktion"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"QR Codes","x-displayName":"QR-Codes","description":"Verwaltung der QR-Codes des Kontos, dem der API-Schlüssel gehört (Erstellen, Suchen, Ändern, Archivieren, Löschen), Herunterladen ihres Bildes und Abrufen ihrer Statistiken."},{"name":"Usage","x-displayName":"Erstellungskontingent","description":"Stand des API-Erstellungskontingents des Kontos, dem der API-Schlüssel gehört."}],"paths":{"/qr-codes":{"get":{"operationId":"listQrCodes","tags":["QR Codes"],"summary":"Liste der QR-Codes des Kontos","description":"Erforderliches Recht: **`QR_READ`**. Kein Kontingent (nur Lesen).\n\nOhne Parameter: alle QR-Codes des Kontos, aktive und archivierte, die neuesten zuerst\n(absteigendes Erstellungsdatum, dann `id`).\n\nOptionale, kombinierbare Filter: Status (`active`), Typ (`type`), Suche (`q`),\nErstellungszeitraum (`createdFrom`, `createdBefore`), Sortierung (`sort`). Die Suche\numfasst die Bezeichnung, das Ziel und das Ende der öffentlichen URL (`slug`), nie die\nFelder einer Kontaktkarte. Ein ungültiger Wert eines dieser Parameter wird mit `400\nvalidation_error` abgelehnt (die Meldung nennt den Parameter), nie durch einen\nStandardwert ersetzt: Eine Integration erhält nie, ohne es zu wissen, eine andere Liste\nals die angeforderte. Ein doppelt angegebener oder vorhandener, aber leerer Parameter\n(`?active=`) wird ebenso abgelehnt; einzig ein leerer `cursor` (erste Seite) und `limit`\nsind davon ausgenommen. Ein unbekannter Parametername wird ignoriert.\n\nPaginierung per opakem Cursor (`nextCursor`). Senden Sie für die nächste Seite **dieselben**\nFilter- und Sortierparameter erneut, zusammen mit `cursor`; nur `limit` darf sich ändern.\nEin Cursor, der mit anderen Parametern oder von einem anderen Konto erhalten wurde, wird\nmit `400` abgelehnt. Der Filter `accountId` des Schlüssels bleibt die einzige Grundlage\ndafür, was zurückgegeben wird.\n","parameters":[{"$ref":"#/components/parameters/ActiveFilterParam"},{"$ref":"#/components/parameters/TypeFilterParam"},{"$ref":"#/components/parameters/SearchParam"},{"$ref":"#/components/parameters/CreatedFromParam"},{"$ref":"#/components/parameters/CreatedBeforeParam"},{"$ref":"#/components/parameters/SortParam"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"Ergebnisseite.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeListPage"}}}},"400":{"description":"Ungültiger Wert eines Filter-, Sortier- oder Cursor-Parameters, wiederholter Parameter\noder Cursor, der mit anderen Parametern erhalten wurde oder dessen letzter QR-Code\ninzwischen gelöscht oder umbenannt wurde (Sortierung nach Bezeichnung). Die Meldung\nnennt den Parameter, nie seinen Wert.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createQrCode","tags":["QR Codes"],"summary":"Erstellen eines QR-Codes","description":"Erforderliches Recht: **`QR_CREATE`**. Unterliegt dem kommerziellen\nErstellungskontingent (monatliches Kontingent des Plans: 5 mit Pro, 100 mit Entreprise,\ndanach Guthaben der API-Erstellungspakete) — geprüft in derselben Transaktion wie das\nSchreiben, gegen eine gleichzeitige Erstellung knapp an der Grenze. Eine Erstellung\nbleibt gezählt, auch wenn der QR-Code danach gelöscht wird.\n\n`type` wird bei der Erstellung festgelegt (`URL` oder `VCARD`, Diskriminator des\nAnfrageinhalts) und ist danach **nie** änderbar. Bei `VCARD` sind `phones`/`emails`/\n`addresses` jeweils optional — fehlen sie, werden sie als leere Arrays behandelt (eine\nminimale Karte mit Vor- und Nachname ist ein gültiger Anwendungsfall).\n\nWird auf das allgemeine Limit **und** auf das gesonderte Limit `api-create` angerechnet\n(8/min, zusätzlich zum allgemeinen Limit von 30/min).\n\n**Wiederholungen ohne Duplikate.** Mit dem optionalen Header `Idempotency-Key` erzeugt\neine nach einem Netzwerkunterbruch oder einer Zeitüberschreitung erneut gesendete\nAnfrage keinen zweiten QR-Code und verbraucht keine zweite Erstellung des Kontingents.\nVerwenden Sie für jede Erstellung einen neuen Zufallswert (UUID v4 empfohlen) und\nsetzen Sie keine Personendaten ein. Verhalten:\n- gleicher Wert und gleicher Inhalt innerhalb von 24 Stunden: `201` mit dem bereits\n  erstellten QR-Code in seinem aktuellen Zustand und dem Header\n  `Idempotent-Replayed: true`; vom Kontingent wird nichts abgezogen. Diese Wiederholung\n  wird wie eine Erstellung auf die Limits angerechnet;\n- gleicher Wert mit anderem Inhalt: `422 idempotency_key_mismatch`, es wird nichts\n  erstellt;\n- gleicher Wert, während die erste Anfrage noch läuft: Die zweite wartet\n  normalerweise deren Ende ab und erhält denselben QR-Code; dauert das Warten zu lange,\n  `409 idempotency_in_progress` mit `Retry-After`;\n- der mit diesem Wert erstellte QR-Code wurde inzwischen gelöscht:\n  `410 resource_deleted`, es wird nichts neu erstellt;\n- eine abgelehnte Anfrage (`400`, `403`, `429`, Serverfehler) wird nicht festgehalten:\n  Derselbe Wert kann nach Behebung des Problems erneut verwendet werden;\n- nach 24 Stunden ist der Wert vergessen: Eine neue Anfrage mit demselben Wert\n  erstellt einen neuen QR-Code und verbraucht eine Erstellung.\n\nDer Wert von `Idempotency-Key` gilt pro API-Schlüssel: Derselbe Wert mit einem anderen API-Schlüssel\nerstellt einen anderen QR-Code. Gespeichert werden nur nicht umkehrbare Fingerabdrücke des\nWerts und des Inhalts, zusammen mit der Kennung des erstellten QR-Codes; sie werden\nwährend 24 Stunden berücksichtigt.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeCreateInput"}}}},"responses":{"201":{"description":"QR-Code erstellt, oder Erstellung mit demselben `Idempotency-Key` wiederholt.","headers":{"Idempotent-Replayed":{"description":"`true`, wenn die Antwort eine bereits mit demselben `Idempotency-Key` erfolgte\nErstellung wiederholt (keine neue Erstellung, kein Kontingent verbraucht). Sonst\nnicht vorhanden.\n","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"410":{"$ref":"#/components/responses/ResourceDeleted"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"}}}},"/qr-codes/{id}":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCode","tags":["QR Codes"],"summary":"Details eines QR-Codes","description":"Erforderliches Recht: **`QR_READ`**. Kein Kontingent.\n","responses":{"200":{"description":"QR-Code gefunden.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateQrCode","tags":["QR Codes"],"summary":"Teilweises Bearbeiten eines QR-Codes","description":"Erforderliches Recht: **`QR_UPDATE`**. Kein Erstellungskontingent: Weder eine Bearbeitung\nnoch eine Reaktivierung (`active: true`) verbraucht ein Guthaben.\n\n**Archivieren und Reaktivieren**: `active: false` archiviert den QR-Code — seine\nöffentliche Weiterleitung antwortet dann mit `404` («QR code introuvable ou désactivé.»),\ngenau wie nach dem Archivieren im Dashboard — und `active: true` reaktiviert ihn. `active` muss ein\nJSON-Boolean sein: `\"false\"`, `0` oder `null` werden mit `400 validation_error`\nabgelehnt. Enthält der Body nur `active` und befindet sich der QR-Code bereits im\nverlangten Zustand, lautet die Antwort `200`, ohne dass etwas geschrieben wird\n(`updatedAt` unverändert). `active` lässt sich\nmit den anderen Feldern kombinieren: Der ganze Body wird vor jedem Schreibvorgang\ngeprüft und dann in einem Schritt angewendet — bei einem `400`-Fehler wird nichts\nangewendet, weder die Archivierung noch die anderen Felder.\n\n`type` ist **nie** änderbar (ein Schlüssel `\"type\"` im Anfrageinhalt wird stillschweigend\nignoriert, nie ein Fehler `400`) — es ist immer der bereits gespeicherte Typ, der\nbestimmt, welche Felder des Anfrageinhalts unten tatsächlich gelten. Felder, die den\nbestehenden Typ der Ressource nicht betreffen, werden ebenso stillschweigend ignoriert\n(nie abgelehnt).\n\nBedeutung eines teilweisen PATCH: Ein im JSON-Inhalt fehlendes Feld lässt den aktuellen\nWert unverändert. Bei `organization`/`jobTitle`/`website` (VCARD) löscht ein\n**vorhandenes** Feld mit leerer Zeichenkette den Wert ausdrücklich (auf `null` gesetzt)\n— entscheidend ist sein Vorhandensein im JSON-Inhalt, nicht nur sein Wert. Bei\n`phones`/`emails`/`addresses` (VCARD) ersetzt ein **vorhandenes** Array (auch leer) die\ngesamte Kategorie.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeUpdateInput"}}}},"responses":{"200":{"description":"QR-Code aktualisiert.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteQrCode","tags":["QR Codes"],"summary":"Endgültiges Löschen eines QR-Codes","description":"Erforderliches Recht: **`QR_DELETE`**. Kein Kontingent. Eine Löschung gibt die Erstellung\nnicht an das monatliche Kontingent zurück.\n\n**Tatsächliche** Löschung (keine Archivierung): Telefonnummern, E-Mail-Adressen, Adressen\nund Tagesstatistiken des QR-Codes werden mit ihm gelöscht, und sein Slug wird gleichzeitig\ndauerhaft gesperrt — ein bereits mit diesem Slug gedrucktes Medium zeigt nie auf einen\nanderen, später erstellten QR-Code.\n","responses":{"204":{"description":"QR-Code gelöscht, kein Antwortinhalt."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/qr-codes/{id}/stats":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCodeStats","tags":["QR Codes"],"summary":"Scan-Statistiken eines QR-Codes","description":"Erforderliches Recht: **`STATS_READ`**. Kein Kontingent.\n\n`dailyStats` kann Lücken haben: Nur Tage mit mindestens einem Scan haben einen Eintrag\n(Tage ohne Scan werden nicht mit 0 aufgefüllt, anders als im Diagramm des Dashboards) —\nwer die API integriert, verarbeitet Rohdaten, keine Darstellung. Zeitfenster begrenzt auf\nden Verlauf der Pläne Pro und Entreprise (15 Monate) — der Zugang\nzur API selbst ist nur mit einem Pro- oder Entreprise-Plan möglich.\n","responses":{"200":{"description":"Statistiken des QR-Codes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/qr-codes/{id}/image":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCodeImage","tags":["QR Codes"],"summary":"Bild eines QR-Codes (PNG oder SVG)","description":"Erforderliches Recht: **`QR_READ`**. Kein Kontingent.\n\nGibt die Bilddatei des QR-Codes zurück, auf dem Server mit genau denselben Einstellungen\nwie im Dashboard erstellt (schwarz auf weiss, Fehlerkorrektur Stufe M, Rand von\n4 Modulen): Ein über die API heruntergeladener QR-Code ist identisch mit dem aus dem\nDashboard. In dieser Version lässt sich keine visuelle Option (Farbe, Logo, Rand)\neinstellen.\n\nDer codierte Inhalt ist die öffentliche URL des QR-Codes (`publicUrl` von\n`GET /qr-codes/{id}`): auf der eigenen Domain des Kontos, wenn sie aktiv ist, sonst auf\nder Standard-Domain. Das Bild wird auch geliefert, wenn der QR-Code deaktiviert ist\n(`active: false`).\n\nDie Datei wird als Anhang zurückgegeben (`Content-Disposition: attachment`), mit dem\nNamen `qr-suisse-<slug>.png` oder `qr-suisse-<slug>.svg`. Die Antwort enthält ein `ETag`:\nWenn Sie dessen Wert in `If-None-Match` zurücksenden, erhalten Sie `304 Not Modified`\nohne Inhalt, sofern sich das Bild nicht geändert hat (der Aufruf bleibt authentifiziert,\nwird auf das allgemeine Limit angerechnet und protokolliert, aber nicht auf das Limit für\nPNG-Erzeugungen).\n\nRatenbegrenzung: Zusätzlich zum allgemeinen Limit (30 Anfragen / 60 s pro Schlüssel)\nwird jede PNG-Erzeugung auf das gesonderte Limit von **10 Erzeugungen / 60 s pro Konto**\nangerechnet, das alle Schlüssel des Kontos teilen. SVG ist von diesem gesonderten Limit\nnicht betroffen.\nDas `ETag` ändert sich insbesondere, wenn die eigene Domain des Kontos aktiviert oder\ndeaktiviert wird.\n","parameters":[{"name":"format","in":"query","required":false,"description":"Bildformat, in Kleinbuchstaben. Jeder andere Wert ergibt `400 validation_error` (nie\nein stillschweigender Wechsel auf ein anderes Format).\n","schema":{"type":"string","enum":["png","svg"],"default":"png"}},{"name":"size","in":"query","required":false,"description":"Nur PNG: Seitenlänge des Quadrats in Pixeln, Rand inbegriffen, ganze Zahl von 128 bis\n1024. Ein nicht ganzzahliger Wert oder ein Wert ausserhalb dieser Grenzen ergibt\n`400 validation_error`. Wird bei SVG ignoriert (Vektorformat, verlustfrei skalierbar\n— das bevorzugte Format für den Grossformatdruck).\n","schema":{"type":"integer","minimum":128,"maximum":1024,"default":1024}},{"name":"If-None-Match","in":"header","required":false,"description":"`ETag`-Wert aus einem früheren Aufruf.","schema":{"type":"string"}}],"responses":{"200":{"description":"Bild des QR-Codes.","headers":{"ETag":{"description":"Fingerabdruck des Bildes (Format, Grösse und codierter Inhalt).","schema":{"type":"string"}},"Content-Disposition":{"description":"`attachment; filename=\"qr-suisse-<slug>.<png|svg>\"`","schema":{"type":"string"}},"Cache-Control":{"description":"`private, no-cache` — authentifizierte Antwort, nie in einem gemeinsamen Cache gespeichert, bei jeder Verwendung neu validiert.","schema":{"type":"string"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string"}}}},"304":{"description":"Das Bild hat sich seit dem in `If-None-Match` übermittelten `ETag` nicht geändert — kein Inhalt.","headers":{"ETag":{"description":"Fingerabdruck des Bildes, identisch mit dem übermittelten.","schema":{"type":"string"}}}},"400":{"description":"Ungültiger Parameter `format` oder `size`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"`rate_limited` — allgemeines Limit überschritten (30 Anfragen / 60 s pro\nAPI-Schlüssel), zu viele mit `401` abgelehnte Anfragen von derselben IP-Adresse\n(60 / 60 s oder 300 / 60 s pro IPv6-Netz /48) oder, beim PNG-Format, Limit für\nErzeugungen überschritten (10 Erzeugungen / 60 s pro Konto).\n","headers":{"Retry-After":{"description":"Geschätzte Anzahl Sekunden, bis das aktuelle Zeitfenster frei wird.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}}}}},"/usage":{"get":{"operationId":"getUsage","tags":["Usage"],"summary":"API-Erstellungskontingent des Kontos abrufen","description":"Erforderliches Recht: **`QR_CREATE`**. Kein Kontingent: Diese Abfrage verbraucht keine\nErstellung.\n\nLiefert für das Konto, dem der Schlüssel gehört, dieselben Zahlen, die im selben Moment\nauf der Seite «API-Schlüssel» des Dashboards angezeigt werden: API-Erstellungen im\nlaufenden UTC-Kalendermonat, monatliches Kontingent des Plans, verbleibende Guthaben in\nden API-Erstellungspaketen und Datum, an dem das monatliche Kontingent zurückgesetzt\nwird. Eine erfolgreiche Erstellung erhöht `used` um 1; eine Löschung senkt den Wert\nnicht. Über das monatliche Kontingent hinaus verbraucht jede Erstellung zusätzlich ein\nPaketguthaben.\n\nRichtwerte: Eine gleichzeitige Erstellung mit einem anderen Schlüssel des Kontos kann sie\nschon beim Lesen ungenau machen. Nur `POST /qr-codes` prüft das Kontingent verbindlich\n(siehe «Kommerzielles Erstellungskontingent»).\n","responses":{"200":{"description":"API-Erstellungskontingent des Kontos.","headers":{"Cache-Control":{"description":"`no-store` — die Antwort ändert sich mit jeder Erstellung und wird nie zwischengespeichert.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"qrs_live_<apiKeyId>.<secret>","description":"Opakes zweiteiliges Token, **kein JWT**: `qrs_live_<apiKeyId>.<secret>`,\nübermittelt in `Authorization: Bearer <Token>`.\n"}},"parameters":{"QrCodeIdParam":{"name":"id","in":"path","required":true,"description":"Kennung (`cuid`) des QR-Codes.","schema":{"type":"string"}},"LimitParam":{"name":"limit","in":"query","required":false,"description":"Höchstzahl der Elemente pro Seite. Standard 20. Ein fehlender oder nicht ganzzahliger Wert\noder ein Wert ≤ 0 fällt stillschweigend auf den Standard zurück (Komfortparameter, nie ein\nFehler `400`); ein Wert über 100 wird auf 100 begrenzt.\n","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"CursorParam":{"name":"cursor","in":"query","required":false,"description":"Wert von `nextCursor` aus der vorherigen Seite, unverändert mit denselben Filter- und\nSortierparametern zurückzusenden. Opaker Wert: Leiten Sie nichts daraus ab, sein Format\nkann sich ändern. Fehlt bei der ersten Seite. Ein ungültiger Cursor oder ein Cursor, der\nmit anderen Parametern oder von einem anderen Konto erhalten wurde, wird mit `400`\nabgelehnt. Der frühere Cursor der v1.0 (die `id` des letzten Elements) bleibt während der\ngesamten v1 gültig, für die Liste ohne Filter und ohne Sortierung.\n","schema":{"type":"string","maxLength":512}},"ActiveFilterParam":{"name":"active","in":"query","required":false,"description":"`true`: nur aktive QR-Codes; `false`: nur archivierte. Fehlt: beide.\nGenau `true` oder `false`, in Kleinbuchstaben.\n","schema":{"type":"string","enum":["true","false"]}},"TypeFilterParam":{"name":"type","in":"query","required":false,"description":"Ein oder mehrere Typen, durch Kommas getrennt (höchstens 8 Werte, Gross- oder\nKleinschreibung): `url` (alle Links, `type` `URL`), `web` (Webseite), `email` (Ziel\n`mailto:`), `phone` (`tel:`), `sms` (`sms:`), `ftp` (`ftp:`), `calendar`\n(Kalenderereignis), `vcard` (Kontaktkarte). Ein QR-Code wird zurückgegeben, wenn er einem\nder angegebenen Typen entspricht.\n","style":"form","explode":false,"schema":{"type":"array","maxItems":8,"items":{"type":"string","enum":["url","web","email","phone","sms","ftp","calendar","vcard"]}},"example":["vcard","email"]},"SearchParam":{"name":"q","in":"query","required":false,"description":"Suche in der Bezeichnung, im Ziel und im Ende der öffentlichen URL (`slug`), ohne\nUnterscheidung von Gross- und Kleinschreibung und Akzenten («geneve» findet «Genève»).\nNie in den Feldern einer Kontaktkarte. 1 bis 100 Zeichen nach Entfernen der Leerzeichen am\nAnfang und am Ende, ohne Steuerzeichen; `%` und `_` werden wörtlich gesucht.\n","schema":{"type":"string","minLength":1,"maxLength":100}},"CreatedFromParam":{"name":"createdFrom","in":"query","required":false,"description":"QR-Codes, die ab diesem Datum erstellt wurden, einschliesslich. ISO 8601: `JJJJ-MM-TT`\n(Mitternacht UTC) oder Datum mit Uhrzeit und `Z` oder einem Versatz. Kodieren Sie in der\nURL das `+` eines Versatzes als `%2B` (`createdFrom=2026-10-01T08:00:00%2B02:00`) oder\nverwenden Sie `Z` (`createdFrom=2026-10-01T06:00:00Z`); ein nicht kodiertes `+`, das als\nLeerzeichen ankommt, wird unmittelbar vor dem Versatz ebenfalls akzeptiert. Jahr von 1970\nbis 9999, in UTC berechnet, nachdem der Versatz angewendet wurde; ein nicht existierendes\nDatum (`2026-02-30`) oder ein Datum ausserhalb dieses Bereichs wird mit\n`400 validation_error` abgelehnt.\n","schema":{"type":"string"},"example":"2026-10-01"},"CreatedBeforeParam":{"name":"createdBefore","in":"query","required":false,"description":"QR-Codes, die vor diesem Datum erstellt wurden, ausschliesslich (gleiches Format wie\n`createdFrom`). Mit `createdFrom=2026-10-01&createdBefore=2026-11-01`: der ganze Monat\nOktober (UTC). `createdFrom` muss vor `createdBefore` liegen.\n","schema":{"type":"string"},"example":"2026-11-01"},"SortParam":{"name":"sort","in":"query","required":false,"description":"Reihenfolge der Liste: `created_desc` (Standard, neueste zuerst), `created_asc`,\n`label_asc` (Bezeichnung von A bis Z), `label_desc`. Bei Gleichstand entscheidet `id`.\nWird mit `label_asc` oder `label_desc` der letzte QR-Code einer Seite vor dem nächsten\nAufruf gelöscht oder umbenannt, antwortet die nächste Seite mit `400` (beginnen Sie die\nPaginierung wieder bei der ersten Seite), statt stillschweigend QR-Codes auszulassen: Für\neine vollständige Synchronisierung ist `created_desc` oder\n`created_asc` vorzuziehen.\n","schema":{"type":"string","enum":["created_desc","created_asc","label_asc","label_desc"],"default":"created_desc"}},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"description":"Vom Client für eine logische Erstellung gewählter Schlüssel, mit dem dieselbe Anfrage\nohne Duplikat erneut gesendet werden kann (siehe `POST /qr-codes`). 1 bis 255 sichtbare\nASCII-Zeichen, ohne Leerzeichen; umschliessende Anführungszeichen werden ignoriert.\nUUID v4 empfohlen, keine Personendaten. Ein ungültiger Wert ergibt\n`400 validation_error`.\n","schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]{1,255}$"},"example":"3f1c2b0e-8a4d-4c6e-9b7a-1d2e3f4a5b6c"}},"schemas":{"QrCodeType":{"type":"string","enum":["URL","VCARD"],"description":"Bei der Erstellung festgelegt, danach nie änderbar."},"PhoneType":{"type":"string","enum":["PROFESSIONAL","MOBILE","FAX"]},"EmailType":{"type":"string","enum":["PROFESSIONAL","PERSONAL"]},"AddressType":{"type":"string","enum":["PROFESSIONAL","HOME"]},"PhoneEntry":{"type":"object","required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/PhoneType"},"value":{"type":"string","minLength":1,"maxLength":255,"description":"Telefonnummer wie eingegeben (kein normiertes Format vorgeschrieben)."}},"description":"Höchstens 3 Telefonnummern pro VCARD-QR-Code."},"EmailEntry":{"type":"object","required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/EmailType"},"value":{"type":"string","format":"email","maxLength":255}},"description":"Höchstens 2 E-Mail-Adressen pro VCARD-QR-Code."},"AddressEntry":{"type":"object","required":["type"],"properties":{"type":{"$ref":"#/components/schemas/AddressType"},"street":{"type":"string","maxLength":255},"complement":{"type":"string","maxLength":255},"postalCode":{"type":"string","maxLength":10},"city":{"type":"string","maxLength":255}},"description":"Höchstens 2 Postadressen pro VCARD-QR-Code. Die 4 Felder sind\njeweils optional (keines ist für einen gültigen Eintrag erforderlich).\n"},"VCardData":{"type":"object","required":["firstName","lastName","phones","emails","addresses"],"properties":{"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255},"jobTitle":{"type":"string","maxLength":255},"website":{"type":"string","maxLength":255},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2}},"description":"Nur in `QrCodeApiDto.vcard` vorhanden, wenn `type === \"VCARD\"`. `organization`/\n`jobTitle`/`website` fehlen in der Antwort, wenn sie nicht ausgefüllt sind (nie ein\nausdrückliches `null`) — wer die API integriert, prüft das Vorhandensein des Schlüssels,\nnicht seinen Wert.\n"},"QrCodeApiDto":{"type":"object","required":["id","type","label","active","scanCount","slug","publicUrl","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"Kennung (`cuid`) des QR-Codes."},"type":{"$ref":"#/components/schemas/QrCodeType"},"label":{"type":"string"},"active":{"type":"boolean"},"scanCount":{"type":"integer","description":"Gesamtzahl der Scans seit der Erstellung."},"slug":{"type":"string"},"publicUrl":{"type":"string","format":"uri","description":"Vollständige öffentliche Weiterleitungs-URL (eigene Domain des Kontos, wenn sie aktiv\nist, sonst Standard-Domain) — nie so gespeichert, immer aus dem aktuellen Zustand des\nKontos berechnet.\n"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"destinationUrl":{"type":"string","format":"uri","description":"Nur vorhanden, wenn `type === \"URL\"`, und nur, wenn ausgefüllt."},"vcard":{"allOf":[{"$ref":"#/components/schemas/VCardData"}],"description":"Nur vorhanden, wenn `type === \"VCARD\"`."}},"example":{"id":"cm2f8k3qz0001xyz9abcd1234","type":"URL","label":"Affiche salle d'attente","active":true,"scanCount":42,"slug":"AbC123_-xy","publicUrl":"https://app.qrsuisse.ch/r/AbC123_-xy","createdAt":"2026-10-01T08:30:00.000Z","updatedAt":"2026-10-12T14:05:00.000Z","destinationUrl":"https://www.exemple.ch/rendez-vous"},"description":"Öffentliche Darstellung eines QR-Codes: Es werden nur die folgenden Felder\nzurückgegeben, keine internen Daten des Kontos.\n"},"QrCodeListPage":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/QrCodeApiDto"}},"nextCursor":{"type":["string","null"],"description":"Opaker Cursor, in `?cursor` mit denselben Filter- und Sortierparametern zu übergeben,\num die nächste Seite zu erhalten; `null`, wenn dies die letzte Seite ist.\n"}}},"QrCodeCreateUrlInput":{"type":"object","required":["type","label","destinationUrl"],"properties":{"type":{"type":"string","enum":["URL"]},"label":{"type":"string","minLength":1,"maxLength":255},"destinationUrl":{"type":"string","maxLength":2048,"description":"Ziel-URL — akzeptierte Protokolle: `http:`/`https:` (ohne weitere Einschränkung),\n`mailto:` (lokaler Teil vor einem allfälligen `?` als E-Mail-Adresse geprüft),\n`tel:`/`sms:` (nur Ziffern/Leerzeichen/`+`/`-`/Klammern, 4 bis 20 Zeichen, `*`/`#`\nausdrücklich verboten — Risiko eines USSD-Codes), `ftp:` (Zugangsdaten im Klartext in\nder Authority verboten). Kein Steuerzeichen (wörtlich oder als `%XX` codiert).\n"}},"example":{"type":"URL","label":"Affiche salle d'attente","destinationUrl":"https://www.exemple.ch/rendez-vous"},"description":"Inhalt von `POST /qr-codes` für einen QR-Code vom Typ `URL`."},"QrCodeCreateVcardInput":{"type":"object","required":["type","label","firstName","lastName"],"properties":{"type":{"type":"string","enum":["VCARD"]},"label":{"type":"string","minLength":1,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255},"jobTitle":{"type":"string","maxLength":255},"website":{"type":"string","maxLength":255},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3,"default":[]},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2,"default":[]},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2,"default":[]}},"example":{"type":"VCARD","label":"Carte de visite Dr Muster","firstName":"Anna","lastName":"Muster","organization":"Cabinet Muster","phones":[{"type":"PROFESSIONAL","value":"+41 27 123 45 67"}],"emails":[{"type":"PROFESSIONAL","value":"anna.muster@exemple.ch"}]},"description":"Inhalt von `POST /qr-codes` für einen QR-Code vom Typ `VCARD`. Nur `label`/`firstName`/\n`lastName` sind Pflicht — eine minimale Karte (nur ein Name) ist ein gültiger\nAnwendungsfall; fehlende `phones`/`emails`/`addresses` entsprechen einem leeren Array.\n"},"QrCodeCreateInput":{"oneOf":[{"$ref":"#/components/schemas/QrCodeCreateUrlInput"},{"$ref":"#/components/schemas/QrCodeCreateVcardInput"}],"discriminator":{"propertyName":"type","mapping":{"URL":"#/components/schemas/QrCodeCreateUrlInput","VCARD":"#/components/schemas/QrCodeCreateVcardInput"}},"description":"Nach `type` unterschiedene Union, bei der Erstellung festgelegt — dieselben\nValidierungsregeln wie im Dashboard (unverändert übernommen, kein paralleles Schema).\n"},"QrCodeUpdateInput":{"type":"object","properties":{"active":{"type":"boolean","description":"`false` archiviert den QR-Code, `true` reaktiviert ihn. Strikter JSON-Boolean, gilt für beide Typen."},"label":{"type":"string","minLength":1,"maxLength":255},"destinationUrl":{"type":"string","maxLength":2048,"description":"Gilt nur, wenn die bestehende Ressource vom Typ `URL` ist — siehe Beschreibung der Operation."},"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255,"description":"Eine leere Zeichenkette löscht den Wert ausdrücklich (`null`), sofern das Feld vorhanden ist."},"jobTitle":{"type":"string","maxLength":255,"description":"Eine leere Zeichenkette löscht den Wert ausdrücklich (`null`), sofern das Feld vorhanden ist."},"website":{"type":"string","maxLength":255,"description":"Eine leere Zeichenkette löscht den Wert ausdrücklich (`null`), sofern das Feld vorhanden ist."},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3,"description":"Falls vorhanden (auch leer), ersetzt die gesamte Kategorie."},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2,"description":"Falls vorhanden (auch leer), ersetzt die gesamte Kategorie."},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2,"description":"Falls vorhanden (auch leer), ersetzt die gesamte Kategorie."}},"example":{"destinationUrl":"https://www.exemple.ch/nouvelle-page"},"description":"Inhalt von `PATCH /qr-codes/{id}` — **alle Felder sind optional** (teilweise Bearbeitung:\nEin fehlendes Feld lässt den aktuellen Wert unverändert). `type` erscheint **nie** in\ndiesem Inhalt (nicht änderbar, siehe Beschreibung der Operation): Es ist der bereits\ngespeicherte Typ, der bestimmt, welche Felder tatsächlich gelten, `destinationUrl`\n(`URL`) oder `firstName`/`lastName`/`organization`/`jobTitle`/`website`/`phones`/\n`emails`/`addresses` (`VCARD`) — Felder, die den bestehenden Typ nicht betreffen, werden\nstillschweigend ignoriert, nie abgelehnt.\n"},"StatsApiDto":{"type":"object","required":["qrCodeId","scanCountTotal","dailyStats"],"properties":{"qrCodeId":{"type":"string"},"scanCountTotal":{"type":"integer","description":"Gesamtzahl der Scans seit der Erstellung (`QrCode.scanCount`), nicht nur im Zeitfenster von `dailyStats`."},"dailyStats":{"type":"array","items":{"type":"object","required":["date","count"],"properties":{"date":{"type":"string","format":"date-time"},"count":{"type":"integer"}}},"description":"Ein Element pro Tag mit mindestens einem Scan (keine Auffüllung mit 0), begrenzt auf\ndas Verlaufsfenster der Pläne Pro und Entreprise (15 Monate).\n"}},"example":{"qrCodeId":"cm2f8k3qz0001xyz9abcd1234","scanCountTotal":42,"dailyStats":[{"date":"2026-10-11T00:00:00.000Z","count":5},{"date":"2026-10-12T00:00:00.000Z","count":3}]},"description":"Öffentliches DTO des Statistik-Endpoints."},"UsageApiDto":{"type":"object","required":["used","monthlyQuota","packCreditsRemaining","resetAt"],"additionalProperties":false,"properties":{"used":{"type":"integer","minimum":0,"description":"API-Erstellungen im laufenden UTC-Kalendermonat, einschliesslich derjenigen, die ein\nPaketguthaben verbraucht haben, und derjenigen von seither gelöschten QR-Codes. Kann\ndaher `monthlyQuota` übersteigen.\n"},"monthlyQuota":{"type":"integer","minimum":0,"description":"Im Plan monatlich enthaltene Erstellungen (5 mit Pro, 100 mit Entreprise)."},"packCreditsRemaining":{"type":"integer","minimum":0,"description":"Noch verfügbare Guthaben in den API-Erstellungspaketen des Kontos, alle nutzbaren Pakete zusammen."},"resetAt":{"type":"string","format":"date-time","description":"Zurücksetzung des monatlichen Kontingents — 1. Tag des folgenden UTC-Kalendermonats, um Mitternacht UTC."}},"example":{"used":7,"monthlyQuota":5,"packCreditsRemaining":98,"resetAt":"2026-11-01T00:00:00.000Z"},"description":"Öffentliches DTO des Kontingent-Endpoints — nur diese vier Felder."},"ApiErrorBase":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["unauthorized","plan_required","forbidden","account_disabled","https_required","not_found","method_not_allowed","validation_error","quota_exceeded","rate_limited","idempotency_key_mismatch","idempotency_in_progress","resource_deleted","payload_too_large"]},"message":{"type":"string"}},"description":"Gemeinsame Form aller Fehler der API v1."},"UnauthorizedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"unauthorized","message":"Authentification invalide ou absente."}},"PlanRequiredError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"plan_required","message":"Cette fonctionnalité nécessite un plan Pro ou Entreprise."}},"ForbiddenError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"forbidden","message":"Cette clé API n'a pas le droit requis pour cette opération."}},"AccountDisabledError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"account_disabled","message":"Ce compte a été suspendu. Contactez le support QR Suisse à l'adresse contact@qrsuisse.ch pour plus d'informations."},"description":"Konto ausgesetzt oder wartet auf die Löschung. `message` (auf Französisch) lautet je nach\nZustand:\n- ausgesetzt: « Ce compte a été suspendu. Contactez le support QR Suisse à l'adresse\n  contact@qrsuisse.ch pour plus d'informations. » (Dieses Konto wurde ausgesetzt.\n  Kontaktieren Sie den QR-Suisse-Support unter contact@qrsuisse.ch für weitere\n  Informationen.)\n- wartet auf die Löschung: « Ce compte est en attente de suppression pour inactivité.\n  Utilisez le lien de récupération reçu par email pour le réactiver. » (Dieses Konto\n  wartet wegen Inaktivität auf die Löschung. Verwenden Sie den per E-Mail erhaltenen\n  Wiederherstellungslink, um es zu reaktivieren.)\n"},"HttpsRequiredError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"https_required","message":"L'API n'accepte que HTTPS : la requête n'a pas été traitée. Une clé envoyée en HTTP a circulé en clair, faites-la tourner depuis le tableau de bord."},"description":"Über HTTP erhaltene Anfrage: weder umgeleitet noch verarbeitet, noch bevor der Schlüssel\ngeprüft wird. Ein so gesendeter Schlüssel wurde unverschlüsselt übertragen und muss\nrotiert werden.\n"},"NotFoundError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"not_found","message":"Ressource introuvable."}},"ValidationError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"validation_error","message":"Le libellé est requis"},"description":"`message` übernimmt die erste festgestellte Validierungsmeldung (z. B. ein Pflichtfeld,\ndas fehlt oder zu lang ist) — der genaue Text hängt vom betroffenen Feld ab, die Form\n`{ error, message }` bleibt immer gleich. Dieser Text ist auf Französisch; eine allgemeine\nMeldung nennt das betroffene Feld (z. B. «Champ « type » requis : valeurs acceptées URL,\nVCARD.»): Nur der Code `error` ist garantiert.\n"},"QuotaExceededError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["resetAt"],"properties":{"resetAt":{"type":"string","format":"date-time","description":"1. Tag des folgenden UTC-Kalendermonats, um Mitternacht UTC — Datum, an dem das Kontingent zurückgesetzt wird."}}}],"example":{"error":"quota_exceeded","message":"Quota mensuel de créations API atteint.","resetAt":"2026-10-01T00:00:00.000Z"}},"RateLimitedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"rate_limited","message":"Trop de requêtes pour cette clé API, réessayez plus tard."}},"IdempotencyKeyMismatchError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"idempotency_key_mismatch","message":"Cette clé Idempotency-Key a déjà servi pour une requête différente. Utilisez une nouvelle clé pour une nouvelle création."}},"IdempotencyInProgressError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"idempotency_in_progress","message":"Une requête avec cette clé Idempotency-Key est encore en cours de traitement, réessayez dans un instant."}},"ResourceDeletedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"resource_deleted","message":"Le QR code créé avec cette clé Idempotency-Key a été supprimé depuis. Utilisez une nouvelle clé pour une nouvelle création."}},"PayloadTooLargeError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"payload_too_large","message":"Corps de la requête trop volumineux : 64 Kio maximum."}}},"responses":{"Unauthorized":{"description":"Header `Authorization` fehlt oder ist fehlerhaft, Schlüssel unbekannt/widerrufen/eingefroren oder Geheimnis falsch.","headers":{"WWW-Authenticate":{"description":"Immer `Bearer realm=\"qrsuisse-api\"`, unabhängig vom Grund der Ablehnung.","schema":{"type":"string","const":"Bearer realm=\"qrsuisse-api\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}},"ForbiddenOrPlanRequired":{"description":"`403` — entweder wurde die Anfrage über HTTP gesendet (`https_required`, noch bevor der\nSchlüssel geprüft wird), oder das Konto ist ausgesetzt oder wartet auf die Löschung\n(`account_disabled`, bei einem gültigen Schlüssel zuerst geprüft), oder das Konto hat\nkeinen Pro- oder Entreprise-Plan mehr (`plan_required`), oder der authentifizierte\nAPI-Schlüssel hat nicht das für diese Operation erforderliche atomare Recht\n(`forbidden`).\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/HttpsRequiredError"},{"$ref":"#/components/schemas/AccountDisabledError"},{"$ref":"#/components/schemas/PlanRequiredError"},{"$ref":"#/components/schemas/ForbiddenError"}]}}}},"NotFound":{"description":"Ressource fehlt oder gehört zu einem anderen Konto (nie von einem einfachen Fehlen unterschieden).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"ValidationError":{"description":"Anfrageinhalt fehlt, ist kein JSON oder entspricht nicht den Validierungsregeln des betroffenen Felds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"RateLimited":{"description":"Allgemeines Limit überschritten (30 Anfragen / 60 s pro API-Schlüssel) oder zu viele\nmit `401` abgelehnte Anfragen von derselben IP-Adresse (60 / 60 s oder 300 / 60 s pro\nIPv6-Netz /48).\n","headers":{"Retry-After":{"description":"Geschätzte Anzahl Sekunden, bis das aktuelle Zeitfenster frei wird.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}},"RateLimitedOrQuotaExceeded":{"description":"`429` — entweder ein Limit der Ratenbegrenzung überschritten (allgemein 30/min,\nErstellung 8/min oder 60 `401`-Ablehnungen/min von derselben IP-Adresse, 300/min pro\nIPv6-Netz /48 — `rate_limited`), oder das kommerzielle monatliche Kontingent an\nAPI-Erstellungen erreicht (`quota_exceeded`, noch vor jedem Schreiben in die Datenbank).\n","headers":{"Retry-After":{"description":"Nur bei `rate_limited` vorhanden — geschätzte Anzahl Sekunden, bis das aktuelle Zeitfenster frei wird.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/QuotaExceededError"}]}}}},"IdempotencyKeyMismatch":{"description":"`422` — derselbe `Idempotency-Key` wurde innerhalb der letzten 24 Stunden bereits für eine\nAnfrage mit anderem Inhalt verwendet. Es wird nichts erstellt, kein Kontingent verbraucht.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyKeyMismatchError"}}}},"IdempotencyInProgress":{"description":"`409` — eine Anfrage mit demselben `Idempotency-Key` läuft noch, und das Warten auf ihr\nErgebnis hat zu lange gedauert. Diese Anfrage erstellt nichts: Senden Sie sie nach\n`Retry-After` erneut, sie erhält den von der ersten erstellten QR-Code.\n","headers":{"Retry-After":{"description":"Empfohlene Wartezeit vor einem neuen Versuch, in Sekunden.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyInProgressError"}}}},"ResourceDeleted":{"description":"`410` — der mit diesem `Idempotency-Key` erstellte QR-Code wurde inzwischen gelöscht. Es\nwird nichts neu erstellt: Verwenden Sie für eine neue Erstellung einen neuen Schlüssel.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceDeletedError"}}}},"PayloadTooLarge":{"description":"`413` — Request-Body grösser als 64 KiB (65 536 Byte): Die Anfrage wird abgelehnt, es wird\nnichts geschrieben.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayloadTooLargeError"}}}}}}}