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
| Host | credential-issuance (Docker) or localhost (local dev) |
| Port | 50051 |
| Protocol | HTTP/2 (gRPC) |
| TLS | mTLS 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 doc3. 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 code | Meaning |
|---|---|
INVALID_ARGUMENT | Missing/invalid request fields (e.g. issuer_did empty) |
NOT_FOUND | Credential doesn’t exist |
INTERNAL | Unexpected 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/IssueCredentiallogging:
level:
io.grpc: DEBUG
com.attestpro: DEBUG