Skip to Content
How-To GuidesIntegrate a Credential Issuance Client

Integrate a Credential Issuance gRPC Client

This guide explains how Core Engine (or any other service) invokes the Credential Issuance Service’s gRPC endpoints. For the full RPC/message reference, see Credential Issuance.

There is no batchIssueCredentials RPC. Batch issuance is implemented entirely in Core Engine’s REST layer (IssueCredentialController / BatchIssueCredentialRequest), which loops the single IssueCredential RPC shown below once per record.

Service location

Hostcredential-issuance (Docker) or localhost (local dev)
Port50051
ProtocolHTTP/2 (gRPC)
TLSmTLS required — see Internal gRPC mTLS

1. Add gRPC dependencies

dependencies { implementation project(':shared') implementation 'net.devh:grpc-client-spring-boot-starter:2.15.0.RELEASE' implementation 'io.grpc:grpc-netty-shaded:1.59.0' implementation 'io.grpc:grpc-protobuf:1.59.0' implementation 'io.grpc:grpc-stub:1.59.0' }

2. Configure the client

grpc: client: credential-issuance: address: static://credential-issuance:50051 negotiation-type: plaintext # tls + mTLS in production, see internal-mtls doc

3. Inject the stub

@Service public class CredentialOrchestrationService { @GrpcClient("credential-issuance") private CredentialIssuanceServiceStub credentialIssuanceStub; }

4. Issue a single credential

IssueCredentialRequest request = IssueCredentialRequest.newBuilder() .setIssuerDid(issuerDid) .setHolderDid(holderDid) .setCredentialType(credentialType) .setCredentialData(ByteString.copyFromUtf8("{\"name\":\"John Doe\"}")) .setExpirationTimestamp(System.currentTimeMillis() + (365L * 24 * 60 * 60 * 1000)) .build(); credentialIssuanceStub.issueCredential(request, new StreamObserver<IssueCredentialResponse>() { @Override public void onNext(IssueCredentialResponse response) { if ("ISSUED".equals(response.getStatus())) { LOG.info("Credential issued: {}", response.getCredentialId()); } else { LOG.error("Issuance failed: {}", response.getErrorMessage()); } } @Override public void onError(Throwable t) { LOG.error("gRPC error during issuance", t); } @Override public void onCompleted() { } });

5. Issue multiple credentials (the real pattern — no batch RPC)

Call issueCredential once per record and aggregate results yourself. This is exactly what Core Engine’s REST /api/v1/credentials/batch endpoint does today:

public void issueMany(String issuerDid, List<CredentialData> credentials) { for (CredentialData cred : credentials) { IssueCredentialRequest request = IssueCredentialRequest.newBuilder() .setIssuerDid(issuerDid) .setHolderDid(cred.getHolderDid()) .setCredentialType(cred.getType()) .setCredentialData(ByteString.copyFromUtf8(cred.getJsonData())) .build(); credentialIssuanceStub.issueCredential(request, /* per-record observer */); } }

6. Retrieve a credential

GetCredentialRequest request = GetCredentialRequest.newBuilder() .setCredentialId(credentialId) .build(); credentialIssuanceStub.getCredential(request, new StreamObserver<GetCredentialResponse>() { @Override public void onNext(GetCredentialResponse response) { LOG.info("Credential retrieved: id={}, status={}, issuer={}", credentialId, response.getStatus(), response.getIssuerDid()); if (response.getRevokedAt() > 0) { LOG.info("Revoked at: {}", Instant.ofEpochMilli(response.getRevokedAt())); } } @Override public void onError(Throwable t) { LOG.warn("Credential not found or error: {}", credentialId, t); } @Override public void onCompleted() { } });

7. Revoke a credential

RevokeCredentialRequest request = RevokeCredentialRequest.newBuilder() .setCredentialId(credentialId) .setIssuerDid(issuerDid) .setReason(reason) .build(); credentialIssuanceStub.revokeCredential(request, /* StreamObserver */);

Error handling

gRPC codeMeaning
INVALID_ARGUMENTMissing/invalid request fields (e.g. issuer_did empty)
NOT_FOUNDCredential doesn’t exist
INTERNALUnexpected server error
credentialIssuanceStub.getCredential(request, new StreamObserver<GetCredentialResponse>() { @Override public void onError(Throwable t) { if (t instanceof StatusRuntimeException sre) { Status status = sre.getStatus(); switch (status.getCode()) { case NOT_FOUND -> LOG.warn("Credential not found"); case INVALID_ARGUMENT -> LOG.warn("Invalid request: {}", status.getDescription()); case INTERNAL -> LOG.error("Server error: {}", status.getDescription()); default -> LOG.error("Unexpected status", t); } } else { LOG.error("Unexpected error", t); } } @Override public void onCompleted() { } });

Synchronous vs asynchronous

// Non-blocking — preferred for gRPC credentialIssuanceStub.issueCredential(request, responseObserver); // Blocking — less efficient, occasionally simpler for tests/scripts @GrpcClient("credential-issuance") private CredentialIssuanceServiceBlockingStub credentialIssuanceBlockingStub; IssueCredentialResponse response = credentialIssuanceBlockingStub.issueCredential(request);

Testing with an in-process channel

@Test void testCredentialOrchestrationWithMock() throws IOException { String serverName = "test-" + UUID.randomUUID(); server = InProcessServerBuilder.forName(serverName) .addService(new CredentialIssuanceServiceImpl(credentialRepository, registryStub, objectMapper)) .build().start(); channel = InProcessChannelBuilder.forName(serverName).build(); stub = CredentialIssuanceServiceGrpc.newStub(channel); // call stub and verify behavior }

Debugging

grpcurl -plaintext localhost:50051 list grpcurl -plaintext \ -d '{"issuer_did":"did:key:issuer123","holder_did":"did:key:holder456","credential_type":"SecondaryEducationCredential","credential_data":"e30="}' \ localhost:50051 \ com.attestpro.credential.CredentialIssuanceService/IssueCredential
logging: level: io.grpc: DEBUG com.attestpro: DEBUG