Embed and card verification
The enrolment form on your site
Section titled “The enrolment form on your site”Every association that enabled public enrolment has a page https://app.vimine.network/enroll/<slug>
that can be placed in an iframe on the association’s site:
<iframe src="https://app.vimine.network/enroll/your-slug" style="width:100%;min-height:900px;border:0"></iframe>The association must list the site’s domain among its embed domains in its settings. The sign-in and consent page of Sign in with Vimine, on the other hand, cannot be placed in an iframe: it is always a full-page on vimine.network.
?locale=en opens the form in English (the <vimine-enroll> element passes it by itself, from the page’s language).
The form’s look
Section titled “The form’s look”The form can take your site’s colours. A page in an iframe cannot see its host’s CSS, so the style travels in the address:
| Parameter | Values | |
|---|---|---|
scheme |
light, dark |
the page’s scheme; without it, the background’s or the visitor’s |
accent |
a hex colour (1e3a5f) |
buttons, links, focused fields and the association’s name |
background |
a hex colour, or transparent |
the ground behind the cards; transparent lets your page show through |
font |
inter, roboto, open-sans, montserrat, nunito, lora, merriweather, roboto-slab, system |
the face of the whole page, fields included, among those Vimine serves itself (system: the device’s own) |
<iframe src="https://app.vimine.network/enroll/your-slug?scheme=light&accent=1e3a5f&background=transparent" style="width:100%;min-height:900px;border:0"></iframe>Other values are ignored. With <vimine-enroll> there is no address to write: set --vimine-scheme, --vimine-accent,
--vimine-background and --vimine-typeface in your CSS (elements). With transparent, pick the scheme that reads on
your page: the form’s cards have their own fill, the text outside them sits on yours.
Someone who already has a Vimine account
Section titled “Someone who already has a Vimine account”One identity serves every association: someone who already has an account need not fill the form again. Opened
directly, the page …/enroll/<slug> recognises the session and shows one screen — the membership type, the statuto
and the privacy notice of this association, and only what this association asks on top of that (the address, where it
declared that it collects one). The statuto and the notice are accepted every time: nothing passes from one
association to another.
Inside an iframe the session cannot be read: a browser keeps a framed page apart from the same site opened directly.
So the framed form offers Already have a Vimine account? Continue on Vimine, which opens the page in a new tab. If you
write the iframe by hand and use sandbox, include allow-popups and allow-popups-to-escape-sandbox, or that tab
stays inside the sandbox; <vimine-enroll> already does.
The public profile
Section titled “The public profile”GET /v1/aps/{slug} returns the association’s public profile — branding, membership types with their fees,
in-force statuto — if you want to build an experience of your own:
import { publicApi } from '@vimine/sdk';
const { aps, membership_types, statuto } = await publicApi().association('your-slug');No credentials. From a page where the member did Sign in with Vimine you can then collect the request:
const { aps, membership_types, statuto, privacy_notice } = await publicApi().association('your-slug');
await vimine.membership.request({ membership_type, accept_statuto: { version: statuto.version }, // the association's privacy notice, which your page showed privacy_notice: privacy_notice && { version: privacy_notice.version, sha256: privacy_notice.sha256, locale: privacy_notice.locale }, // the address, where the association collects one (aps.member_address: 'optional' | 'required') address: { line: 'Via Roma 1', postal_code: '20100', city: 'Milano', province: 'MI' }});What an association asks of an applicant does not depend on the door the request comes through:
- the privacy notice. While the association has one in force that the person has not seen yet,
privacy_noticeis required: without it,422 validation_failedon theprivacy_noticefield; with a version that is no longer in force,422 privacy_notice_outdated.vimine.legal.acceptances()says what is still to confirm; the text isGET /v1/aps/{slug}/privacy-notice. The acknowledgement is kept as a proof row tied to this membership. - the address.
aps.member_addresssays whether the association collects it: withrequired, a request withoutaddressanswers422 validation_failedon theaddressfield; withnoneit is ignored. - who the person is. When the profile is not identified yet — neither a fiscal code, nor date, place and country of
birth — the request answers
422 identification_required: complete the profile withvimine.me.update(...)and repeat (see Members without a fiscal code).
Card verification
Section titled “Card verification”The digital card is a signed token (vmt_…) the member shows as a QR code. It can be verified in two ways.
Online
Section titled “Online”No credentials, no personal data in the response:
GET https://api.vimine.network/v1/verify/vmt_…→ { "status": "active", "valid_until": "2027-03-31T22:59:59.000Z" } or→ { "status": "inactive", "reason": "expired" } or→ { "status": "inactive", "reason": "not_yet_valid", "valid_from": "2026-09-19T19:00:00.000Z" }not_yet_valid: the card is genuine and the membership is active, but the organisation set an interval between
admission and the card being usable. valid_from says when it starts to count; until then, at the door, it is a card
that does not count. The same instant is in the token (nbf), so an offline scanner reaches the same answer alone.
import { publicApi } from '@vimine/sdk';
const { status } = await publicApi().verifyCard(tokenReadFromQr);Offline
Section titled “Offline”The token is signed with Ed25519; the public keys are at
https://api.vimine.network/.well-known/vimine-card-keys.json (also GET /v1/card-keys, or
publicApi().cardKeys()). A scanner verifies three things with the cached key: the signature, the validity window (nbf/exp, at most 90
days, then the token rotates) and that the status in the payload is active — s === "active". Then it re-syncs
online when the network is back. An unknown kid
means the key rotated: reload the keys.
The token rotates on every status change, on every renewal and every 90 days anyway: an old screenshot does not open anything forever.
A card exists only for an active membership: while the request is pending (admission or fee), and when the
membership is suspended or revoked, the API issues no token — card is null in GET /v1/me/membership, and
GET /v1/me/membership/card, the image, the e-mail and the Google Wallet link answer 409 not_active. Checking s
stays mandatory: it rejects tokens issued before this rule and any token that does not declare an active status.
Honour nbf too: an organisation may decide that a card becomes usable some hours after admission, and that instant
travels there.
Fiscal code
Section titled “Fiscal code”The fiscal code is optional: someone who has none joins with their date, place and country of birth (see
Members without a fiscal code). When there is one, POST /v1/validate/fiscal-code
(or publicApi().validateFiscalCode(...)) checks the structure and the check character of an Italian codice fiscale
and, if you also pass the personal data, their consistency with the code — and says what the code itself holds:
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' }// or// { valid: false, error: 'mismatch', mismatches: ['date'], hints: [], birth_date: '1980-01-01', birth_country: 'IT' }error is invalid (structure or check character), mismatch (a well-formed code that disagrees with the data
sent) or null; hints holds names_swapped when the code matches once surname and given name are exchanged. For a
person born abroad send country (ISO 3166-1 alpha-2): their code holds the state only, and place — the city — is
not compared.