# 嵌入與會員卡核驗

> 您網站上的入會表單、協會的公開資料，以及線上和離線的會員卡核驗。

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

## 您網站上的入會表單

每個開放了公開入會的協會都有一個頁面 `https://app.vimine.network/enroll/<slug>`，可以放進協會網站的 iframe 中：

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

協會必須在自己的設定中，把該網站的網域列入**允許嵌入（iframe）的網域**。而“使用 Vimine 登入”的登入和授權同意頁面則**不能**放進 iframe：它始終是 vimine.network 上的一個完整頁面。

`?locale=en` 會以英文開啟表單（`<vimine-enroll>` 元素會根據頁面的語言自動傳入該參數）。

### 表單的外觀

表單可以採用您網站的配色。iframe 中的頁面看不到外層頁面的 CSS，因此樣式要透過網址傳遞：

| 參數 | 取值 | |
|---|---|---|
| `scheme` | `light`、`dark` | 頁面的明暗模式；不指定時，跟隨背景或訪客的設定 |
| `accent` | 十六進位顏色（`1e3a5f`） | 按鈕、連結、獲得焦點的欄位以及協會名稱 |
| `background` | 十六進位顏色，或 `transparent` | 卡片後面的底色；`transparent` 會透出您的頁面 |
| `font` | `inter`、`roboto`、`open-sans`、`montserrat`、`nunito`、`lora`、`merriweather`、`roboto-slab`、`system` | 整個頁面（包括欄位）的字型，從 Vimine 自行託管的字型中選擇（`system`：裝置內建的字型） |

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

其他取值會被忽略。使用 `<vimine-enroll>` 時無需編寫網址：在您的 CSS 中設定 `--vimine-scheme`、`--vimine-accent`、`--vimine-background` 和 `--vimine-typeface` 即可（[元素](https://developers.vimine.network/zh-hant/sdk/elements/)）。使用 `transparent` 時，請選擇在您的頁面上清晰可讀的明暗模式：表單的卡片有自己的底色，卡片之外的文字則直接落在您的頁面上。

### 已有 Vimine 帳戶的人

一個身份適用於所有協會：已有帳戶的人無需再次填寫表單。直接開啟時，頁面 `…/enroll/<slug>` 會識別工作階段，並只顯示**一個頁面**——*本*協會的會員卡類型、章程和隱私權聲明，以及本協會額外要求的內容（地址，如果協會聲明了要收集地址）。章程和隱私權聲明每次都需要接受：任何內容都不會從一個協會帶到另一個協會。

在 iframe 中無法讀取工作階段：瀏覽器會把嵌入框架中的頁面與直接開啟的同一網站隔離開來。因此，嵌入的表單會提供“已有 Vimine 帳戶？前往 Vimine 繼續”，在新分頁中開啟該頁面。如果您手寫 iframe 並使用了 `sandbox`，請加上 `allow-popups` 和 `allow-popups-to-escape-sandbox`，否則那個分頁仍會被困在沙箱中；`<vimine-enroll>` 已經這樣做了。

## 公開資料

如果您想建構自己的體驗，`GET /v1/aps/{slug}` 會傳回協會的公開資料——品牌形象、會員卡類型及其會費、現行章程：

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

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

無需任何憑證。之後，在會員已透過“使用 Vimine 登入”登入的頁面上，您就可以收集入會申請：

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

await vimine.membership.request({
    membership_type,
    accept_statuto: { version: statuto.version },
    // 協會的隱私權聲明，即您的頁面所展示的那一份
    privacy_notice: privacy_notice && { version: privacy_notice.version, sha256: privacy_notice.sha256, locale: privacy_notice.locale },
    // 地址，在協會收集地址時傳入（aps.member_address: 'optional' | 'required'）
    address: { line: 'Via Roma 1', postal_code: '20100', city: 'Milano', province: 'MI' }
});
```

協會對申請人的要求，與申請從哪個入口提交無關：

- **隱私權聲明**：只要協會有一份當事人尚未閱讀的現行隱私權聲明，`privacy_notice` 就是必填的：缺少時，`privacy_notice` 欄位上傳回 `422 validation_failed`；版本已不再現行時，傳回 `422 privacy_notice_outdated`。`vimine.legal.acceptances()` 會告訴您還有哪些需要確認；文字在 `GET /v1/aps/{slug}/privacy-notice`。閱讀確認會作為一條與該會員資格關聯的證明記錄儲存。
- **地址**：`aps.member_address` 說明協會是否收集地址：為 `required` 時，不帶 `address` 的申請會在 `address` 欄位上傳回 `422 validation_failed`；為 `none` 時，地址會被忽略。
- **當事人的身份**：當個人資料尚未識別身份時——既沒有稅號，也沒有出生日期、出生地和出生國——申請傳回 `422 identification_required`：請用 `vimine.me.update(...)` 完善個人資料後重試（參見[沒有義大利稅號的會員](https://developers.vimine.network/zh-hant/guides/members-without-cf/#identification_required)）。

:::caution[2026 年 10 月 2 日起的變化]
在 API 的 `2026-09-27` 版本之前，此請求既不要求隱私權聲明，也不要求地址。如果某個整合在協會有現行隱私權聲明的情況下呼叫它而不帶 `privacy_notice`，現在會收到 `422`。
:::

## 會員卡核驗

數位會員卡是一個**簽名權杖**（`vmt_…`），會員以 QR Code 的形式出示。有兩種核驗方式。

### 線上

無需憑證，回應中不含任何個人資料：

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

`not_yet_valid`：會員卡是真實的，會員資格也已生效，但機構設定了從獲准入會到會員卡可以使用之間的間隔。`valid_from` 說明它從何時開始算數；在此之前，在入場時它是一張不算數的會員卡。權杖中（`nbf`）也帶有同一時刻，因此離線掃描器自己就能得出相同的結果。

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

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

### 離線

權杖使用 **Ed25519** 簽名；公開金鑰位於 `https://api.vimine.network/.well-known/vimine-card-keys.json`（也可透過 `GET /v1/card-keys` 或 `publicApi().cardKeys()` 取得）。掃描器使用快取的公開金鑰核驗**三件事**：簽名、有效時段（`nbf`/`exp`，最長 90 天，之後權杖會輪換），以及酬載中的狀態為有效——**`s === "active"`**。網路恢復後再線上重新同步。遇到未知的 `kid`，說明金鑰已輪換：請重新載入公開金鑰。

權杖在每次狀態變化、每次續期時都會輪換，而且無論如何每 90 天輪換一次：一張舊截圖不可能永遠有效。

只有**有效**的會員資格才有會員卡：申請仍在等待（入會審核或會費）期間，以及會員資格被暫停或撤銷時，API 不簽發任何權杖——`GET /v1/me/membership` 中的 `card` 為 `null`，而 `GET /v1/me/membership/card`、會員卡圖片、電子郵件和 Google Wallet 連結都傳回 `409 not_active`。檢查 `s` 仍是必須的：它會拒絕在此規則之前簽發的權杖，以及任何未宣告有效狀態的權杖。也請遵守 `nbf`：機構可以決定會員卡在獲准入會幾小時後才可使用，這一時刻就記錄在那裡。

:::caution
離線核驗只能說明權杖在簽發時**真實且處於有效時段內**，並不能說明會員卡在此刻仍然有效：之後發生的暫停只有線上才能看到。請儘快重新同步。
:::

## 稅號

稅號是**可選的**：沒有稅號的人可以憑出生日期、出生地和出生國入會（參見[沒有義大利稅號的會員](https://developers.vimine.network/zh-hant/guides/members-without-cf/)）。有稅號時，`POST /v1/validate/fiscal-code`（或 `publicApi().validateFiscalCode(...)`）會檢查義大利稅號（codice fiscale）的結構和檢查碼；如果您同時傳入個人資訊，還會檢查它們與稅號是否一致——並說明稅號本身包含什麼：

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

`error` 為 `invalid`（結構或檢查碼有誤）、`mismatch`（格式正確但與所傳送資訊不一致的稅號）或 `null`；當姓和名互換後稅號能對上時，`hints` 中包含 `names_swapped`。對於在國外出生的人，請傳送 `country`（ISO 3166-1 alpha-2）：他們的稅號中只包含國家，`place`（城市）不參與比對。
