Skip to Content
GuidesCredential Flow

End-to-End Credential Flow

This guide walks through the complete lifecycle of a Verifiable Credential on Solidus: from creating an identity, through KYC verification and credential issuance, to presentation, verification, and revocation. Each step includes code and an explanation of what happens on-chain versus off-chain.

Overview

Create DID ──> Complete KYC ──> Receive Credential ──> Store Revoke <── Verify <── Present

1. Create a DID

Every credential holder needs a Decentralized Identifier. Creating a DID generates an Ed25519 key pair and registers the public key on the Solidus blockchain.

import * as ed25519 from '@noble/ed25519' import { createSdk } from '@solidus-network/sdk' // Generate a key pair const privateKey = ed25519.utils.randomPrivateKey() const publicKey = await ed25519.getPublicKeyAsync(privateKey) const privateKeyHex = Buffer.from(privateKey).toString('hex') const publicKeyHex = Buffer.from(publicKey).toString('hex') // Create the SDK instance const sdk = createSdk({ mode: 'testnet', rpcUrl: 'https://rpc.solidus.network', signerPrivateKey: privateKeyHex, }) // Register the DID on-chain (for the signer's own public key) const did = await sdk.did.create(publicKeyHex) console.log('DID:', did.id) // => did:solidus:testnet:7Hk3mRtQZv...

On-chain: A DID Document is written to the Solidus DID Registry. The document contains the public key and verification methods. The DID address is derived from a BLAKE3 hash of the public key.

Off-chain: The private key stays on the user’s device. It is never sent to the network.

2. Complete KYC Verification

The user completes identity verification through the Solidus Verify service. This can be initiated programmatically or by redirecting the user to the hosted verification page.

// Create a verification session via the Verify API const session = await fetch('https://verify.solidus.network/v1/verifications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY', }, body: JSON.stringify({ subjectDid: did.id, level: 1, redirectUrl: 'https://myapp.com/verification-complete', }), }) const { sessionToken, hostedUrl } = await session.json() // Redirect the user to the verification page // hostedUrl => https://verify.solidus.network/v/s/<sessionToken>

There is no webhook field on session creation — webhooks are a separate resource. Register an endpoint once via POST /webhooks and it receives events for all future sessions; see the Webhooks guide.

On-chain: Nothing happens yet. KYC verification is an off-chain process.

Off-chain: The user uploads identity documents (passport, ID card, or driver’s license), completes a liveness check, and the Verify service runs OCR, face matching, and document authenticity checks.

3. Receive the Credential

After successful verification, the Verify service issues a Verifiable Credential to the user’s DID. The credential is signed by the Solidus Verify issuer DID and its hash is anchored on-chain.

// After KYC passes, query the issued credential const verification = await fetch( \`https://verify.solidus.network/v1/verifications/${sessionId}\`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' }, } ) const result = await verification.json() // The credential object const credential = result.credential console.log('Credential ID:', credential.id) console.log('Type:', credential.type) // => ["VerifiableCredential", "KYCCredential"] console.log('Issuer:', credential.issuer) // => did:solidus:testnet:verify-service console.log('Claims:', credential.credentialSubject) // => { id: "did:solidus:testnet:7Hk3...", level: 1, country: "US" }

The credential follows the W3C Verifiable Credentials Data Model. Its structure looks like this:

{ "@context": ["https://www.w3.org/ns/credentials/v2"], "id": "urn:solidus:credential:credential-id-here", "type": ["VerifiableCredential", "KYCCredential"], "issuer": "did:solidus:testnet:verify-service", "validFrom": "2026-05-07T12:00:00Z", "validUntil": "2027-05-07T12:00:00Z", "credentialSubject": { "id": "did:solidus:testnet:7Hk3mRtQZv...", "level": 1, "country": "US", "verifiedAt": "2026-05-07T12:00:00Z" }, "proof": { "type": "Ed25519Signature2020", "created": "2026-05-07T12:00:00Z", "verificationMethod": "did:solidus:testnet:verify-service#key-1", "proofPurpose": "assertionMethod", "proofValue": "z3FXQje2Y..." } }

On-chain: A BLAKE3 hash of the credential is written via a native CredentialIssue transaction — there is no smart-contract VM on Solidus today (see Architecture). This creates an immutable anchor proving the credential was issued at a specific time, without storing any personal data on-chain.

Off-chain: The full credential (with personal data) is returned only to the user and stored in their wallet.

4. Store the Credential

The user stores the credential in their Solidus Identity wallet or any compatible storage.

// Option A: Store in the Solidus Identity app // The credential is automatically saved when the user completes // verification through the hosted flow. // Option B: Store locally in your application import fs from 'fs' // Save the credential to a secure local store fs.writeFileSync( './credentials/kyc-credential.json', JSON.stringify(credential, null, 2) ) // Option C: Store in an encrypted database await db.credentials.create({ did: did.id, type: 'KYCCredential', credential: JSON.stringify(credential), issuedAt: new Date(credential.validFrom), expiresAt: credential.validUntil ? new Date(credential.validUntil) : null, })

On-chain: Nothing. Credential storage is entirely off-chain.

Off-chain: The credential is stored by the holder. Only the holder decides when and where to present it.

5. Present the Credential

When a relying party (such as a DeFi protocol or an exchange) asks for proof of identity, the user creates a Verifiable Presentation. This bundles one or more credentials with a proof that the presenter controls the DID.

@solidus-network/auth exports plain functions (createChallenge, verifyPresentation) — there is no client/verifier class, and it does not build or sign a presentation for you. The relying party issues a challenge; the application builds and signs the VerifiablePresentation object itself (per the shape below) using the holder’s private key.

import { createChallenge } from '@solidus-network/auth' // The relying party creates a challenge for the user's DID const challengeResponse = await fetch('https://relying-party.com/api/auth/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ did: did.id }), }) const challenge = await challengeResponse.json() // challenge = { id, did, nonce, issuedAt, expiresAt } // The user builds and signs a presentation over challenge.nonce with their // private key, then sends it to the relying party for verification const verifyResponse = await fetch('https://relying-party.com/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challengeId: challenge.id, presentation }), })

The presentation structure looks like this:

{ "@context": ["https://www.w3.org/2018/credentials/v1"], "type": ["VerifiablePresentation"], "holder": "did:solidus:testnet:7Hk3mRtQZv...", "verifiableCredential": [ { "...the full KYC credential..." } ], "proof": { "type": "Ed25519Signature2020", "created": "2026-05-07T14:30:00Z", "challenge": "<the challenge nonce>", "verificationMethod": "did:solidus:testnet:7Hk3...#key-1", "proofPurpose": "authentication", "jws": "<detached JWS signature>" } }

See the @solidus-network/auth reference for the exact JWS signing format.

On-chain: Nothing during presentation. The relying party will verify on-chain in the next step.

Off-chain: The user signs the presentation with their private key, binding the credentials to the specific challenge and domain. This prevents replay attacks.

6. Verify the Credential

The relying party verifies the presentation (proving the user controls the DID) and separately verifies each credential referenced in it. Credential verification checks three things: the signature (validated by the chain at submission time), expiration (Solidus credentials don’t expire on-chain), and revocation status.

import { createSdk } from '@solidus-network/sdk' import { verifyPresentation } from '@solidus-network/auth' const sdk = createSdk({ mode: 'testnet', rpcUrl: 'https://rpc.solidus.network', }) // Verify the presentation (proves the user controls the DID) — resolve the // holder's DID document to get the public key that signed it const presentationResult = await verifyPresentation( challenge, presentation, async (verificationMethodId) => { const doc = await sdk.did.resolve(presentation.holder) // ...decode the matching verificationMethod's publicKeyMultibase to raw bytes return publicKeyBytes }, ) console.log('Presentation valid:', presentationResult.valid) // Verify each credential referenced in the presentation, by ID for (const cred of presentation.verifiableCredential ?? []) { const result = await sdk.credentials.verify(cred.id) console.log(\`Credential ${cred.id}:\`) console.log(' Valid:', result.valid) console.log(' Checks:', result.checks) // => { signature: true, expiry: true, revocation: true } }

On-chain: The verifier reads the DID Registry to get the holder’s public key, then reads the credential record by ID to confirm it exists and has not been revoked. These are read-only queries.

Off-chain: Signature and challenge/nonce checks on the presentation happen in memory. The verifier never contacts the issuer directly.

7. Revoke a Credential

If a credential needs to be invalidated (for example, when a user’s identity information changes or fraud is detected), the issuer can revoke it on-chain. revoke() takes the credential ID and the issuer’s private key, and resolves with no return value once confirmed.

// Only the original issuer can revoke a credential const issuerSdk = createSdk({ mode: 'testnet', rpcUrl: 'https://rpc.solidus.network', signerPrivateKey: process.env.ISSUER_PRIVATE_KEY!, }) // Revoke the credential await issuerSdk.credentials.revoke(credential.id, process.env.ISSUER_PRIVATE_KEY!) // After revocation, verification will fail the revocation check const result = await sdk.credentials.verify(credential.id) console.log('Valid:', result.valid) // => false console.log('Checks:', result.checks) // { signature: true, expiry: true, revocation: false }

On-chain: A revocation entry is written via a native CredentialRevoke transaction. The credential is marked as revoked, and future verification queries will see this status.

Off-chain: The credential data itself is not modified. The holder still possesses the credential, but any verifier checking on-chain will see it is revoked.

Summary

StepOn-chainOff-chain
Create DIDDID Document written to registryPrivate key stored locally
Complete KYCNothingDocument upload, OCR, liveness check
Receive credentialCredential hash anchoredFull credential returned to holder
Store credentialNothingHolder saves to wallet/database
Present credentialNothingHolder signs a Verifiable Presentation
Verify credentialRead DID Registry + Credential StoreSignature and expiration checks
Revoke credentialRevocation entry writtenNothing

Next Steps

Last updated on