{"openapi":"3.1.0","info":{"title":"API pubblica di QR Suisse — v1","version":"1.1.1","description":"API REST pubblica di QR Suisse, disponibile con i piani Pro ed Entreprise. Documenta gli\n8 endpoint esposti sotto `/api/v1/*`, nella loro versione 1.1.1. All'interno della v1, le\nevoluzioni sono aggiunte compatibili, salvo alcuni cambiamenti di comportamento elencati nella\n«Cronologia delle versioni».\n\n## Autenticazione\n\nHeader `Authorization: Bearer qrs_live_<apiKeyId>.<secret>` — un'unica stringa da copiare\ncosì com'è dal riquadro di visualizzazione della dashboard (alla creazione o alla rotazione\ndella chiave) e da incollare direttamente nell'header. **Non è un JWT**: la parte prima del\nprimo `.` (dopo il prefisso fisso `qrs_live_`) è l'identificativo della chiave (ricerca\ndiretta, mai segreto), la parte dopo il `.` è il segreto opaco ad alta entropia, confrontato\nin tempo costante sul server. Un header assente o malformato, una chiave revocata o\ninesistente, una chiave congelata (vedi sotto) o un segreto errato restituiscono **tutti** lo\nstesso errore `401 unauthorized` — nessuno di questi casi è distinto nella risposta\n(protezione contro l'enumerazione). Ogni `401` riporta lo stesso header\n`WWW-Authenticate: Bearer realm=\"qrsuisse-api\"` (RFC 6750), senza alcun dettaglio sul\nmotivo del rifiuto.\n\nIl nome dello schema, `Bearer`, è riconosciuto senza distinzione tra maiuscole e minuscole (RFC\n9110); è seguito da un solo spazio, poi dalla chiave. La chiave, invece, distingue tra maiuscole\ne minuscole: la invii esattamente come è stata copiata.\n\n**Solo HTTPS; una chiave inviata in HTTP deve essere ruotata.** L'API risponde solo in HTTPS.\nUna richiesta inviata in HTTP (`http://`) non viene né reindirizzata né elaborata: riceve\n`403 https_required`, prima di qualsiasi verifica della chiave. La chiave ha però già\nviaggiato in chiaro sulla rete: la consideri esposta e la ruoti dalla dashboard (vedi\n«Rotazione e revoca»).\n\nL'accesso all'API nel suo insieme è una funzionalità dei **piani Pro ed Entreprise**,\nverificata a ogni richiesta (mai memorizzata nella cache). Ogni piano consente un numero\nmassimo di chiavi attive contemporaneamente: **1 con il piano Pro, 10 con il piano\nEntreprise**. Quando un cambio di piano riduce questo limite, le chiavi in eccesso (le più\nrecenti) vengono **congelate** — né revocate né eliminate — e tornano utilizzabili\nautomaticamente non appena è di nuovo attivo un piano che consente più chiavi. Quando un\nabbonamento Pro o Entreprise termina e l'account torna al piano Gratuito, **tutte** le sue\nchiavi vengono congelate: smettono immediatamente di funzionare, anche in lettura, con\n`401 unauthorized`. `403 plan_required` riguarda solo una chiave non congelata di un account\nche non è più su un piano Pro o Entreprise (caso limite, al di fuori della fine normale di\nun abbonamento).\n\n## Ottenere una chiave\n\nUna chiave API si crea soltanto dalla dashboard di QR Suisse, da parte di un membro di un\naccount Pro o Entreprise che ha il permesso «Gestore delle chiavi API» (o Amministratore);\nquesto membro sceglie anche i diritti della chiave. Non esiste OAuth, né una registrazione\nautomatica dei client, né un endpoint dell'API che rilasci una chiave: un agente o uno strumento\nnon può ottenere una chiave da solo, è questo membro a fornirgliela. Un agente senza chiave\nchiede quindi all'utente di farne creare una con i diritti necessari e di fornirgliela tramite\nla configurazione dell'agente. La chiave completa viene mostrata una sola volta, alla creazione\no alla rotazione: QR Suisse non la conserva in chiaro e non può inviarla di nuovo.\n\n## Account sospeso o in attesa di eliminazione\n\nQuando l'account proprietario della chiave è **sospeso** da QR Suisse, o **in attesa di\neliminazione** per inattività, ogni richiesta con una chiave valida restituisce\n`403 account_disabled`, **lettura compresa**. Il messaggio indica quale dei due stati si\napplica, mai il motivo della sospensione. Questo controllo avviene dopo la verifica della\nchiave e prima di quella del piano e dei diritti: una chiave non valida, revocata o congelata\nresta in `401 unauthorized`. La chiave non viene né revocata né congelata: è inutile\nrigenerarla, serve un intervento del titolare dell'account. Quando la sospensione viene\ntolta, la chiave torna a funzionare automaticamente, senza rotazione. L'attesa di eliminazione\nriguarda solo account con il piano Gratuito: dopo la riattivazione, l'account resta al piano\nGratuito e si applicano le regole abituali (chiave congelata, quindi `401 unauthorized`, o\n`403 plan_required`).\n\n## Diritti atomici di una chiave\n\nUna chiave porta un sottoinsieme dei 5 diritti atomici seguenti: `QR_CREATE`, `QR_READ`,\n`QR_UPDATE`, `QR_DELETE`, `STATS_READ`. Ogni operazione indica il diritto che richiede («Diritto\nrichiesto») — **non** è modellato con scope OAuth2 (questa API ha un solo schema di\nautenticazione Bearer, non un meccanismo di scope standard). Una chiamata con una chiave\nautenticata ma priva del diritto richiesto restituisce `403 forbidden`.\n\n## Isolamento multi-tenant\n\nOgni chiave agisce solo sulle risorse dell'account che l'ha emessa. Una risorsa inesistente\nO appartenente a un altro account restituisce **sempre** `404 not_found` — mai `403`: l'API\nnon conferma mai l'esistenza di una risorsa di un altro account.\n\n## Rotazione e revoca\n\nLa **rotazione** di una chiave, dalla dashboard, genera un nuovo segreto e invalida quello\nvecchio nello stesso istante: una chiave non ha mai due segreti validi contemporaneamente.\nCambia solo il segreto: la chiave conserva il suo identificativo (la parte prima del `.`), il\nsuo nome e i suoi diritti; finché lo strumento che la utilizza non ha ricevuto il nuovo valore,\nle sue chiamate vengono rifiutate con `401 unauthorized`. La **revoca** rende la chiave\ninutilizzabile immediatamente e definitivamente (`401 unauthorized`): non può più essere né\nriattivata né sottoposta a rotazione. QR Suisse può anche revocare tutte le chiavi di un\naccount, attive o congelate, su richiesta di un amministratore dell'account o se una chiave\npotrebbe essere stata esposta o utilizzata senza il consenso del titolare; gli amministratori\ndell'account ne vengono informati per e-mail. Né la rotazione né la revoca modificano o\neliminano alcun codice QR.\n\n## Protezione della chiave\n\n- Tratti la chiave completa come una password: la conservi in un gestore di segreti o in una\n  variabile d'ambiente, mai in codice versionato, in un file condiviso, in un ticket o in uno\n  screenshot.\n- Non incolli mai la chiave in una conversazione condivisa o conservata, anche con un assistente\n  IA. Un agente riceve la chiave tramite la sua configurazione (variabile d'ambiente, gestore di\n  segreti) e non chiede mai all'utente di digitarla nella conversazione.\n- La invii solo nell'header `Authorization`, in HTTPS, a `https://app.qrsuisse.ch/api/v1` — mai\n  in un URL. Non registri questo header nei log.\n- Dia a una chiave solo i diritti necessari: per esempio `QR_READ` e `STATS_READ`\n  (l'impostazione predefinita «Agente assistente IA» della dashboard) per un agente che si\n  limita a consultare. Con il piano Entreprise, usi una chiave per strumento, per poterne\n  revocare una senza interrompere le altre.\n- Se la chiave potrebbe essere stata esposta, ne esegua subito la rotazione o la revochi dalla\n  dashboard; se l'account è nel piano Gratuito, scriva a contact@qrsuisse.ch dall'indirizzo\n  e-mail di un amministratore dell'account. Un agente che sospetta che la sua chiave possa\n  essere stata esposta lo segnala al titolare dell'account.\n\n## Limitazione delle richieste (rate limiting)\n\nTre limiti indipendenti:\n- **Generico** — 30 richieste / 60 secondi per chiave API, applicato a **ogni** richiesta\n  autenticata sugli 8 endpoint.\n- **Creazione** — 8 richieste / 60 secondi per chiave API, applicato **in aggiunta** al\n  limite generico, solo su `POST /qr-codes`.\n- **Immagine PNG** — 10 rendering / 60 secondi **per account** (condiviso tra tutte le chiavi\n  dell'account), applicato **in aggiunta** al limite generico, solo su\n  `GET /qr-codes/{id}/image` in formato PNG. Né il formato SVG né una risposta `304` vengono\n  conteggiati.\n\nUn superamento restituisce `429 rate_limited` con un header `Retry-After` (secondi). I limiti\ndi una chiave vengono conteggiati non appena la chiave e il suo segreto sono riconosciuti,\nanche quando la richiesta viene poi rifiutata (`403 account_disabled`, `plan_required` o\n`forbidden`): una volta esaurito un limite, viene restituito `429 rate_limited`, prima di\nquesti rifiuti.\n\nLe richieste rifiutate con `401 unauthorized` sono inoltre limitate per indirizzo IP:\n60 rifiuti / 60 secondi per indirizzo IPv4 o per rete IPv6 /64, e 300 rifiuti / 60 secondi\nper rete IPv6 /48. Oltre questa soglia, l'API restituisce `429 rate_limited` (con\n`Retry-After`) senza verificare la chiave, anche se valida, fino alla fine della finestra.\n\n## Quota commerciale di creazione\n\n`POST /qr-codes` è inoltre soggetto a una quota mensile di creazioni: **5 creazioni per mese\ncivile UTC con il piano Pro, 100 con il piano Entreprise**, poi il saldo dei pacchetti di\ncreazioni API acquistati (acquisto unico, consumati dopo la quota mensile, dal più vecchio al\npiù recente). Ogni creazione riuscita viene conteggiata, anche se il codice QR viene\neliminato in seguito: un'eliminazione non restituisce alcuna creazione. La quota mensile non\nè riportabile e si azzera il 1° di ogni mese alle\n00:00 UTC. Quando la quota mensile e tutti i pacchetti disponibili sono esauriti, la\ncreazione restituisce `429 quota_exceeded` con la data di azzeramento della quota mensile\n(`resetAt`, ISO 8601) — blocco rigido, nessuna fatturazione a consumo nella v1.\n\n`GET /usage` indica il saldo in qualsiasi momento: creazioni del mese, quota mensile, crediti\nrimanenti nei pacchetti e data di azzeramento.\n\n## Formato d'errore comune\n\nOgni errore di questa API restituisce la stessa forma `{ \"error\": \"<code>\", \"message\":\n\"<testo>\", ...eventuali dettagli }`. 14 codici stabili: un codice esistente non cambia mai;\nuna versione minore può aggiungerne di nuovi, solo per una funzione nuova e facoltativa (vedi\n«Cronologia delle versioni»). I codici: `unauthorized` (401), `plan_required` (403), `forbidden` (403), `account_disabled` (403), `https_required`\n(403), `not_found` (404), `method_not_allowed` (405), `validation_error` (400),\n`quota_exceeded` (429), `rate_limited` (429), e per `POST /qr-codes` con l'header\n`Idempotency-Key`: `idempotency_key_mismatch` (422), `idempotency_in_progress` (409),\n`resource_deleted` (410), e per `POST /qr-codes` e `PATCH /qr-codes/{id}`: `payload_too_large`\n(413), corpo della richiesta superiore a 64 KiB.\nIl testo di `message` è un aiuto alla lettura, in francese come negli esempi di questa\nspecifica, e può cambiare senza preavviso. Basi il Suo codice sul codice `error`, mai su\n`message`.\nUn percorso URL che contiene una sequenza `%` non valida (es. `%zz`) viene rifiutato prima\ndi qualsiasi autenticazione con `400 validation_error`. Un percorso sconosciuto sotto\n`/api/v1/` restituisce `404 not_found`. Un metodo HTTP non supportato da un endpoint\nesistente restituisce `405 method_not_allowed`, con un header `Allow` che elenca i metodi\naccettati. Queste due risposte sono identiche con o senza chiave API. Una richiesta in HTTP\nriceve `403 https_required` prima di tutti questi controlli (vedi «Autenticazione»).\n\n## Indice pubblico\n\n`GET /api/v1`, senza autenticazione, restituisce un documento JSON statico: nome e versione\ndell'API, e link alla sua specifica OpenAPI, al suo catalogo (RFC 9727), alla sua\ndocumentazione e alla sua guida all'autenticazione. La specifica è pubblicata su\n`https://www.qrsuisse.ch/openapi.json` (e `/openapi.yaml`), in inglese per impostazione\npredefinita; `?lang=it`, `fr` o `de` ne restituisce la versione tradotta. L'indice è\nlimitato a 60 richieste / 60 secondi per indirizzo IPv4 o per rete IPv6 /64, e a 300\nrichieste / 60 secondi per rete IPv6 /48 (oltre, `429 rate_limited`).\n\nLa guida all'autenticazione, in inglese, è pubblicata su `https://www.qrsuisse.ch/auth.md`.\n\n## Buone pratiche\n\n- Invii una `Idempotency-Key` con ogni creazione (un nuovo valore casuale, UUID v4\n  consigliato, nessun dato personale) e invii di nuovo lo stesso valore se la richiesta\n  fallisce per un tempo scaduto o un'interruzione della connessione (vedi `POST /qr-codes`).\n- In caso di `429 rate_limited` o `409 idempotency_in_progress`, attenda il tempo indicato in\n  `Retry-After` prima di riprovare; in caso di `429 quota_exceeded`, non riprovi prima di\n  `resetAt` o dell'acquisto di un pacchetto (il saldo si legge con `GET /usage`).\n- Per scorrere a pagine un elenco filtrato o ordinato, invii di nuovo gli stessi parametri di\n  filtro e di ordinamento con `cursor`; solo `limit` può cambiare.\n- In una data con scostamento (`createdFrom`, `createdBefore`), codifichi il `+` in `%2B`, o\n  usi `Z`.\n- Rinvii l'`ETag` di un'immagine in `If-None-Match` per non scaricarla di nuovo, e preferisca\n  l'SVG per la stampa.\n- Gestisca gli errori in base al codice `error`, mai in base a `message`.\n\n## Cronologia delle versioni\n\nLa versione figura in `info.version` di questa specifica. All'interno della v1, una versione\nminore aggiunge solo funzioni facoltative: un'integrazione scritta per la v1.0 continua a\nfunzionare senza modifiche, con le riserve della rubrica «Cambiamenti di comportamento» qui\nsotto. Una versione correttiva (per esempio 1.1.1) corregge un difetto, senza funzioni nuove;\npuò rifiutare una richiesta che non era conforme a questa documentazione. Un'evoluzione\nincompatibile sarebbe pubblicata come v2, a un altro indirizzo.\n\n### 1.1.1 — ottobre 2026\n\nVersione correttiva, senza funzioni nuove.\n\nCambiamenti di comportamento:\n- Un corpo di `POST /qr-codes` o `PATCH /qr-codes/{id}` che non è UTF-8 valido (per esempio\n  testo codificato in ISO-8859-1) è rifiutato con `400 validation_error`. Prima, i byte non\n  validi erano sostituiti da «�» e registrati.\n- Due `DELETE` simultanei sullo stesso codice QR potevano ancora rispondere `500`, e un `PATCH`\n  su un codice QR eliminato nello stesso momento poteva rispondere `200` senza registrare\n  nulla, o `500`: la richiesta che trova il codice QR già eliminato risponde ora\n  `404 not_found`, come annunciato nella 1.1.0.\n\n### 1.1.0 — ottobre 2026\n\nNovità:\n- **Archiviare e riattivare** un codice QR con `PATCH /qr-codes/{id}` e il campo `active`\n  (`false` archivia, `true` riattiva), senza consumare una creazione. Diritto `QR_UPDATE`.\n- **Filtrare, cercare e ordinare l'elenco** `GET /qr-codes`: `active`, `type`, `q`\n  (denominazione, destinazione e fine dell'URL pubblico), `createdFrom`, `createdBefore`, `sort`.\n  Per la pagina successiva, invii di nuovo gli stessi parametri con `cursor`.\n- **Leggere la quota di creazioni** con `GET /usage`: creazioni del mese, quota mensile,\n  crediti dei pacchetti rimanenti, data di azzeramento. Diritto `QR_CREATE`.\n- **Ripetere una creazione senza duplicati** con l'header facoltativo `Idempotency-Key` su\n  `POST /qr-codes` (UUID v4 consigliato, nessun dato personale, 24 ore). Tre nuovi codici\n  d'errore, restituiti solo con questo header: `422 idempotency_key_mismatch`,\n  `409 idempotency_in_progress`, `410 resource_deleted`.\n\nCambiamenti di comportamento:\n- `GET /qr-codes` senza parametri ordina per data di creazione decrescente, poi per `id`\n  (prima: solo per `id` decrescente); in pratica l'ordine ottenuto è lo stesso.\n- `nextCursor` è ora un valore opaco. Un cursore non valido, ottenuto con altri parametri o da\n  un altro account è rifiutato con `400 validation_error`. Il vecchio cursore della v1.0\n  (l'`id` dell'ultimo elemento) resta accettato per tutta la v1, per l'elenco senza filtri né\n  ordinamento.\n- I parametri `active`, `type`, `q`, `createdFrom`, `createdBefore` e `sort` dell'elenco,\n  finora ignorati, sono ora applicati e convalidati: un valore non valido, vuoto o ripetuto è\n  rifiutato con `400 validation_error`.\n- Un campo `active` nel corpo di `PATCH /qr-codes/{id}`, finora ignorato, è ora applicato.\n- Un corpo della richiesta superiore a 64 KiB su `POST /qr-codes` o `PATCH /qr-codes/{id}` è\n  rifiutato con `413 payload_too_large` (nuovo codice; un corpo normale non si avvicina a\n  questo limite).\n- Una denominazione che contiene un carattere di controllo (carattere nullo, tabulazione, a\n  capo…) è rifiutata con `400 validation_error`.\n- Tutte le risposte portano `Cache-Control: no-store`, tranne l'immagine di un codice QR\n  (`private, no-cache` con `ETag`).\n- Un `PATCH` o un `DELETE` su un codice QR eliminato nello stesso momento da un'altra\n  richiesta risponde `404 not_found` invece di `500`.\n- Una stringa Unicode malformata (un surrogato isolato, per esempio `\\ud800`) nel corpo di\n  `POST /qr-codes` o `PATCH /qr-codes/{id}` è rifiutata con `400 validation_error` invece di\n  un errore `500`.\n\n### 1.0.0 — settembre 2026\n\nPrima versione pubblica: creazione, lettura, elenco, modifica ed eliminazione dei codici QR\n(tipi `URL` e `VCARD`), immagine PNG o SVG, statistiche di scansione.\n","contact":{"name":"QR Suisse"}},"servers":[{"url":"https://app.qrsuisse.ch/api/v1","description":"Produzione"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"QR Codes","x-displayName":"Codici QR","description":"Gestione dei codici QR dell'account proprietario della chiave API (creazione, ricerca, modifica, archiviazione, eliminazione), download della loro immagine e consultazione delle loro statistiche."},{"name":"Usage","x-displayName":"Quota di creazione","description":"Saldo della quota di creazioni API dell'account proprietario della chiave API."}],"paths":{"/qr-codes":{"get":{"operationId":"listQrCodes","tags":["QR Codes"],"summary":"Elenco dei codici QR dell'account","description":"Diritto richiesto: **`QR_READ`**. Nessuna quota (sola lettura).\n\nSenza parametri: tutti i codici QR dell'account, attivi e archiviati, dal più recente\n(data di creazione decrescente, poi `id`).\n\nFiltri facoltativi e combinabili: stato (`active`), tipo (`type`), ricerca (`q`), periodo\ndi creazione (`createdFrom`, `createdBefore`), ordinamento (`sort`). La ricerca riguarda\nla denominazione, la destinazione e la fine dell'URL pubblico (`slug`), mai i campi di un\nbiglietto da visita. Un valore non valido di uno di questi parametri è rifiutato con `400\nvalidation_error` (il messaggio indica il parametro), mai sostituito da un valore\npredefinito: un'integrazione non riceve mai, senza saperlo, un elenco diverso da quello\nrichiesto. Un parametro indicato due volte, o presente ma vuoto (`?active=`), è\nrifiutato allo stesso modo; fanno eccezione solo `cursor` vuoto (prima pagina) e `limit`.\nUn nome di parametro sconosciuto è ignorato.\n\nPaginazione tramite cursore opaco (`nextCursor`). Per la pagina successiva, invii di\nnuovo **gli stessi** parametri di filtro e di ordinamento, con `cursor`; solo `limit` può\ncambiare. Un cursore ottenuto con altri parametri, o da un altro account, è rifiutato con\n`400`. Il filtro `accountId` della chiave resta l'unica fonte di ciò che viene restituito.\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":"Pagina di risultati.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeListPage"}}}},"400":{"description":"Valore non valido di un parametro di filtro, di ordinamento o di cursore, parametro\nripetuto, o cursore ottenuto con altri parametri o il cui ultimo codice QR è stato\nnel frattempo eliminato o rinominato (ordinamento per denominazione). Il messaggio indica\nil parametro, mai il suo valore.\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":"Creazione di un codice QR","description":"Diritto richiesto: **`QR_CREATE`**. Soggetto alla quota commerciale di creazione (quota\nmensile del piano: 5 con Pro, 100 con Entreprise, poi saldo dei pacchetti di creazioni\nAPI) — verificata nella stessa transazione della scrittura, contro una creazione\nconcorrente vicina al limite. Una creazione resta conteggiata anche se il codice QR viene\neliminato in seguito.\n\n`type` è fissato alla creazione (`URL` o `VCARD`, discriminante del corpo della\nrichiesta) e non è **mai** modificabile in seguito. Per `VCARD`, `phones`/`emails`/\n`addresses` sono singolarmente facoltativi — se assenti, vengono trattati come array vuoti\n(un biglietto da visita minimo con nome e cognome è un caso d'uso valido).\n\nLa richiesta viene conteggiata nel limite generico **e** nel limite dedicato\n`api-create` (8/min, in aggiunta al limite generico di 30/min).\n\n**Nuovi tentativi senza duplicati.** Con l'header facoltativo `Idempotency-Key`,\nuna richiesta inviata di nuovo dopo un'interruzione di rete o un timeout non crea un\nsecondo codice QR e non consuma una seconda creazione della quota. Usi un nuovo valore\ncasuale per ogni creazione (UUID v4 consigliato) e non vi inserisca alcun dato personale.\nComportamento:\n- stesso valore e stesso corpo entro 24 ore: `201` con il codice QR già creato, nel suo\n  stato attuale, e l'header `Idempotent-Replayed: true`; nulla viene detratto\n  dalla quota. Questa ripetizione viene conteggiata nei limiti come una creazione;\n- stesso valore con un corpo diverso: `422 idempotency_key_mismatch`, nulla viene creato;\n- stesso valore mentre la prima richiesta è ancora in corso: la seconda attende\n  normalmente la sua fine e riceve lo stesso codice QR; se l'attesa è troppo lunga,\n  `409 idempotency_in_progress` con `Retry-After`;\n- il codice QR creato con questo valore è stato eliminato nel frattempo:\n  `410 resource_deleted`, nulla viene ricreato;\n- una richiesta rifiutata (`400`, `403`, `429`, errore del server) non viene registrata:\n  lo stesso valore può essere riutilizzato una volta risolto il problema;\n- dopo 24 ore il valore è dimenticato: una nuova richiesta con lo stesso valore crea un\n  nuovo codice QR e consuma una creazione.\n\nIl valore di `Idempotency-Key` è proprio di ogni chiave API: lo stesso valore inviato con un'altra chiave API\ncrea un altro codice QR. Vengono registrate solo impronte non reversibili del valore e\ndel corpo, con l'identificativo del codice QR creato, e prese in considerazione per 24 ore.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeCreateInput"}}}},"responses":{"201":{"description":"Codice QR creato, o creazione ripetuta con la stessa `Idempotency-Key`.","headers":{"Idempotent-Replayed":{"description":"`true` quando la risposta ripete una creazione già effettuata con la stessa\n`Idempotency-Key` (nessuna nuova creazione, nessuna quota consumata). Assente\naltrimenti.\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":"Dettaglio di un codice QR","description":"Diritto richiesto: **`QR_READ`**. Nessuna quota.\n","responses":{"200":{"description":"Codice QR trovato.","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":"Modifica parziale di un codice QR","description":"Diritto richiesto: **`QR_UPDATE`**. Nessuna quota di creazione: né una modifica né una\nriattivazione (`active: true`) consumano un credito.\n\n**Archiviazione e riattivazione**: `active: false` archivia il codice QR — il suo\nreindirizzamento pubblico risponde allora `404` («QR code introuvable ou désactivé.»),\nesattamente come dopo un'archiviazione dalla dashboard — e `active: true` lo riattiva. `active` deve essere\nun booleano JSON: `\"false\"`, `0` o `null` vengono rifiutati con `400 validation_error`. Se\nil corpo contiene solo `active` e il codice QR è già nello stato richiesto, la risposta è\n`200` senza alcuna scrittura (`updatedAt` invariato). `active` si combina con gli altri campi: l'intero corpo viene\nconvalidato prima di qualsiasi scrittura, poi applicato in un'unica volta — un errore\n`400` non applica nulla, né l'archiviazione né gli altri campi.\n\n`type` non è **mai** modificabile (una chiave `\"type\"` presente nel corpo viene ignorata\nsenza segnalazione, non è mai un errore `400`) — è sempre il tipo già registrato a\ndeterminare quali campi del corpo della richiesta qui sotto si applicano realmente. Allo\nstesso modo, i campi che non riguardano il tipo esistente della risorsa vengono ignorati\nsenza segnalazione (mai rifiutati).\n\nSemantica di un PATCH parziale: un campo assente dal corpo JSON lascia invariato il\nvalore attuale. Per `organization`/`jobTitle`/`website` (VCARD), un campo **presente**\ncon una stringa vuota cancella esplicitamente il valore (impostato a `null`) — conta la\nsua presenza nel corpo JSON, non solo il suo valore. Per `phones`/`emails`/`addresses`\n(VCARD), un array **presente** (anche vuoto) sostituisce l'intera categoria.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeUpdateInput"}}}},"responses":{"200":{"description":"Codice QR aggiornato.","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":"Eliminazione definitiva di un codice QR","description":"Diritto richiesto: **`QR_DELETE`**. Nessuna quota. Un'eliminazione non restituisce la\ncreazione alla quota mensile.\n\nEliminazione **reale** (non un'archiviazione): i telefoni, le e-mail, gli indirizzi e le\nstatistiche giornaliere del codice QR vengono eliminati con esso, e il suo slug viene\nritirato definitivamente nello stesso momento — un supporto già stampato con questo slug\nnon torna mai a puntare a un codice QR diverso creato in seguito.\n","responses":{"204":{"description":"Codice QR eliminato, nessun corpo di risposta."},"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":"Statistiche di scansione di un codice QR","description":"Diritto richiesto: **`STATS_READ`**. Nessuna quota.\n\n`dailyStats` può presentare lacune: solo i giorni con almeno una scansione hanno una riga\n(i giorni senza scansioni non vengono completati con 0, a differenza del grafico della\ndashboard) — chi integra l'API consuma dati grezzi, non una presentazione. Finestra\nlimitata alla cronologia dei piani Pro ed Entreprise (15 mesi) —\nl'accesso all'API stesso è possibile solo con un piano Pro o Entreprise.\n","responses":{"200":{"description":"Statistiche del codice QR.","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":"Immagine di un codice QR (PNG o SVG)","description":"Diritto richiesto: **`QR_READ`**. Nessuna quota.\n\nRestituisce il file immagine del codice QR, generato sul server con esattamente le\nstesse impostazioni della dashboard (nero su bianco, correzione d'errore di livello M,\nmargine di 4 moduli): un codice QR scaricato tramite l'API è identico a quello della\ndashboard. In questa versione non è configurabile alcuna opzione visiva (colore, logo,\nmargine).\n\nIl contenuto codificato è l'URL pubblico del codice QR (`publicUrl` di\n`GET /qr-codes/{id}`): sul dominio personalizzato dell'account se è attivo, altrimenti\nsul dominio standard. L'immagine viene fornita anche se il codice QR è disattivato\n(`active: false`).\n\nIl file viene restituito come allegato (`Content-Disposition: attachment`), con il nome\n`qr-suisse-<slug>.png` o `qr-suisse-<slug>.svg`. La risposta contiene un `ETag`: se ne\nrinvia il valore in `If-None-Match`, ottiene `304 Not Modified` senza corpo se\nl'immagine non è cambiata (la chiamata resta autenticata, conteggiata nel limite generico\ne registrata nel log, ma non nel limite dedicato ai rendering PNG).\n\nLimitazione delle richieste: oltre al limite generico (30 richieste / 60 s per chiave),\nogni rendering PNG viene conteggiato nel limite dedicato di **10 rendering / 60 s per\naccount**, condiviso tra tutte le chiavi dell'account. Il formato SVG non è interessato\nda questo limite dedicato.\nL'`ETag` cambia in particolare quando il dominio personalizzato dell'account viene\nattivato o disattivato.\n","parameters":[{"name":"format","in":"query","required":false,"description":"Formato dell'immagine, in minuscolo. Qualsiasi altro valore restituisce\n`400 validation_error` (mai un ripiego silenzioso su un altro formato).\n","schema":{"type":"string","enum":["png","svg"],"default":"png"}},{"name":"size","in":"query","required":false,"description":"Solo PNG: lato del quadrato in pixel, margine compreso, numero intero da 128 a 1024.\nUn valore non intero o fuori dai limiti restituisce `400 validation_error`. Ignorato\nper l'SVG (vettoriale, ridimensionabile senza perdita — il formato da preferire per la\nstampa di grande formato).\n","schema":{"type":"integer","minimum":128,"maximum":1024,"default":1024}},{"name":"If-None-Match","in":"header","required":false,"description":"Valore di `ETag` ottenuto in una chiamata precedente.","schema":{"type":"string"}}],"responses":{"200":{"description":"Immagine del codice QR.","headers":{"ETag":{"description":"Impronta dell'immagine (formato, dimensione e contenuto codificato).","schema":{"type":"string"}},"Content-Disposition":{"description":"`attachment; filename=\"qr-suisse-<slug>.<png|svg>\"`","schema":{"type":"string"}},"Cache-Control":{"description":"`private, no-cache` — risposta autenticata, mai memorizzata in una cache condivisa, riconvalidata a ogni utilizzo.","schema":{"type":"string"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string"}}}},"304":{"description":"L'immagine non è cambiata rispetto all'`ETag` fornito in `If-None-Match` — nessun corpo.","headers":{"ETag":{"description":"Impronta dell'immagine, identica a quella fornita.","schema":{"type":"string"}}}},"400":{"description":"Parametro `format` o `size` non valido.","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` — limite generico superato (30 richieste / 60 s per chiave API), troppe\nrichieste rifiutate con `401` dallo stesso indirizzo IP (60 / 60 s, o 300 / 60 s per\nrete IPv6 /48) o, per il formato PNG, limite dedicato ai rendering superato\n(10 rendering / 60 s per account).\n","headers":{"Retry-After":{"description":"Stima in secondi prima che la finestra corrente si liberi.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}}}}},"/usage":{"get":{"operationId":"getUsage","tags":["Usage"],"summary":"Ottenere la quota di creazioni API dell'account","description":"Diritto richiesto: **`QR_CREATE`**. Nessuna quota: questa lettura non consuma alcuna\ncreazione.\n\nRestituisce, per l'account proprietario della chiave, le stesse cifre mostrate nello\nstesso momento dalla schermata «Chiavi API» della dashboard: creazioni API del mese\ncivile UTC in corso, quota mensile del piano, crediti rimanenti nei pacchetti di\ncreazioni API e data di azzeramento della quota mensile. Una creazione riuscita aumenta\n`used` di 1; un'eliminazione non lo fa diminuire. Oltre la quota mensile, ogni creazione\nconsuma anche un credito di pacchetto.\n\nValori indicativi: una creazione effettuata nello stesso momento con un'altra chiave\ndell'account può renderli inesatti già al momento della lettura. Solo `POST /qr-codes`\nverifica la quota in modo certo (vedi «Quota commerciale di creazione»).\n","responses":{"200":{"description":"Quota di creazioni API dell'account.","headers":{"Cache-Control":{"description":"`no-store` — la risposta cambia a ogni creazione, non viene mai memorizzata in cache.","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":"Token opaco in due parti, **non un JWT**: `qrs_live_<apiKeyId>.<secret>`, trasmesso in\n`Authorization: Bearer <token>`.\n"}},"parameters":{"QrCodeIdParam":{"name":"id","in":"path","required":true,"description":"Identificativo (`cuid`) del codice QR.","schema":{"type":"string"}},"LimitParam":{"name":"limit","in":"query","required":false,"description":"Numero massimo di elementi per pagina. Valore predefinito: 20. Un valore assente, non intero o\n≤ 0 ricade silenziosamente sul valore predefinito (parametro di comodità, mai un errore\n`400`); un valore superiore a 100 viene limitato a 100.\n","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"CursorParam":{"name":"cursor","in":"query","required":false,"description":"Valore di `nextCursor` ricevuto con la pagina precedente, da rinviare così com'è con gli\nstessi parametri di filtro e di ordinamento. Valore opaco: non ne deduca nulla, il suo\nformato può cambiare. Assente per la prima pagina. Un cursore non valido, ottenuto con\naltri parametri o da un altro account è rifiutato con `400`. Il vecchio cursore della\nv1.0 (l'`id` dell'ultimo elemento) resta accettato per tutta la v1, per l'elenco senza\nfiltri né ordinamento.\n","schema":{"type":"string","maxLength":512}},"ActiveFilterParam":{"name":"active","in":"query","required":false,"description":"`true`: solo i codici QR attivi; `false`: solo quelli archiviati. Assente: entrambi.\nEsattamente `true` o `false`, in minuscolo.\n","schema":{"type":"string","enum":["true","false"]}},"TypeFilterParam":{"name":"type","in":"query","required":false,"description":"Uno o più tipi separati da virgole (al massimo 8 valori, maiuscole o minuscole): `url`\n(tutti i link, `type` `URL`), `web` (pagina web), `email` (destinazione `mailto:`),\n`phone` (`tel:`), `sms` (`sms:`), `ftp` (`ftp:`), `calendar` (evento di calendario),\n`vcard` (biglietto da visita). Un codice QR è restituito se è di uno dei tipi indicati.\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":"Ricerca nella denominazione, nella destinazione e nella fine dell'URL pubblico (`slug`), senza\ndistinzione tra maiuscole e minuscole né di accenti («geneve» trova «Genève»). Mai nei\ncampi di un biglietto da visita. Da 1 a 100 caratteri dopo la rimozione degli spazi\niniziali e finali, senza caratteri di controllo; `%` e `_` sono cercati letteralmente.\n","schema":{"type":"string","minLength":1,"maxLength":100}},"CreatedFromParam":{"name":"createdFrom","in":"query","required":false,"description":"Codici QR creati a partire da questa data, inclusa. ISO 8601: `AAAA-MM-GG` (mezzanotte\nUTC) o data e ora con `Z` o uno scostamento. Nell'URL, codifichi il `+` di uno\nscostamento in `%2B` (`createdFrom=2026-10-01T08:00:00%2B02:00`) o usi `Z`\n(`createdFrom=2026-10-01T06:00:00Z`); un `+` non codificato, ricevuto come spazio, è\naccettato anche subito prima dello scostamento. Anno dal 1970 al 9999, calcolato in UTC\ndopo l'applicazione dello scostamento; una data inesistente (`2026-02-30`) o fuori da\nquesto intervallo è rifiutata con `400 validation_error`.\n","schema":{"type":"string"},"example":"2026-10-01"},"CreatedBeforeParam":{"name":"createdBefore","in":"query","required":false,"description":"Codici QR creati prima di questa data, esclusa (stesso formato di `createdFrom`). Con\n`createdFrom=2026-10-01&createdBefore=2026-11-01`: tutto il mese di ottobre (UTC).\n`createdFrom` deve essere anteriore a `createdBefore`.\n","schema":{"type":"string"},"example":"2026-11-01"},"SortParam":{"name":"sort","in":"query","required":false,"description":"Ordine dell'elenco: `created_desc` (predefinito, dal più recente), `created_asc`,\n`label_asc` (denominazione dalla A alla Z), `label_desc`. A parità decide `id`. Con\n`label_asc` o `label_desc`, se l'ultimo codice QR di una pagina viene eliminato o\nrinominato prima della chiamata successiva, la pagina successiva risponde `400`, invece di\nomettere in silenzio dei codici QR (ricominci la paginazione dalla prima\npagina): per una sincronizzazione completa, preferisca `created_desc` o `created_asc`.\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":"Chiave scelta dal client per una creazione logica, che permette di inviare di nuovo la\nstessa richiesta senza creare un duplicato (vedi `POST /qr-codes`). Da 1 a 255 caratteri\nASCII visibili, senza spazi; le virgolette che racchiudono il valore vengono ignorate.\nUUID v4 consigliato, nessun dato personale. Un valore non valido restituisce\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":"Fissato alla creazione, mai modificabile in seguito."},"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":"Numero di telefono così come inserito (nessun formato normalizzato imposto)."}},"description":"Al massimo 3 numeri di telefono per codice QR VCARD."},"EmailEntry":{"type":"object","required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/EmailType"},"value":{"type":"string","format":"email","maxLength":255}},"description":"Al massimo 2 indirizzi e-mail per codice QR VCARD."},"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":"Al massimo 2 indirizzi postali per codice QR VCARD. I 4 campi sono\nsingolarmente facoltativi (nessuno è obbligatorio per una voce valida).\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":"Presente solo in `QrCodeApiDto.vcard` per `type === \"VCARD\"`. `organization`/\n`jobTitle`/`website` sono assenti dalla risposta se non sono compilati (mai un `null`\nesplicito) — chi integra l'API verifica la presenza della chiave, non il suo valore.\n"},"QrCodeApiDto":{"type":"object","required":["id","type","label","active","scanCount","slug","publicUrl","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"Identificativo (`cuid`) del codice QR."},"type":{"$ref":"#/components/schemas/QrCodeType"},"label":{"type":"string"},"active":{"type":"boolean"},"scanCount":{"type":"integer","description":"Numero totale di scansioni dalla creazione."},"slug":{"type":"string"},"publicUrl":{"type":"string","format":"uri","description":"URL pubblico completo di reindirizzamento (dominio personalizzato dell'account se è\nattivo, altrimenti dominio standard) — mai memorizzato così com'è, sempre ricalcolato\ndallo stato attuale dell'account.\n"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"destinationUrl":{"type":"string","format":"uri","description":"Presente solo per `type === \"URL\"`, e solo se compilato."},"vcard":{"allOf":[{"$ref":"#/components/schemas/VCardData"}],"description":"Presente solo per `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":"Rappresentazione pubblica di un codice QR: vengono restituiti solo i campi seguenti,\nnessun dato interno dell'account.\n"},"QrCodeListPage":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/QrCodeApiDto"}},"nextCursor":{"type":["string","null"],"description":"Cursore opaco da passare in `?cursor`, con gli stessi parametri di filtro e di\nordinamento, per ottenere la pagina successiva; `null` se è l'ultima pagina.\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":"URL di destinazione — protocolli accettati: `http:`/`https:` (senza ulteriori\nrestrizioni), `mailto:` (parte locale prima di un eventuale `?` convalidata come\nindirizzo e-mail), `tel:`/`sms:` (solo cifre/spazi/`+`/`-`/parentesi, da 4 a 20\ncaratteri, `*`/`#` esplicitamente vietati — rischio di codice USSD), `ftp:`\n(credenziali in chiaro nell'authority vietate). Nessun carattere di controllo\n(letterale o codificato come `%XX`).\n"}},"example":{"type":"URL","label":"Affiche salle d'attente","destinationUrl":"https://www.exemple.ch/rendez-vous"},"description":"Corpo di `POST /qr-codes` per un codice QR di tipo `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":"Corpo di `POST /qr-codes` per un codice QR di tipo `VCARD`. Solo `label`/`firstName`/\n`lastName` sono obbligatori — un biglietto da visita minimo (solo un nome) è un caso d'uso valido;\n`phones`/`emails`/`addresses` assenti equivalgono a un array vuoto.\n"},"QrCodeCreateInput":{"oneOf":[{"$ref":"#/components/schemas/QrCodeCreateUrlInput"},{"$ref":"#/components/schemas/QrCodeCreateVcardInput"}],"discriminator":{"propertyName":"type","mapping":{"URL":"#/components/schemas/QrCodeCreateUrlInput","VCARD":"#/components/schemas/QrCodeCreateVcardInput"}},"description":"Unione discriminata da `type`, fissato alla creazione — stesse regole di convalida della\ndashboard (riutilizzate così come sono, nessuno schema parallelo).\n"},"QrCodeUpdateInput":{"type":"object","properties":{"active":{"type":"boolean","description":"`false` archivia il codice QR, `true` lo riattiva. Booleano JSON rigoroso, valido per entrambi i tipi."},"label":{"type":"string","minLength":1,"maxLength":255},"destinationUrl":{"type":"string","maxLength":2048,"description":"Applicabile solo se la risorsa esistente è di tipo `URL` — vedi la descrizione dell'operazione."},"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255,"description":"Una stringa vuota, se il campo è presente, cancella esplicitamente il valore (`null`)."},"jobTitle":{"type":"string","maxLength":255,"description":"Una stringa vuota, se il campo è presente, cancella esplicitamente il valore (`null`)."},"website":{"type":"string","maxLength":255,"description":"Una stringa vuota, se il campo è presente, cancella esplicitamente il valore (`null`)."},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3,"description":"Se presente (anche vuoto), sostituisce l'intera categoria."},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2,"description":"Se presente (anche vuoto), sostituisce l'intera categoria."},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2,"description":"Se presente (anche vuoto), sostituisce l'intera categoria."}},"example":{"destinationUrl":"https://www.exemple.ch/nouvelle-page"},"description":"Corpo di `PATCH /qr-codes/{id}` — **tutti i campi sono facoltativi** (semantica di\nmodifica parziale: un campo assente lascia invariato il valore attuale). `type` non\ncompare **mai** in questo corpo (non modificabile, vedi la descrizione dell'operazione):\nè il tipo già registrato a determinare quale dei campi si applica realmente,\n`destinationUrl` (`URL`) o `firstName`/`lastName`/`organization`/`jobTitle`/`website`/\n`phones`/`emails`/`addresses` (`VCARD`) — i campi che non riguardano il tipo esistente\nvengono ignorati senza segnalazione, mai rifiutati.\n"},"StatsApiDto":{"type":"object","required":["qrCodeId","scanCountTotal","dailyStats"],"properties":{"qrCodeId":{"type":"string"},"scanCountTotal":{"type":"integer","description":"Totale delle scansioni dalla creazione (`QrCode.scanCount`), non solo nella finestra di `dailyStats`."},"dailyStats":{"type":"array","items":{"type":"object","required":["date","count"],"properties":{"date":{"type":"string","format":"date-time"},"count":{"type":"integer"}}},"description":"Un elemento per ogni giorno con almeno una scansione (nessun completamento con 0),\nlimitato alla finestra di cronologia dei piani Pro ed Entreprise (15 mesi).\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":"DTO pubblico dell'endpoint delle statistiche."},"UsageApiDto":{"type":"object","required":["used","monthlyQuota","packCreditsRemaining","resetAt"],"additionalProperties":false,"properties":{"used":{"type":"integer","minimum":0,"description":"Creazioni API del mese civile UTC in corso, comprese quelle che hanno consumato un\ncredito di pacchetto e quelle di codici QR eliminati nel frattempo. Può quindi\nsuperare `monthlyQuota`.\n"},"monthlyQuota":{"type":"integer","minimum":0,"description":"Creazioni incluse ogni mese nel piano (5 con Pro, 100 con Entreprise)."},"packCreditsRemaining":{"type":"integer","minimum":0,"description":"Crediti ancora disponibili nei pacchetti di creazioni API dell'account, tutti i pacchetti utilizzabili insieme."},"resetAt":{"type":"string","format":"date-time","description":"Azzeramento della quota mensile — 1° giorno del mese civile UTC successivo, a mezzanotte UTC."}},"example":{"used":7,"monthlyQuota":5,"packCreditsRemaining":98,"resetAt":"2026-11-01T00:00:00.000Z"},"description":"DTO pubblico dell'endpoint quota — solo questi quattro campi."},"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":"Forma comune a ogni errore dell'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":"Account sospeso o in attesa di eliminazione. A seconda dello stato, `message` (in\nfrancese) vale:\n- sospeso: « Ce compte a été suspendu. Contactez le support QR Suisse à l'adresse\n  contact@qrsuisse.ch pour plus d'informations. » (Questo account è stato sospeso.\n  Contatti l'assistenza QR Suisse all'indirizzo contact@qrsuisse.ch per maggiori\n  informazioni.)\n- in attesa di eliminazione: « 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. » (Questo account è\n  in attesa di eliminazione per inattività. Utilizzi il link di recupero ricevuto per\n  e-mail per riattivarlo.)\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":"Richiesta ricevuta in HTTP: né reindirizzata né elaborata, prima di qualsiasi verifica\ndella chiave. Una chiave inviata in questo modo ha viaggiato in chiaro e deve essere\nruotata.\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` riprende il primo messaggio di convalida riscontrato (es. un campo obbligatorio\nassente o troppo lungo) — il testo esatto varia a seconda del campo interessato, la forma `{ error, message }`\nresta sempre la stessa. Questo testo è in francese; un messaggio generico indica\nil campo interessato (es. «Champ « type » requis : valeurs acceptées URL, VCARD.»): solo il\ncodice `error` è garantito.\n"},"QuotaExceededError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["resetAt"],"properties":{"resetAt":{"type":"string","format":"date-time","description":"1° giorno del mese civile UTC successivo, a mezzanotte UTC — data di azzeramento della quota."}}}],"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` assente o malformato, chiave sconosciuta/revocata/congelata, o segreto errato.","headers":{"WWW-Authenticate":{"description":"Sempre `Bearer realm=\"qrsuisse-api\"`, qualunque sia il motivo del rifiuto.","schema":{"type":"string","const":"Bearer realm=\"qrsuisse-api\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}},"ForbiddenOrPlanRequired":{"description":"`403` — la richiesta è stata inviata in HTTP (`https_required`, prima di qualsiasi\nverifica della chiave), oppure l'account è sospeso o in attesa di eliminazione\n(`account_disabled`, verificato per primo per una chiave valida), oppure l'account non è\npiù su un piano Pro o Entreprise (`plan_required`), oppure la chiave API autenticata non\nha il diritto atomico richiesto per questa operazione (`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":"Risorsa inesistente, o appartenente a un altro account (mai distinta da una semplice assenza).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"ValidationError":{"description":"Corpo della richiesta assente, non JSON, o non conforme alle regole di convalida del campo interessato.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"RateLimited":{"description":"Limite generico superato (30 richieste / 60 s per chiave API), o troppe richieste\nrifiutate con `401` dallo stesso indirizzo IP (60 / 60 s, o 300 / 60 s per rete IPv6\n/48).\n","headers":{"Retry-After":{"description":"Stima in secondi prima che la finestra corrente si liberi.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}},"RateLimitedOrQuotaExceeded":{"description":"`429` — è stato superato un limite di richieste (generico 30/min, dedicato alla creazione\n8/min, o 60 rifiuti `401`/min dallo stesso indirizzo IP, 300/min per rete IPv6 /48 —\n`rate_limited`), oppure è stata raggiunta la quota mensile commerciale di creazioni API\n(`quota_exceeded`, prima di qualsiasi scrittura nel database).\n","headers":{"Retry-After":{"description":"Presente solo per `rate_limited` — stima in secondi prima che la finestra corrente si liberi.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/QuotaExceededError"}]}}}},"IdempotencyKeyMismatch":{"description":"`422` — la stessa `Idempotency-Key` è già stata usata, nelle ultime 24 ore, per una\nrichiesta con un corpo diverso. Nulla viene creato, nessuna quota viene consumata.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyKeyMismatchError"}}}},"IdempotencyInProgress":{"description":"`409` — una richiesta con la stessa `Idempotency-Key` è ancora in corso e l'attesa del\nsuo risultato è stata troppo lunga. Questa richiesta non crea nulla: la invii di nuovo\ndopo `Retry-After`: riceverà il codice QR creato dalla prima.\n","headers":{"Retry-After":{"description":"Attesa consigliata prima di un nuovo tentativo, in secondi.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyInProgressError"}}}},"ResourceDeleted":{"description":"`410` — il codice QR creato con questa `Idempotency-Key` è stato eliminato nel frattempo.\nNulla viene ricreato: usi una nuova chiave per una nuova creazione.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceDeletedError"}}}},"PayloadTooLarge":{"description":"`413` — corpo della richiesta superiore a 64 KiB (65 536 byte): la richiesta viene\nrifiutata, nulla viene scritto.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayloadTooLargeError"}}}}}}}