Skip to content

Members without a fiscal code

View as Markdown

Joining an association does not take an Italian fiscal code (codice fiscale). What it takes is that the person is identified, and on Vimine they are in one of two ways:

  • by a fiscal code, or
  • by their particulars: dob + birthplace + birth_country (ISO 3166-1 alpha-2).

Never neither. The rest of this guide is what changes, field by field, for an integration that enrols people — exchange students, volunteers who have just arrived, members who live abroad.

POST /v1/enroll and POST /v1/signup take the same fields:

Field
codicefiscale optional. When present it is validated and cross-checked against the names, the date and the place of birth
dob date of birth, YYYY-MM-DD
birthplace the comune for a person born in Italy; the city, as free text, for a person born abroad
birth_country country of birth, two letters (IT, FR, SN…)
gender demographic only: it is no longer cross-checked against the code
// No fiscal code: date, place and country of birth
await vimineServer.enroll({
email: 'marie.dupont@example.org',
first_name: 'Marie',
last_name: 'Dupont',
dob: '1985-03-10',
birthplace: 'Lyon',
birth_country: 'FR',
accept_statuto: { version: statuto.version, sha256: statuto.sha256 }
});

On /v1/enroll, without a code, each missing particular is a 422 validation_failed with an entry in errors[]:

{ "code": "validation_failed", "errors": [{ "field": "birth_country", "code": "required" }] }

/v1/signup creates the account only: the particulars stay optional there, and will be needed when the person asks for a membership.

The fiscal code of a person born in Italy holds the comune of birth; that of a person born abroad holds the state only. So for them the cross-check compares birth_country and treats birthplace — the city — as free text. Sending birth_country is what makes a person born abroad with a valid code pass: without it, “Lyon” would be looked up among the Italian comuni and the code would come out inconsistent. (A form that so far sent the Italian name of the state as the place of birth — birthplace: 'Francia' — keeps passing; birth_country is the form to prefer.)

The same holds for the public check, which now also says what the code holds:

const r = await publicApi().validateFiscalCode({
code: 'DPNMRA85C50Z110U', first_name: 'Marie', last_name: 'Dupont', date: '1985-03-10', place: 'Lyon', country: 'FR'
});
// { valid: true, error: null, mismatches: [], hints: [], birth_date: '1985-03-10', birth_country: 'FR' }

birth_date and birth_country are read from the code itself: you can use them to prefill the form. This endpoint still compares gender, when you send it; enrolment and the profile do not.

A code that disagrees, and the confirmation

Section titled “A code that disagrees, and the confirmation”

A mistyped code (structure or check character) is invalid_codicefiscale: it has to be corrected, it can never be confirmed. A well-formed code that disagrees with the data entered answers 422 with a richer entry instead:

{
"code": "validation_failed",
"errors": [{
"field": "codicefiscale",
"code": "codicefiscale_mismatch",
"mismatches": ["place"],
"hints": [],
"confirmable": true
}]
}
  • mismatches — what disagrees: lastName, firstName, date, place;
  • hints — names_swapped when the code matches once surname and given name are exchanged: offering the swap is usually enough;
  • confirmable — whether the person may confirm that this is the code on their health card. It does happen: comuni that no longer exist, transliterations, names registered in another order.

When confirmable is true, ask the person and repeat the request with codicefiscale_confirmed: true. When both names and the date all disagree, nothing ties that code to that person any more: confirmable is false and the only way forward is to correct it.

import { VimineError } from '@vimine/sdk/server';
try {
await vimineServer.enroll(input);
} catch (err) {
const entry = err instanceof VimineError && err.errors?.find((e) => e.code === 'codicefiscale_mismatch');
if (!entry) throw err;
if (entry.confirmable && (await askThePerson(entry.mismatches, entry.hints))) {
await vimineServer.enroll({ ...input, codicefiscale_confirmed: true });
}
}

Do not send codicefiscale_confirmed on your own initiative: it is a statement of the person, and it is recorded as one.

Someone who joined without a code can add it once they have one, from their own profile:

const me = await vimine.me.update({ codicefiscale: 'DPNMRA85C50Z110U' });
me.codicefiscale_check; // 'matched'

PATCH /v1/me accepts birth_country and the write-only codicefiscale_confirmed; codicefiscale may also be set back to null. GET /v1/me says how the person is identified:

codicefiscale_check
matched a fiscal code that agrees with the person’s particulars
mismatch_confirmed a code that disagrees, confirmed by the person
absent no code: date, place and country of birth identify them
null not identified yet: they cannot ask for a membership

matched means consistent, not verified with the tax registry: a fiscal code stays a declared datum.

An account may exist without being identified — someone who registered only to sign in, for example. When such a person asks for a membership, the API says so and names what is missing:

  • POST /v1/me/membership → 422 identification_required, with errors[] = the missing particulars ({ field: 'birthplace', code: 'required' }…). Send the person to complete their profile with PATCH /v1/me (permission profile:write) and repeat the request.
  • POST /v1/enroll/confirm — the “confirm your request” link the owner of an existing account receives → 409 identification_required, same errors[]. Ask for the particulars on the page and post the token again with identification:
await publicApi().confirmEnrollment(token, { dob: '1985-03-10', birthplace: 'Lyon', birth_country: 'FR' });
// POST /v1/enroll/confirm { token, identification: { dob, birthplace, birth_country, codicefiscale?, codicefiscale_confirmed? } }

The particulars travel in the request body, never in the link: nothing personal is ever in the link. identification only completes the empty fields of the profile, it does not overwrite what is there; if it does not hold, the answer is a 422 validation_failed as above.

Admission does not change: pending_reasons are whatever the association’s admission model says, for everybody (see Admission and activation).

A fiscal code that agrees with the particulars can be checked against them; the particulars alone, or a code the person confirmed, cannot. For these members the association looks at a document once, the first time the person is there. Whoever integrates a scanner sees it in POST /v1/checkins (permission checkins:write), in the answers for a card:

const r = await vimineServer.checkins.create('vmt_…');
// { kind: 'card', verdict: 'valid', …, identity_check: 'pending' }
if (r.identity_check === 'pending') show('First entry: ask for a document');
identity_check
pending the member has no verifiable fiscal code and nobody of the association has looked at a document yet: show it to whoever staffs the gate
verified somebody of the association has looked at a document
flagged somebody looked at one and it did not agree: the association’s office takes it from there
null nothing to check

It is a note, it is not part of verdict and it restricts nothing: a valid card with identity_check: 'pending' goes in. The outcome is recorded from the association’s console, not from the API. The public verification GET /v1/verify/{card_token} never carries it: whoever checks a card from outside sees status and expiry only.

Some associations keep their members’ address in their register, others do not. The choice is the association’s and holds for every member or for nobody; the public profile declares it:

GET https://api.vimine.network/v1/aps/{slug}
→ { "aps": { …, "member_address": "optional" } }
member_address
none do not ask: an address sent anyway is not stored
optional ask, without making it mandatory
required without an address enrolment answers 422 with { field: 'address', code: 'required' }
await vimineServer.enroll({
…,
address: { kind: 'residence', line: 'Via Roma 1', postal_code: '20121', city: 'Milano', province: 'MI', country: 'IT' }
});

line and city are required inside address; kind is residence (the default) or domicile, where it differs from the residence; country is IT when omitted; province concerns Italian addresses only. /v1/signup has no address: the address belongs to the membership, not to the account.

There is no “fiscal code required” setting. An association cannot demand a fiscal code from every applicant: whoever has none is identified by their particulars, everywhere. If your form makes it mandatory, that is a choice of your form — and it leaves out people the association could welcome.

All additive: a form that always sends a fiscal code keeps working untouched. What an existing integration may notice:

  • codicefiscale is no longer required in POST /v1/enroll; without a code, dob, birthplace and birth_country are needed (422 required on each missing one);
  • new optional fields birth_country, codicefiscale_confirmed (enroll, signup, PATCH /v1/me) and address (enroll);
  • gender is no longer cross-checked against the code: a 422 about the sex alone does not come any more;
  • a code that disagrees still answers codicefiscale_mismatch, now with hints and confirmable next to mismatches; a malformed code stays invalid_codicefiscale;
  • GET/PATCH /v1/me now have a declared schema (Me), with birth_country and codicefiscale_check;
  • a new error identification_required: 422 on POST /v1/me/membership, 409 on POST /v1/enroll/confirm (which accepts identification in the body);
  • POST /v1/validate/fiscal-code accepts country and also answers hints, birth_date, birth_country; error is invalid, mismatch or null;
  • POST /v1/checkins adds identity_check to the answers for a card; GET /v1/verify/{card_token} stays as it was;
  • the public profile adds aps.member_address.