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 <── Present1. 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
| Step | On-chain | Off-chain |
|---|---|---|
| Create DID | DID Document written to registry | Private key stored locally |
| Complete KYC | Nothing | Document upload, OCR, liveness check |
| Receive credential | Credential hash anchored | Full credential returned to holder |
| Store credential | Nothing | Holder saves to wallet/database |
| Present credential | Nothing | Holder signs a Verifiable Presentation |
| Verify credential | Read DID Registry + Credential Store | Signature and expiration checks |
| Revoke credential | Revocation entry written | Nothing |
Next Steps
- KYC Integration — embed the verification flow in your app
- Webhooks — receive notifications when verification completes
- How Solidus Works — protocol architecture and consensus