跳到內容

Issuer · 發行憑證

這頁是給 Issuer 角色:簽出並交付憑證的組織。從你的後端視角,發行其實只是一支 API 呼叫POST /v1/offers。其後(fetch offer、換 code、組 proof JWT、取 SD-JWT)都在錢包端完成;保管模式下由 TCS Holder Service 替你做。


Rendering diagram...

你要實作的兩步:

  1. 建立 offer。 你的後端把 credential payload POST 到 Credential Issuer。TCS 回 offer URI 與 deep link。
  2. 交付 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 細節見非保管模式)。


用你的 Issuer API key 對 Credential Issuer 打。Offer 帶著憑證型別、claims、與 flow 設定。

Terminal window
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_idstringIssuer metadata credential_configurations_supported map 的 key。可用 GET /.well-known/openid-credential-issuer/:tenant 查詢可用 ID。
credential.claimsobject要包含的憑證 claims(鍵值對)
flowstring發行流程型別。生產環境目前僅支援 "pre-authorized""authorization-code" 在 roadmap 上 —— 見 標準 Roadmap
expires_innumberOffer 存活秒數(預設 3600)
issuer_didstring為這張 offer 覆寫預設 issuer DID。必須與 issuer_private_key 一起提供 —— 缺一補一會回 400
holder_didstring把 offer 綁定到特定 holder DID。
issuer_private_keystring覆寫預設 issuer 簽章金鑰。必須與 issuer_did 一起提供 —— 缺一補一會回 400
notarize_on_iotaboolean是否把這張憑證錨點到 IOTA。issuer_diddid:iota:* 時預設 true,其餘 false。明確設 trueissuer_did 必須是 did:iota:*。見 鏈上 notarization
callback_urlstring鏈上 notarization 完成後 TCS 會 POST 的 HTTPS URL。單次交付、不重試。notarize_on_iota=false 時仍會儲存但永不觸發。最長 2048 字元。見 Step 3 · 追蹤發行狀態
狀態原因
400Body 驗證失敗(缺欄位、claims 格式錯、config_id 不存在);亦於 issuer_didissuer_private_key 未同時提供 / 同時省略時回傳
401X-API-Key 或 key 無效
403API key 沒被授權使用該 tenant 或 schema;或所提供的 issuer_did 未註冊於呼叫方租戶下

credential_offer_uri 怎麼交付,看你的模式。

直接把 URI 交給 Holder Service。後端已經握有該 user 的 access token 與 DID 私鑰(見 Holder · 管理錢包與憑證 § 1)。

Terminal window
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 跑(見非保管模式)。


Offer 交付之後,有兩種方式可以得知憑證實際發出並完成鏈上錨點:一是 TCS 在鏈上 notarization 完成後主動 POST 的 webhook,二是隨時可查的 polling 端點。

實務上把 webhook 當主訊號、polling 當 fallback。Webhook 為單次交付 —— production 程式碼應在 webhook 未於可接受時間內抵達時,改用 polling 補上。

POST /v1/offerscallback_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 POSTContent-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)。

GET /v1/offers/{offerId}/credentials 回該 offer 已發出的憑證。在 webhook 不可用(X.509 路徑、notarize_on_iota=false)、或 webhook 未到、或想隨時做一次同步狀態查詢時使用。

Terminal window
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 }
]
欄位意義
statusACTIVE / REVOKED / EXPIRED / DELETED憑證 lifecycle
on_chain_statusPENDING / IN_PROGRESS / COMPLETED / FAILEDNotarization 進度。終局狀態為 COMPLETEDFAILED
object_idstring / nullIOTA object id;僅在 on_chain_statusCOMPLETED 時有值
blockchainiota錨點網路
狀態原因
400offerId 不是合法 UUID
401X-API-Key 或 key 無效
404Offer 不存在,或不屬於這支 API key(兩種情況故意無法區分)

TCS 在簽出 SD-JWT VC 之外,可以把每張憑證同步發布到 IOTA Tangle。鏈上 object 儲存 issuer 簽過的 JWT,任何人拿到 object_id 就有一份長期、可由第三方獨立驗證的紀錄 —— 不依賴 TCS 或 issuer 的線上可用性。多數 TCS 客戶採用此模式,也是平台預設。

  • 公部門或合規場景,需要憑證在簽發後多年仍可稽核。
  • 跨組織憑證,需要在不呼叫 TCS 的情況下被獨立驗證。
  • 任何把「簽發事件本身」視為公開可解析資產的流程。

若以上皆非適用情境(例如純內部員工識別、無外部 relying party),則設 notarize_on_iota: false 略過鏈上錨點,僅使用離線 SD-JWT。

決定點只在 offer 建立時 —— POST /v1/offersnotarize_on_iota 欄位(見 Step 1 參數)。同一張 offer 會把這個旗標帶到後續任一交付模式(保管 / 非保管);credential 請求本身沒有單張覆寫機制,錢包 credential 請求中夾帶的 is_on_chain 會被忽略。

Issuer 簽章路徑notarize_on_iota 預設呼叫方可設定
did:iota:*truetrue(預設)或 false 略過上鏈
X.509 / x5cfalse僅可設 false(預設)—— 明確設 true 會在 offer 建立時回 400

Offer 一旦建立,credential 請求無法再改變結果。

鏈上 notarization 僅在 issuer 走 did:iota(EdDSA)簽章路徑、且 issuer_private_key 可提供給 TCS 時才會執行。註冊於 X.509 / x5c 路徑(HAIP 合規)的 issuer 無法上鏈 —— 當 issuer_diddid: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。

鏈上 object 儲存的是 issuer 簽過的 JWT(header.payload.signature —— SD-JWT VC 的簽章部分,未附加 disclosures)。要在不依賴 TCS 的情況下驗證:

  1. GET /v1/vc/{credential_id}(或簽發回應)取出憑證的 reference.object_id
  2. 在對應網路(testnet / mainnet)的 IOTA Notarization explorer 上解析該 object —— state 欄位就是 JWT bytes。
  3. 解析 issuer DID(did:iota:...),對 DID Document 上的驗證方法驗章。

成功解析 + 驗章即為獨立證明:issuer 在鏈上 object 建立的時間點確實簽出了該張憑證。

鏈上 object 原則上可解(unpublish notarization 交易),但目前未公開 API。若需移除特定 object_id(例如隱私 / 個資刪除請求),請聯絡 TCS 團隊並提供 credential_idobject_id。同步參見 架構 · 合規與資料處理 · 當事人刪除權


預設 offer 使用預授權碼流程 — Issuer 在離線端認證 holder(例如在你產品內),然後把一次性的 pre-auth code 嵌進 offer。這是最簡單、也是保管模式採用的方式。

互動式流程的目的是在 holder 兌換憑證之前,於 Authorization Server 上完成認證(例如 hosted login)。對非保管部署比較重要 —— 那種情境下你的產品並未握有 user 的身分情境。