# Members without a fiscal code

> A person is identified by a fiscal code or by their date, place and country of birth. What to send for people born abroad, how a code that disagrees is confirmed, what to show at the gate.

URL: https://developers.vimine.network/en/guides/members-without-cf/

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.

## Identifying a person

`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 |

```js
// 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[]`:

```json
{ "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.

## Born abroad: send `birth_country`

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:

```js
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

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:

```json
{
  "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.

```js
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.

## Adding the code later

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

```js
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.

## `identification_required`

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`:

```js
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](https://developers.vimine.network/en/guides/admission/)).

## At the gate: the identity check

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:

```js
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.

## The address

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' }` |

```js
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.

## An association cannot demand the code

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.

## What changed (version 2026-09-19)

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`.
