# Integración y verificación de carnés

> El formulario de inscripción en tu sitio, el perfil público de la asociación y la verificación de carnés en línea y sin conexión.

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

## El formulario de inscripción en tu sitio

Cada asociación que ha activado la inscripción pública tiene una página `https://app.vimine.network/enroll/<slug>`
que se puede colocar en un iframe en el sitio de la asociación:

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

La asociación debe incluir el dominio del sitio entre sus **dominios de integración** en su configuración. En cambio,
la página de inicio de sesión y de consentimiento de *Iniciar sesión con Vimine* **no** se puede colocar en un
iframe: siempre es una página completa en vimine.network.

`?locale=en` abre el formulario en inglés (el elemento `<vimine-enroll>` lo pasa por sí solo, según el idioma de la
página).

### El aspecto del formulario

El formulario puede adoptar los colores de tu sitio. Una página en un iframe no ve el CSS de la página que la aloja,
así que el estilo viaja en la dirección:

| Parámetro | Valores | |
|---|---|---|
| `scheme` | `light`, `dark` | el esquema de la página; sin él, el del fondo o el del visitante |
| `accent` | un color hexadecimal (`1e3a5f`) | botones, enlaces, campos con el foco y el nombre de la asociación |
| `background` | un color hexadecimal, o `transparent` | el fondo detrás de las tarjetas; `transparent` deja ver tu página |
| `font` | `inter`, `roboto`, `open-sans`, `montserrat`, `nunito`, `lora`, `merriweather`, `roboto-slab`, `system` | el tipo de letra de toda la página, campos incluidos, entre los que sirve el propio Vimine (`system`: el del dispositivo) |

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

Los demás valores se ignoran. Con `<vimine-enroll>` no hay ninguna dirección que escribir: define `--vimine-scheme`,
`--vimine-accent`, `--vimine-background` y `--vimine-typeface` en tu CSS ([elementos](https://developers.vimine.network/es/sdk/elements/)). Con
`transparent`, elige el esquema que se lea bien sobre tu página: las tarjetas del formulario tienen su propio fondo,
el texto de fuera se apoya sobre el tuyo.

### Quien ya tiene una cuenta de Vimine

Una sola identidad sirve para todas las asociaciones: quien ya tiene una cuenta no necesita rellenar de nuevo el
formulario. Abierta directamente, la página `…/enroll/<slug>` reconoce la sesión y muestra **una sola pantalla** — el
tipo de carné, el estatuto y la política de privacidad de *esta* asociación, y solo lo que esta asociación pide
además (la dirección, si ha declarado que la recoge). El estatuto y la política se aceptan cada vez: nada pasa de
una asociación a otra.

Dentro de un iframe la sesión no se puede leer: un navegador mantiene una página enmarcada separada del mismo sitio
abierto directamente. Por eso el formulario enmarcado ofrece *¿Ya tienes una cuenta de Vimine? Continuar en Vimine*,
que abre la página en una pestaña nueva. Si escribes el iframe a mano y usas `sandbox`, incluye `allow-popups` y
`allow-popups-to-escape-sandbox`, o esa pestaña se queda dentro del sandbox; `<vimine-enroll>` ya lo hace.

## El perfil público

`GET /v1/aps/{slug}` devuelve el perfil público de la asociación — imagen, tipos de carné con sus cuotas, estatuto
en vigor — por si quieres construir una experiencia propia:

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

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

Sin credenciales. Desde una página en la que el socio ha hecho *Iniciar sesión con Vimine* puedes luego recoger la
solicitud:

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

await vimine.membership.request({
    membership_type,
    accept_statuto: { version: statuto.version },
    // la política de privacidad de la asociación, que tu página ha mostrado
    privacy_notice: privacy_notice && { version: privacy_notice.version, sha256: privacy_notice.sha256, locale: privacy_notice.locale },
    // la dirección, si la asociación la recoge (aps.member_address: 'optional' | 'required')
    address: { line: 'Via Roma 1', postal_code: '20100', city: 'Milano', province: 'MI' }
});
```

Lo que una asociación pide a un solicitante no depende de la puerta por la que llegue la solicitud:

- **la política de privacidad.** Mientras la asociación tenga una en vigor que la persona aún no ha visto,
  `privacy_notice` es obligatorio: sin él, `422 validation_failed` en el campo `privacy_notice`; con una versión que
  ya no está en vigor, `422 privacy_notice_outdated`. `vimine.legal.acceptances()` dice lo que queda por confirmar; el
  texto está en `GET /v1/aps/{slug}/privacy-notice`. El acuse de recibo se guarda como una fila de prueba vinculada a
  esta inscripción.
- **la dirección.** `aps.member_address` dice si la asociación la recoge: con `required`, una solicitud sin `address`
  responde `422 validation_failed` en el campo `address`; con `none`, se ignora.
- **quién es la persona.** Cuando el perfil aún no está identificado — ni código fiscal, ni fecha, lugar y país de
  nacimiento — la solicitud responde `422 identification_required`: completa el perfil con `vimine.me.update(...)` y
  repite (consulta [Socios sin código fiscal](https://developers.vimine.network/es/guides/members-without-cf/#identification_required)).

:::caution[Cambiado el 2 de octubre de 2026]
Hasta la versión `2026-09-27` de la API, esta solicitud no pedía ni la política ni la dirección. Una integración que
la llama sin `privacy_notice` para una asociación con una política en vigor recibe ahora `422`.
:::

## Verificación de carnés

El carné digital es un **token firmado** (`vmt_…`) que el socio muestra como código QR. Se puede verificar de dos
maneras.

### En línea

Sin credenciales, sin datos personales en la respuesta:

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

`not_yet_valid`: el carné es auténtico y la inscripción está activa, pero la entidad ha fijado un intervalo entre la
admisión y el momento en que el carné se puede usar. `valid_from` dice cuándo empieza a valer; hasta entonces, en la
entrada, es un carné que no vale. El mismo instante está en el token (`nbf`), así que un escáner sin conexión llega
solo a la misma respuesta.

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

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

### Sin conexión

El token está firmado con **Ed25519**; las claves públicas están en
`https://api.vimine.network/.well-known/vimine-card-keys.json` (también `GET /v1/card-keys`, o
`publicApi().cardKeys()`). Un escáner verifica **tres cosas** con la clave en caché: la firma, la ventana de validez
(`nbf`/`exp`, como máximo 90 días, después el token rota) y que el estado del payload sea activo —
**`s === "active"`**. Después se vuelve a sincronizar en línea cuando vuelve la red. Un `kid` desconocido significa
que la clave ha rotado: vuelve a cargar las claves.

El token rota con cada cambio de estado, con cada renovación y, en cualquier caso, cada 90 días: una captura de
pantalla antigua no abre nada para siempre.

Un carné solo existe para una inscripción **activa**: mientras la solicitud está pendiente (admisión o cuota), y
cuando la inscripción está suspendida o revocada, la API no emite ningún token — `card` es `null` en
`GET /v1/me/membership`, y `GET /v1/me/membership/card`, la imagen, el correo y el enlace de Google Wallet responden
`409 not_active`. Comprobar `s` sigue siendo obligatorio: rechaza los tokens emitidos antes de esta regla y cualquier
token que no declare un estado activo. Respeta también `nbf`: una entidad puede decidir que un carné se pueda usar
algunas horas después de la admisión, y ese instante viaja ahí.

:::caution
La verificación sin conexión dice que el token era **auténtico y estaba dentro de su ventana** cuando se emitió, no
que el carné esté activo en este preciso instante: una suspensión posterior solo se ve en línea. Vuelve a
sincronizar en cuanto puedas.
:::

## Código fiscal

El código fiscal es **opcional**: quien no lo tiene se inscribe con su fecha, lugar y país de nacimiento (consulta
[Socios sin código fiscal](https://developers.vimine.network/es/guides/members-without-cf/)). Cuando lo hay, `POST /v1/validate/fiscal-code` (o
`publicApi().validateFiscalCode(...)`) comprueba la estructura y el carácter de control de un *codice fiscale*
italiano y, si pasas también los datos personales, su coherencia con el código — y dice lo que contiene el propio
código:

```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' }
//   o bien
// { valid: false, error: 'mismatch', mismatches: ['date'], hints: [], birth_date: '1980-01-01', birth_country: 'IT' }
```

`error` es `invalid` (estructura o carácter de control), `mismatch` (un código bien formado que no coincide con los
datos enviados) o `null`; `hints` contiene `names_swapped` cuando el código coincide al intercambiar apellidos y
nombre. Para una persona nacida en el extranjero envía `country` (ISO 3166-1 alpha-2): su código solo contiene el
país, y `place` — la ciudad — no se compara.
