Issue a Credential
Issuing a PDTF credential means building a W3C Verifiable Credential envelope, adding PDTF-specific fields like credentialStatus, and signing it with an issuer key using eddsa-jcs-2022.
In @pdtf/core, the main entry point is VcSigner.
1. Create or load an issuer key
Section titled “1. Create or load an issuer key”VcSigner needs three things:
- a
KeyProvider - a
keyIdin that provider - the issuer DID, usually a
did:webfor adapters and platforms
The exact KeyProvider depends on your environment. In development you might use a local provider, and in production a KMS-backed one.
import { VcSigner } from '@pdtf/core';import type { KeyProvider } from '@pdtf/core';
const keyProvider: KeyProvider = getYourKeyProvider();const keyId = 'adapter/epc/signing-key-1';const issuerDid = 'did:web:adapters.propdata.org.uk:epc';
const signer = new VcSigner(keyProvider, keyId, issuerDid);use pdtf_core::signer::VcSigner;use pdtf_core::keys::provider::memory::MemoryKeyProvider;use pdtf_core::keys::provider::KeyProvider;use pdtf_core::types::KeyCategory;
let provider = MemoryKeyProvider::new();let record = provider.generate_key("adapter-epc", KeyCategory::Adapter).await?;
let signer = VcSigner::from_key_id(&provider, "adapter-epc").await?;from pdtf_core import generate_keypair
keypair = generate_keypair()issuer_did = keypair["did"]secret_key_hex = keypair["secret_key_hex"]
print(f"Issuer DID: {issuer_did}")using Pdtf.Core;
var keypair = PdtfCore.GenerateKeypair();var issuerDid = keypair.Did;
Console.WriteLine($"Issuer DID: {issuerDid}");For did:web issuers, VcSigner uses the conventional verification method did:web:...#key-1, so your hosted DID document must expose the matching key in both verificationMethod and assertionMethod.
2. Allocate a revocation entry before signing
Section titled “2. Allocate a revocation entry before signing”Every PDTF credential must include credentialStatus. That means you should allocate a status list index before issuing the VC.
const credentialStatus = { id: 'https://adapters.propdata.org.uk/status/epc/list-042#18293', type: 'BitstringStatusListEntry' as const, statusPurpose: 'revocation' as const, statusListIndex: '18293', statusListCredential: 'https://adapters.propdata.org.uk/status/epc/list-042',};In a real issuer, these values usually come from your status list service, not hard-coded strings.
3. Build the credential subject
Section titled “3. Build the credential subject”The subject is ordinary JSON, but it must include id and it should match the PDTF schema for the entity and paths you are asserting.
const credentialSubject = { id: 'urn:pdtf:uprn:100023456789', energyEfficiency: { certificate: { currentEnergyRating: 'C', potentialEnergyRating: 'B', lodgementDate: '2026-03-15', expiryDate: '2036-03-14', certificateNumber: '0123-4567-8901-2345-6789', }, },};4. Sign the credential
Section titled “4. Sign the credential”const vc = await signer.sign({ id: 'urn:uuid:6df1a7c2-4207-4d79-9b37-685c6b4c8e74', type: 'PropertyCredential', credentialSubject, credentialStatus, evidence: [ { type: 'ElectronicRecord', source: 'https://epc-register.example/certificates/0123-4567-8901-2345-6789', retrievedAt: new Date().toISOString(), }, ], validFrom: new Date().toISOString(),});
console.log(JSON.stringify(vc, null, 2));use pdtf_core::signer::BuildVcOptions;use pdtf_core::types::*;
let vc = signer.sign(BuildVcOptions { vc_type: vec!["PropertyCredential".to_string()], credential_subject: CredentialSubject { id: "urn:pdtf:uprn:100023456789".to_string(), claims: serde_json::from_value(serde_json::json!({ "energyEfficiency": { "certificate": { "currentEnergyRating": "C", "potentialEnergyRating": "B", } } }))?, }, id: Some("urn:uuid:6df1a7c2-4207-4d79-9b37-685c6b4c8e74".to_string()), valid_from: Some(chrono::Utc::now().to_rfc3339()), credential_status: Some(CredentialStatus { id: "https://adapters.propdata.org.uk/status/epc/list-042#18293".into(), r#type: "BitstringStatusListEntry".into(), status_purpose: "revocation".into(), status_list_index: "18293".into(), status_list_credential: "https://adapters.propdata.org.uk/status/epc/list-042".into(), }), evidence: Some(vec![Evidence { r#type: "ElectronicRecord".into(), source: Some("https://epc-register.example/certificates/0123-4567-8901-2345-6789".into()), retrieved_at: Some(chrono::Utc::now().to_rfc3339()), }]), ..Default::default()}).await?;
println!("{}", serde_json::to_string_pretty(&vc)?);from pdtf_core import sign_vcimport jsonfrom datetime import datetime, timezone
vc = { "@context": [ "https://www.w3.org/ns/credentials/v2", "https://propdata.org.uk/credentials/v2", ], "id": "urn:uuid:6df1a7c2-4207-4d79-9b37-685c6b4c8e74", "type": ["VerifiableCredential", "PropertyCredential"], "issuer": {"id": issuer_did}, "validFrom": datetime.now(timezone.utc).isoformat(), "credentialSubject": credential_subject, "credentialStatus": credential_status,}
signed_vc = sign_vc(json.dumps(vc), secret_key_hex)print(json.dumps(json.loads(signed_vc), indent=2))var vc = JsonSerializer.Serialize(new { context = new[] { "https://www.w3.org/ns/credentials/v2", "https://propdata.org.uk/credentials/v2", }, id = "urn:uuid:6df1a7c2-4207-4d79-9b37-685c6b4c8e74", type = new[] { "VerifiableCredential", "PropertyCredential" }, issuer = new { id = keypair.Did }, validFrom = DateTime.UtcNow.ToString("o"), credentialSubject, credentialStatus,});
var signedVc = PdtfCore.SignVc(vc, keypair.SecretKeyHex);Console.WriteLine(signedVc);The returned credential includes:
- W3C VC v2 context
- PDTF context
- issuer DID
credentialStatus- your subject and evidence
proofwithDataIntegrityProofandcryptosuite: "eddsa-jcs-2022"
5. What VcSigner does for you
Section titled “5. What VcSigner does for you”Under the hood, VcSigner:
- builds the unsigned VC
- derives the verification method from the issuer DID
- canonicalises the proof options and the VC with JCS
- signs the combined hash with your Ed25519 key
- encodes the signature as multibase in
proof.proofValue
You do not need to construct proof manually.
6. Minimal adapter issuance flow
Section titled “6. Minimal adapter issuance flow”async function issueEpcCredential(uprn: string) { const source = await fetchEpcFromSourceApi(uprn); const status = await allocateStatusEntry(issuerDid);
return signer.sign({ type: 'PropertyCredential', credentialSubject: mapEpcToCredentialSubject(source), credentialStatus: status, evidence: [ { type: 'ElectronicRecord', source: source.sourceUrl, retrievedAt: new Date().toISOString(), }, ], });}async fn issue_epc_credential(uprn: &str) -> Result<VerifiableCredential> { let source = fetch_epc_from_source_api(uprn).await?; let status = allocate_status_entry().await?;
signer.sign(BuildVcOptions { vc_type: vec!["PropertyCredential".into()], credential_subject: map_epc_to_credential_subject(&source), credential_status: Some(status), evidence: Some(vec![Evidence { r#type: "ElectronicRecord".into(), source: Some(source.source_url.clone()), retrieved_at: Some(chrono::Utc::now().to_rfc3339()), }]), ..Default::default() }).await}def issue_epc_credential(uprn: str) -> dict: source = fetch_epc_from_source_api(uprn) status = allocate_status_entry()
vc = build_vc( vc_type="PropertyCredential", credential_subject=map_epc_to_credential_subject(source), credential_status=status, ) return json.loads(sign_vc(json.dumps(vc), secret_key_hex))async Task<string> IssueEpcCredential(string uprn){ var source = await FetchEpcFromSourceApi(uprn); var status = await AllocateStatusEntry();
var vc = BuildVc("PropertyCredential", MapEpcToCredentialSubject(source), status);
return PdtfCore.SignVc( JsonSerializer.Serialize(vc), keypair.SecretKeyHex);}7. Production checks before returning the VC
Section titled “7. Production checks before returning the VC”Before you publish or return the credential:
- make sure the issuer DID is active in the TIR for the claimed paths
- confirm the DID document is hosted and resolves correctly
- persist the credential ID to status-list index mapping
- store enough source metadata to support reissuance or audit
- run a self-check with
VcValidator
If you skip credentialStatus, or your DID document does not contain the signing key in assertionMethod, downstream verifiers should reject the credential even if the signature bytes are mathematically valid.