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.
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.
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.
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”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.
Verify your integration
Section titled “Verify your integration”Three concrete assertions confirm the integration works end-to-end. Run them from your test suite or as a CI smoke check:
- Step 1 —
POST /v1/offersreturns201with a non-emptycredential_offer_uriand anexpires_atin the future. - Step 2 —
POST /v1/oid4vci/receivereturns201withstatus: "active"and a non-emptyraw_credentialstring starting witheyJ(the SD-JWT VC’s base64-encoded JWS header). - Step 3 —
GET /v1/vcreturns{ "credentials": [...] }containing an entry whoseidmatches thecredential_idfrom Step 2, withstatus: "active". (Field name isidon the list endpoint;credential_idis 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).
What’s next
Section titled “What’s next”- End-to-end including verification → Holder · Manage Wallets & Credentials
- Customise the offer (PINs, auth-code flow, schema-bound claims) → Issuer · Issue Credential
- Set up the verifier side → Verifier · Verify Credential
- Skip Holder Service (use external wallets) → Non-Custodial Mode