Issuer · 發行憑證
這頁是給 Issuer 角色:簽出並交付憑證的組織。從你的後端視角,發行其實只是一支 API 呼叫 — POST /v1/offers。其後(fetch offer、換 code、組 proof JWT、取 SD-JWT)都在錢包端完成;保管模式下由 TCS Holder Service 替你做。
你做什麼 vs. TCS 做什麼
Section titled “你做什麼 vs. TCS 做什麼”你要實作的兩步:
- 建立 offer。 你的後端把 credential payload POST 到 Credential Issuer。TCS 回 offer URI 與 deep link。
- 交付 offer。
- 保管模式: 把
credential_offer_uri直接傳給 Holder Service 的POST /v1/oid4vci/receive。 - 非保管模式: 把
deep_link渲染成 QR code,或以推播 / email 送給 end user 的錢包 App。
- 保管模式: 把
兩種模式下你都不需要自己跑 OID4VCI 的 token / nonce / proof / credential 那套 — 保管模式由 Holder Service 做,非保管模式由 user 錢包做(wire-level 細節見非保管模式)。
Step 1 · 建立 credential offer
Section titled “Step 1 · 建立 credential offer”用你的 Issuer API key 對 Credential Issuer 打。Offer 帶著憑證型別、claims、與 flow 設定。
curl -X POST https://issuer.turingspace.co/v1/offers \ -H "Content-Type: application/json" \ -H "X-API-Key: tcs_production_9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e" \ -d '{ "credential": { "config_id": "TuringCerts_Standard_Credential_v2_sd_jwt", "claims": { "credentialName": "University Degree", "issuedTime": "2026-03-26T00:00:00Z" } }, "flow": "pre-authorized", "expires_in": 3600 }'回應(201 Created):
{ "offer_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credential_offer_uri": "https://issuer.turingspace.co/v1/offers/a1b2c3d4-e5f6-7890-abcd-ef1234567890", "deep_link": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.turingspace.co%2Fv1%2Foffers%2Fa1b2c3d4-e5f6-7890-abcd-ef1234567890", "expires_at": "2026-03-26T01:00:00Z"}| 名稱 | 型別 | 必填 | 說明 |
|---|---|---|---|
credential.config_id | string | 是 | Issuer metadata credential_configurations_supported map 的 key。可用 GET /.well-known/openid-credential-issuer/:tenant 查詢可用 ID。 |
credential.claims | object | 是 | 要包含的憑證 claims(鍵值對) |
flow | string | 是 | 發行流程型別。生產環境目前僅支援 "pre-authorized"。"authorization-code" 在 roadmap 上 —— 見 標準 Roadmap。 |
expires_in | number | 否 | Offer 存活秒數(預設 3600) |
issuer_did | string | 否 | 為這張 offer 覆寫預設 issuer DID。必須與 issuer_private_key 一起提供 —— 缺一補一會回 400。 |
holder_did | string | 否 | 把 offer 綁定到特定 holder DID。 |
issuer_private_key | string | 否 | 覆寫預設 issuer 簽章金鑰。必須與 issuer_did 一起提供 —— 缺一補一會回 400。 |
notarize_on_iota | boolean | 否 | 是否把這張憑證錨點到 IOTA。issuer_did 為 did:iota:* 時預設 true,其餘 false。明確設 true 時 issuer_did 必須是 did:iota:*。見 鏈上 notarization。 |
callback_url | string | 否 | 鏈上 notarization 完成後 TCS 會 POST 的 HTTPS URL。單次交付、不重試。notarize_on_iota=false 時仍會儲存但永不觸發。最長 2048 字元。見 Step 3 · 追蹤發行狀態。 |
| 狀態 | 原因 |
|---|---|
| 400 | Body 驗證失敗(缺欄位、claims 格式錯、config_id 不存在);亦於 issuer_did 與 issuer_private_key 未同時提供 / 同時省略時回傳 |
| 401 | 缺 X-API-Key 或 key 無效 |
| 403 | API key 沒被授權使用該 tenant 或 schema;或所提供的 issuer_did 未註冊於呼叫方租戶下 |
Step 2 · 交付 offer
Section titled “Step 2 · 交付 offer”credential_offer_uri 怎麼交付,看你的模式。
直接把 URI 交給 Holder Service。後端已經握有該 user 的 access token 與 DID 私鑰(見 Holder · 管理錢包與憑證 § 1)。
curl -X POST https://holder.turingspace.co/v1/oid4vci/receive \ -H "Authorization: Bearer ${HOLDER_USER_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "credential_offer_uri": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.turingspace.co%2Fv1%2Foffers%2Fa1b2c3d4-...", "holder_did": "did:iota:testnet:0x7a8b...", "private_key": "${HOLDER_PRIVATE_KEY}" }'一次 round trip 就把憑證放進 Holder Service 該 user 的錢包。完整流程見 Holder · 管理錢包與憑證 § 2。
把 deep_link 渲染成 QR code、email、或推播。剩下的 OID4VCI 由 user 的錢包 App 直接跟 TCS 跑(見非保管模式)。
Step 3 · 追蹤發行狀態
Section titled “Step 3 · 追蹤發行狀態”Offer 交付之後,有兩種方式可以得知憑證實際發出並完成鏈上錨點:一是 TCS 在鏈上 notarization 完成後主動 POST 的 webhook,二是隨時可查的 polling 端點。
實務上把 webhook 當主訊號、polling 當 fallback。Webhook 為單次交付 —— production 程式碼應在 webhook 未於可接受時間內抵達時,改用 polling 補上。
Webhook(推送)
Section titled “Webhook(推送)”在 POST /v1/offers 帶 callback_url。當該張憑證的鏈上錨點建立完成,TCS 會對該 URL POST 一份 JSON。
Payload:
{ "event": "credential.notarized", "offer_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credential_id": "8f7d2c3a-9b1e-4f3a-b7c6-1a2b3c4d5e6f", "object_id": "0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b", "blockchain": "iota", "notarized_at": "2026-05-12T08:30:00.000Z"}交付語意:
- HTTP
POST、Content-Type: application/json。 - 單次交付、不重試。Receiver 應回
2xx;非2xx會被 TCS 記下,但不會再送。 - Timeout 預設 5 秒。
- 不 follow redirects。
- 只在憑證實際完成鏈上錨點時觸發 —— 即
notarize_on_iota=true且 issuer 走did:iota路徑。X.509 issuer 與notarize_on_iota=false的 offer 不會觸發(見 鏈上 notarization)。
Polling(拉取)
Section titled “Polling(拉取)”GET /v1/offers/{offerId}/credentials 回該 offer 已發出的憑證。在 webhook 不可用(X.509 路徑、notarize_on_iota=false)、或 webhook 未到、或想隨時做一次同步狀態查詢時使用。
curl https://issuer.turingspace.co/v1/offers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/credentials \ -H "X-API-Key: tcs_production_9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e"回應(200 OK)—— offer 尚未被兌換:
[]回應(200 OK)—— on_chain_status: PENDING(排隊中):
[ { "credential_id": "8f7d2c3a-9b1e-4f3a-b7c6-1a2b3c4d5e6f", "issuer_did": "did:iota:issuer-ntu-001", "holder_did": "did:iota:holder-xyz", "vct": "TuringCerts_Standard_Credential_v2_sd_jwt", "format": "dc+sd-jwt", "status": "ACTIVE", "blockchain": "iota", "on_chain_status": "PENDING", "object_id": null, "issued_at": "2026-05-12T08:30:00.000Z", "expires_at": null, "revoked_at": null }]回應(200 OK)—— on_chain_status: IN_PROGRESS:
[ { "...same shape as above...", "on_chain_status": "IN_PROGRESS", "object_id": null }]回應(200 OK)—— on_chain_status: COMPLETED(發行完成):
[ { "...same shape as above...", "on_chain_status": "COMPLETED", "object_id": "0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b" }]回應(200 OK)—— on_chain_status: FAILED(永久失敗):
[ { "...same shape as above...", "on_chain_status": "FAILED", "object_id": null }]| 欄位 | 值 | 意義 |
|---|---|---|
status | ACTIVE / REVOKED / EXPIRED / DELETED | 憑證 lifecycle |
on_chain_status | PENDING / IN_PROGRESS / COMPLETED / FAILED | Notarization 進度。終局狀態為 COMPLETED 與 FAILED。 |
object_id | string / null | IOTA object id;僅在 on_chain_status 為 COMPLETED 時有值 |
blockchain | iota | 錨點網路 |
| 狀態 | 原因 |
|---|---|
| 400 | offerId 不是合法 UUID |
| 401 | 缺 X-API-Key 或 key 無效 |
| 404 | Offer 不存在,或不屬於這支 API key(兩種情況故意無法區分) |
鏈上 notarization
Section titled “鏈上 notarization”TCS 在簽出 SD-JWT VC 之外,可以把每張憑證同步發布到 IOTA Tangle。鏈上 object 儲存 issuer 簽過的 JWT,任何人拿到 object_id 就有一份長期、可由第三方獨立驗證的紀錄 —— 不依賴 TCS 或 issuer 的線上可用性。多數 TCS 客戶採用此模式,也是平台預設。
何時選擇上鏈
Section titled “何時選擇上鏈”- 公部門或合規場景,需要憑證在簽發後多年仍可稽核。
- 跨組織憑證,需要在不呼叫 TCS 的情況下被獨立驗證。
- 任何把「簽發事件本身」視為公開可解析資產的流程。
若以上皆非適用情境(例如純內部員工識別、無外部 relying party),則設 notarize_on_iota: false 略過鏈上錨點,僅使用離線 SD-JWT。
決定點只在 offer 建立時 —— POST /v1/offers 的 notarize_on_iota 欄位(見 Step 1 參數)。同一張 offer 會把這個旗標帶到後續任一交付模式(保管 / 非保管);credential 請求本身沒有單張覆寫機制,錢包 credential 請求中夾帶的 is_on_chain 會被忽略。
| Issuer 簽章路徑 | notarize_on_iota 預設 | 呼叫方可設定 |
|---|---|---|
did:iota:* | true | true(預設)或 false 略過上鏈 |
X.509 / x5c | false | 僅可設 false(預設)—— 明確設 true 會在 offer 建立時回 400 |
Offer 一旦建立,credential 請求無法再改變結果。
路徑限制 —— 只走 DID 路徑
Section titled “路徑限制 —— 只走 DID 路徑”鏈上 notarization 僅在 issuer 走 did:iota(EdDSA)簽章路徑、且 issuer_private_key 可提供給 TCS 時才會執行。註冊於 X.509 / x5c 路徑(HAIP 合規)的 issuer 無法上鏈 —— 當 issuer_did 非 did:iota:* 時,offer 建立階段設 notarize_on_iota: true 會被 400 拒絕。需要鏈上錨點的客戶必須註冊 did:iota issuer;HAIP 與上鏈目前無法同時並存。路徑比較見 DID · 兩條發行方簽章路徑。
Notarization 成功時,holder-receive 回應會包含 reference block:
{ "credentials": [{ "credential": "eyJhbGciOiJFZERTQSIs..." }], "reference": { "chain": "iota", "object_id": "0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b" }}同一份 reference 也會持久化在憑證紀錄上,後續可透過 GET /v1/vc/{credential_id} 從 reference.block_chain + reference.object_id 取出。離線(notarize_on_iota: false)的憑證則不帶 reference block。
如何驗證鏈上紀錄
Section titled “如何驗證鏈上紀錄”鏈上 object 儲存的是 issuer 簽過的 JWT(header.payload.signature —— SD-JWT VC 的簽章部分,未附加 disclosures)。要在不依賴 TCS 的情況下驗證:
- 從
GET /v1/vc/{credential_id}(或簽發回應)取出憑證的reference.object_id。 - 在對應網路(testnet / mainnet)的 IOTA Notarization explorer 上解析該 object ——
state欄位就是 JWT bytes。 - 解析 issuer DID(
did:iota:...),對 DID Document 上的驗證方法驗章。
成功解析 + 驗章即為獨立證明:issuer 在鏈上 object 建立的時間點確實簽出了該張憑證。
移除鏈上紀錄
Section titled “移除鏈上紀錄”鏈上 object 原則上可解(unpublish notarization 交易),但目前未公開 API。若需移除特定 object_id(例如隱私 / 個資刪除請求),請聯絡 TCS 團隊並提供 credential_id 與 object_id。同步參見 架構 · 合規與資料處理 · 當事人刪除權。
進階 · Authorization code flow
Section titled “進階 · Authorization code flow”預設 offer 使用預授權碼流程 — Issuer 在離線端認證 holder(例如在你產品內),然後把一次性的 pre-auth code 嵌進 offer。這是最簡單、也是保管模式採用的方式。
互動式流程的目的是在 holder 兌換憑證之前,於 Authorization Server 上完成認證(例如 hosted login)。對非保管部署比較重要 —— 那種情境下你的產品並未握有 user 的身分情境。
- Holder · 管理錢包與憑證 — 保管模式下後端拿 offer URI 之後做什麼。
- Verifier · 驗證憑證 — 生命週期的另一半。
- 非保管模式 — Holder 是外部錢包時的差異。
- 身份驗證 — DPoP、API key、JWT 細節。