# Embed e verifica tessere

> Il modulo di tesseramento nel tuo sito, il profilo pubblico dell'associazione e la verifica delle tessere online e offline.

URL: https://developers.vimine.network/guides/embed-and-verify/

## Il modulo di tesseramento nel tuo sito

Ogni associazione che ha attivato il tesseramento pubblico ha una pagina
`https://app.vimine.network/enroll/<slug>` che può essere inserita in un iframe nel sito dell'associazione:

```html
<iframe src="https://app.vimine.network/enroll/tuo-slug" style="width:100%;min-height:900px;border:0"></iframe>
```

L'associazione deve elencare il dominio del sito tra i **domini di incorporamento** nelle proprie impostazioni.
La pagina di accesso e consenso di *Accedi con Vimine* invece **non può** essere inserita in un iframe: è
sempre una pagina a tutto schermo su vimine.network.

`?locale=en` apre il modulo in inglese (l'elemento `<vimine-enroll>` lo passa da solo, dalla lingua della pagina).

### L'aspetto del modulo

Il modulo può prendere i colori del tuo sito. Una pagina in un iframe non vede il CSS di chi la ospita, quindi lo stile
viaggia nell'indirizzo:

| Parametro | Valori | |
|---|---|---|
| `scheme` | `light`, `dark` | lo schema della pagina; senza, quello dello sfondo o del visitatore |
| `accent` | un colore esadecimale (`1e3a5f`) | pulsanti, link, campi attivi e il nome dell'associazione |
| `background` | un colore esadecimale, o `transparent` | lo sfondo dietro le schede; `transparent` lascia vedere la tua pagina |
| `font` | `inter`, `roboto`, `open-sans`, `montserrat`, `nunito`, `lora`, `merriweather`, `roboto-slab`, `system` | il carattere di tutta la pagina, campi compresi, tra quelli che Vimine serve da sé (`system`: quello del dispositivo) |

```html
<iframe src="https://app.vimine.network/enroll/tuo-slug?scheme=light&accent=1e3a5f&background=transparent"
        style="width:100%;min-height:900px;border:0"></iframe>
```

Altri valori sono ignorati. Con `<vimine-enroll>` non serve scrivere l'indirizzo: imposti `--vimine-scheme`,
`--vimine-accent`, `--vimine-background` e `--vimine-typeface` nel tuo CSS ([elementi](https://developers.vimine.network/sdk/elements/)). Con `transparent` scegli lo schema
che si legge sulla tua pagina: le schede del modulo hanno il loro fondo, il testo fuori dalle schede sta sul tuo.

### Chi ha già un account Vimine

Un'identità vale per tutte le associazioni: chi ha già un account non deve ricompilare il modulo. Aperta direttamente,
la pagina `…/enroll/<slug>` riconosce la sessione e mostra **una sola schermata** — il tipo di tessera, lo statuto e
l'informativa privacy di *questa* associazione, e soltanto ciò che questa associazione chiede in più (l'indirizzo, se
ha dichiarato di raccoglierlo). Statuto e informativa si accettano ogni volta: nulla passa da un'associazione all'altra.

Dentro un iframe la sessione non è leggibile: il browser tiene separata una pagina incorniciata dallo stesso sito aperto
direttamente. Per questo il modulo incorniciato offre *Hai già un account Vimine? Continua su Vimine*, che apre la
pagina in una nuova scheda. Se scrivi l'iframe a mano e usi `sandbox`, includi `allow-popups` e
`allow-popups-to-escape-sandbox`, altrimenti quella scheda resta chiusa nella sandbox; `<vimine-enroll>` lo fa già.

## Il profilo pubblico

`GET /v1/aps/{slug}` restituisce il profilo pubblico dell'associazione — grafica, tipi di tessera con le quote,
statuto in vigore — se vuoi costruire un'esperienza tua:

```js
import { publicApi } from '@vimine/sdk';

const { aps, membership_types, statuto } = await publicApi().association('tuo-slug');
```

Nessuna credenziale. Da una pagina in cui il socio ha fatto *Accedi con Vimine* puoi poi raccogliere la
richiesta:

```js
const { aps, membership_types, statuto, privacy_notice } = await publicApi().association('tuo-slug');

await vimine.membership.request({
    membership_type,
    accept_statuto: { version: statuto.version },
    // l'informativa privacy dell'associazione, che la tua pagina ha mostrato
    privacy_notice: privacy_notice && { version: privacy_notice.version, sha256: privacy_notice.sha256, locale: privacy_notice.locale },
    // l'indirizzo, se l'associazione lo raccoglie (aps.member_address: 'optional' | 'required')
    address: { line: 'Via Roma 1', postal_code: '20100', city: 'Milano', province: 'MI' }
});
```

Quello che un'associazione chiede a chi aderisce non dipende dalla porta da cui arriva la richiesta:

- **l'informativa privacy.** Finché l'associazione ne ha una in vigore che la persona non ha ancora visto,
  `privacy_notice` è obbligatorio: senza, `422 validation_failed` sul campo `privacy_notice`; con una versione non più
  in vigore, `422 privacy_notice_outdated`. `vimine.legal.acceptances()` dice cosa è ancora da confermare; il testo è
  `GET /v1/aps/{slug}/privacy-notice`. La presa visione resta come riga di prova legata a questa tessera.
- **l'indirizzo.** `aps.member_address` dice se l'associazione lo raccoglie: con `required`, senza `address` la
  richiesta risponde `422 validation_failed` sul campo `address`; con `none` viene ignorato.
- **chi è la persona.** Se il profilo non è ancora identificato — né un codice fiscale, né data, luogo e Stato di
  nascita — la richiesta risponde `422 identification_required`: completa il profilo con `vimine.me.update(...)` e
  ripeti (vedi [Soci senza codice fiscale](https://developers.vimine.network/guides/members-without-cf/#identification_required)).

:::caution[Cambiato il 2 ottobre 2026]
Fino alla versione `2026-09-27` dell'API questa richiesta non chiedeva né l'informativa né l'indirizzo. Un'integrazione
che la chiama senza `privacy_notice` per un'associazione che ha un'informativa in vigore ora riceve `422`.
:::

## Verifica tessere

La tessera digitale è un **token firmato** (`vmt_…`) che il socio mostra come QR. È verificabile in due modi.

### Online

Nessuna credenziale, nessun dato personale nella risposta:

```
GET https://api.vimine.network/v1/verify/vmt_…
→ { "status": "active", "valid_until": "2027-03-31T22:59:59.000Z" }
   oppure
→ { "status": "inactive", "reason": "expired" }
   oppure
→ { "status": "inactive", "reason": "not_yet_valid", "valid_from": "2026-09-19T19:00:00.000Z" }
```

`not_yet_valid`: la tessera è autentica e l'adesione è attiva, ma l'ente ha stabilito un intervallo tra l'ammissione e
l'utilizzabilità della tessera. `valid_from` dice da quando vale; fino ad allora, alla porta, è una tessera che non
conta. Lo stesso istante è nel token (`nbf`), quindi uno scanner offline arriva alla stessa risposta da solo.

```js
import { publicApi } from '@vimine/sdk';

const { status } = await publicApi().verifyCard(tokenLettoDalQr);
```

### Offline

Il token è firmato con **Ed25519**; le chiavi pubbliche stanno su
`https://api.vimine.network/.well-known/vimine-card-keys.json` (anche `GET /v1/card-keys`, o
`publicApi().cardKeys()`). Uno scanner verifica **tre cose**, con la chiave in cache: la firma, la finestra di validità (`nbf`/`exp`, al massimo
90 giorni, poi il token ruota) e che lo stato nel payload sia attivo — **`s === "active"`**. Poi si riallinea online
quando torna la rete. Un `kid` sconosciuto
significa che la chiave è ruotata: ricarica le chiavi.

Il token ruota a ogni cambio di stato, a ogni rinnovo e comunque ogni 90 giorni: uno screenshot vecchio non
apre nulla per sempre.

Una tessera esiste solo per un'adesione **attiva**: finché la domanda è in attesa (di ammissione o della quota), e
quando l'adesione è sospesa o revocata, l'API non rilascia alcun token — `card` è `null` in `GET /v1/me/membership`,
e `GET /v1/me/membership/card`, l'immagine, l'e-mail e il link a Google Wallet rispondono `409 not_active`. Il
controllo di `s` resta obbligatorio: rifiuta i token emessi prima di questa regola e qualunque token che non dichiari
uno stato attivo. Rispetta anche `nbf`: un ente può stabilire che la tessera diventi utilizzabile qualche ora dopo
l'ammissione, e quell'istante viaggia lì.

:::caution
La verifica offline dice che il token era **autentico e nella sua finestra** quando è stato emesso, non che la
tessera è attiva in questo istante: una sospensione avvenuta dopo si vede solo online. Riallinea appena puoi.
:::

## Codice fiscale

Il codice fiscale è **facoltativo**: chi non ne ha uno si iscrive con data, luogo e Stato di nascita (vedi
[Soci senza codice fiscale](https://developers.vimine.network/guides/members-without-cf/)). Quando c'è, `POST /v1/validate/fiscal-code` (o
`publicApi().validateFiscalCode(...)`) controlla struttura e carattere di controllo e, se passi anche i dati
anagrafici, la loro coerenza con il codice — e dice che cosa il codice stesso contiene:

```js
const r = await publicApi().validateFiscalCode({ code: 'RSSMRA80A01H501U', first_name: 'Mario', last_name: 'Rossi', date: '1980-01-01', place: 'Roma', country: 'IT' });
// { valid: true, error: null, mismatches: [], hints: [], birth_date: '1980-01-01', birth_country: 'IT' }
//   oppure
// { valid: false, error: 'mismatch', mismatches: ['date'], hints: [], birth_date: '1980-01-01', birth_country: 'IT' }
```

`error` vale `invalid` (struttura o carattere di controllo), `mismatch` (un codice ben formato che non concorda con i
dati inviati) o `null`; `hints` contiene `names_swapped` quando il codice torna scambiando nome e cognome. Per chi è
nato all'estero invia `country` (ISO 3166-1 alpha-2): il suo codice contiene solo lo Stato, e `place` — la città — non
viene confrontato.
