Skip to content

Quick Start

This is a five-minute, custodial-mode walkthrough. Your backend makes three HTTP calls and at the end one of your end users has a signed SD-JWT credential in TCS Holder Service. No wallet app, no JWT signing on your side.

If you have not picked a mode yet, see Custodial vs Non-Custodial Mode first.


Step 0: Set the environment variables used in the curl examples below

Section titled “Step 0: Set the environment variables used in the curl examples below”

Run this once in the same shell so the next three commands work as written. Replace the placeholders with the values from your sandbox or live tenant.

Terminal window
export ISSUER_API_KEY="tcs_xxxxxxxxxxxxxxxxxxxxxxxx"
export HOLDER_USER_ACCESS_TOKEN="eyJhbGciOiJ...your_holder_login_jwt..."
export HOLDER_PRIVATE_KEY_BASE64URL="base64url-encoded-private-key-from-POST-/v1/did"
export HOLDER_DID="did:iota:testnet:0x5678..."
export ISSUER_BASE="https://issuer.turingspace.co"
export HOLDER_BASE="https://holder.turingspace.co"

Step 1: Your backend creates a credential offer

Section titled “Step 1: Your backend creates a credential offer”

Call Credential Issuer with your Issuer API key. This is the only call needed on the issuer side.

Terminal window
curl -X POST "${ISSUER_BASE}/v1/offers" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${ISSUER_API_KEY}" \
-d '{
"credential": {
"config_id": "TuringCerts_Standard_Credential_v2_sd_jwt",
"claims": {
"credentialName": "Employee Badge",
"issuedTime": "2026-04-01T00:00:00Z"
}
},
"flow": "pre-authorized",
"expires_in": 3600
}'

config_id is a key in your tenant’s issuer metadata credential_configurations_supported map — discover what your sandbox has via GET /.well-known/openid-credential-issuer/{tenant}. Sandbox tenants ship with TuringCerts_Standard_Credential_v2_sd_jwt pre-loaded; for other types see the Schema Registry catalog.

Response:

{
"offer_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"credential_offer_uri": "https://issuer.turingspace.co/v1/offers/a1b2c3d4-...",
"deep_link": "openid-credential-offer://?credential_offer_uri=...",
"expires_at": "2026-04-01T01:00:00Z"
}

In a non-custodial flow you would now deliver deep_link to the user’s wallet app via QR. In custodial mode you pass deep_link (the openid-credential-offer:// form) as credential_offer_uri to Holder Service in the next step.


Step 2: Your backend asks Holder Service to receive the credential

Section titled “Step 2: Your backend asks Holder Service to receive the credential”

Call Holder Service with the end user’s access token (not your Issuer API key) and pass the offer URI together with the user’s DID and private key. Holder Service handles all the wallet-side protocol work — pre-auth code exchange, proof JWT, DPoP — internally.

Terminal window
curl -X POST "${HOLDER_BASE}/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:0x5678...",
"private_key": "${HOLDER_PRIVATE_KEY_BASE64URL}",
"notarize_on_iota": true
}'

Response (201 Created):

{
"credential_id": "f7e8d9c0-1b2a-3c4d-5e6f-7a8b9c0d1e2f",
"vct": "https://schema-registry.turingspace.co/schemas/TuringCerts_Standard_Credential/v2",
"issuer_did": "did:iota:testnet:0xabcdef...",
"holder_did": "did:iota:testnet:0x5678...",
"raw_credential": "eyJhbGciOiJFZERTQSIs...~sd_disclosure_1~sd_disclosure_2~",
"issued_at": "2026-04-01T00:00:30Z",
"status": "active"
}

The credential is now stored in Holder Service for that end user.

The example above passes notarize_on_iota: true explicitly. The signed SD-JWT credential is also anchored on the IOTA Tangle, giving you a public, long-lived audit record alongside the off-chain SD-JWT. To skip the on-chain anchor (signed SD-JWT only), set notarize_on_iota: false. The platform default is true if you omit the field, but pass the value explicitly so behavior is stable across upgrades. See On-chain notarization for the full path, signing-method constraints, and how to verify the on-chain record. Other optional fields tune the receive call: credential_configuration_id (pick one when the offer advertises multiple) and user_pin (only if the issuer set require_transaction_code: true). Full reference: Holder · Manage Wallets & Credentials § 2.


Step 3: Confirm by listing the end user’s credentials

Section titled “Step 3: Confirm by listing the end user’s credentials”
Terminal window
curl "${HOLDER_BASE}/v1/vc" \
-H "Authorization: Bearer ${HOLDER_USER_ACCESS_TOKEN}"

You see the credential issued in Step 2. From here, POST /v1/oid4vp/presentation lets the same user present it to a verifier.


Three concrete assertions confirm the integration works end-to-end. Run them from your test suite or as a CI smoke check:

  1. Step 1 — POST /v1/offers returns 201 with a non-empty credential_offer_uri and an expires_at in the future.
  2. Step 2 — POST /v1/oid4vci/receive returns 201 with status: "active" and a non-empty raw_credential string starting with eyJ (the SD-JWT VC’s base64-encoded JWS header).
  3. Step 3 — GET /v1/vc returns { "credentials": [...] } containing an entry whose id matches the credential_id from Step 2, with status: "active". (Field name is id on the list endpoint; credential_id is the receive-response shape only.)

If any assertion fails, the most common causes are: invalid config_id (Step 1 → 400), expired or reused offer URI (Step 2 → 400), or wrong access_token (Step 2/3 → 401 — re-run POST /v1/user/login).