Credential Issuer API
The Credential Issuer API provides endpoints for managing W3C Verifiable Credential templates, processing issuance requests through a manual review workflow, and creating selective-disclosure verification challenges. All endpoints are tenant-scoped via the x-tenant header and require the manage:credential-issuer permission.
Authentication
All endpoints require:
Authorization: Bearer <access_token>— a valid access tokenx-tenant: <realm>— the tenant realm identifier
Missing or invalid authentication returns 401. A valid token without manage:credential-issuer returns 403.
Response Shape
All responses follow the same envelope:
Success
{
"data": { ... },
"error": null
}Error
{
"data": null,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description"
}
}Common error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | The requested resource does not exist or belongs to a different tenant |
VALIDATION | 400 | A required field is missing or has an invalid value |
INVALID_STATE | 409 | The requested state transition is not permitted for the current status |
Templates
Credential templates define the schema and type label for credentials your tenant issues. A template is referenced by issuance requests and must be active for new requests to be accepted.
List Templates
/api/credential-issuer/templatesRequires: manage:credential-issuerReturns a paginated list of all credential templates for the tenant. Supports full-text search across template names and descriptions.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page |
search | string | — | Full-text filter on name and description |
Success response
{
"data": {
"data": [
{
"id": "cldtempl_abc123",
"tenantId": "tenant_xyz",
"name": "Employee Identity Card",
"description": "Issued to verified employees as proof of employment.",
"type": "VerifiableId",
"schemaFields": [
{ "key": "fullName", "label": "Full Name", "fieldType": "text", "required": true },
{ "key": "employeeId", "label": "Employee ID", "fieldType": "text", "required": true },
{ "key": "startDate", "label": "Start Date", "fieldType": "date", "required": true },
{ "key": "department", "label": "Department", "fieldType": "text", "required": false }
],
"isActive": true,
"createdAt": "2025-03-01T10:00:00Z",
"updatedAt": "2025-03-01T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
},
"error": null
}Create Template
/api/credential-issuer/templatesRequires: manage:credential-issuerCreate a new credential template. The template starts active and accepts issuance requests immediately upon creation.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable template name |
type | string | Yes | Credential type label (e.g., VerifiableId, ProfessionalCertificate) |
description | string | No | Optional description |
schemaFields | TemplateSchemaField[] | No | Field definitions (defaults to []) |
TemplateSchemaField object
| Property | Type | Description |
|---|---|---|
key | string | Machine-readable identifier used as the JSON key in subjectData |
label | string | Human-readable field label |
fieldType | "text" | "date" | "number" | "email" | Data type for validation and display |
required | boolean | Whether this field is mandatory on every issuance request |
Request body example
{
"name": "Professional Certificate",
"type": "ProfessionalCertificate",
"description": "Issued to practitioners who have completed the certified training programme.",
"schemaFields": [
{ "key": "holderName", "label": "Holder Name", "fieldType": "text", "required": true },
{ "key": "certificationId", "label": "Certification ID", "fieldType": "text", "required": true },
{ "key": "issueDate", "label": "Issue Date", "fieldType": "date", "required": true },
{ "key": "expiryDate", "label": "Expiry Date", "fieldType": "date", "required": false }
]
}Success response — 201 Created
{
"data": {
"id": "cldtempl_new456",
"name": "Professional Certificate",
"type": "ProfessionalCertificate",
"description": "Issued to practitioners who have completed the certified training programme.",
"schemaFields": [
{ "key": "holderName", "label": "Holder Name", "fieldType": "text", "required": true },
{ "key": "certificationId", "label": "Certification ID", "fieldType": "text", "required": true },
{ "key": "issueDate", "label": "Issue Date", "fieldType": "date", "required": true },
{ "key": "expiryDate", "label": "Expiry Date", "fieldType": "date", "required": false }
],
"isActive": true,
"createdAt": "2025-03-10T14:22:00Z",
"updatedAt": "2025-03-10T14:22:00Z"
},
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
VALIDATION | 400 | name or type is missing |
Get Template
/api/credential-issuer/templates/[id]Requires: manage:credential-issuerRetrieve a single credential template by its ID.
Success response — same shape as the template object in Create Template.
Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Template does not exist or belongs to a different tenant |
Update Template
/api/credential-issuer/templates/[id]Requires: manage:credential-issuerPartially update a credential template. All fields are optional — only provided fields are
modified. To deactivate a template and prevent new issuance requests, set isActive
to false.
Request body (all fields optional)
| Field | Type | Description |
|---|---|---|
name | string | New template name |
description | string | New description |
type | string | New credential type label |
schemaFields | TemplateSchemaField[] | Replacement field definitions (replaces entire array) |
isActive | boolean | Set to false to prevent new requests against this template |
Request body example
{
"isActive": false
}Success response
{
"data": {
"id": "cldtempl_abc123",
"isActive": false,
"updatedAt": "2025-04-01T09:00:00Z"
},
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Template does not exist |
VALIDATION | 400 | An invalid value was provided for a field |
Delete Template
/api/credential-issuer/templates/[id]Requires: manage:credential-issuerPermanently delete a credential template. Templates that have active issuance requests referencing them cannot be deleted — deactivate the template instead.
Deletion is permanent. Historical credentials issued under this template retain their issuedVcId but the template metadata is no longer available. Deactivate templates instead of deleting them when you need to preserve the audit trail.
Success response
{
"data": { "deleted": true },
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Template does not exist |
Requests
A Credential Request captures the intent to issue a specific credential to a specific subject. Requests move through a four-status lifecycle: PENDING → APPROVED → ISSUED, or PENDING → REJECTED.
List Requests
/api/credential-issuer/requestsRequires: manage:credential-issuerReturns a paginated list of credential requests for the tenant, optionally filtered by status.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 10 | Items per page |
status | PENDING | APPROVED | REJECTED | ISSUED | — | Filter by request status |
Success response
{
"data": {
"data": [
{
"id": "cldreq_def789",
"templateId": "cldtempl_abc123",
"template": { "name": "Employee Identity Card", "type": "VerifiableId" },
"subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"subjectData": {
"fullName": "Jane Doe",
"employeeId": "EMP-00042",
"startDate": "2024-09-01",
"department": "Engineering"
},
"status": "PENDING",
"reviewedBy": null,
"reviewedAt": null,
"rejectionNote": null,
"issuedVcId": null,
"createdAt": "2025-04-10T08:30:00Z",
"updatedAt": "2025-04-10T08:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 1,
"totalPages": 1
}
},
"error": null
}Create Request
/api/credential-issuer/requestsRequires: manage:credential-issuerSubmit a new credential request for a specific subject. The request enters PENDING
status and awaits manual review. The referenced template must exist, belong to the tenant, and
be active.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | Yes | The ID of the credential template to issue from |
subjectDid | string | Yes | The subject’s Decentralized Identifier (DID) |
subjectData | object | Yes | Field values conforming to the template’s schemaFields |
Request body example
{
"templateId": "cldtempl_abc123",
"subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"subjectData": {
"fullName": "Jane Doe",
"employeeId": "EMP-00042",
"startDate": "2024-09-01",
"department": "Engineering"
}
}Success response — 201 Created
{
"data": {
"id": "cldreq_def789",
"templateId": "cldtempl_abc123",
"template": { "name": "Employee Identity Card", "type": "VerifiableId" },
"subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"subjectData": {
"fullName": "Jane Doe",
"employeeId": "EMP-00042",
"startDate": "2024-09-01",
"department": "Engineering"
},
"status": "PENDING",
"reviewedBy": null,
"reviewedAt": null,
"rejectionNote": null,
"issuedVcId": null,
"createdAt": "2025-04-10T08:30:00Z",
"updatedAt": "2025-04-10T08:30:00Z"
},
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
VALIDATION | 400 | templateId, subjectDid, or subjectData is missing |
NOT_FOUND | 404 | Template does not exist, belongs to a different tenant, or is inactive |
Get Request
/api/credential-issuer/requests/[id]Requires: manage:credential-issuerRetrieve a single credential request by its ID.
Success response — same shape as a request object in List Requests.
Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Request does not exist or belongs to a different tenant |
Review Request
/api/credential-issuer/requests/[id]/reviewRequires: manage:credential-issuerApprove or reject a PENDING credential request. The reviewer’s identity is
captured from the access token’s sub claim. Attempting to review a request that
is not in PENDING status returns INVALID_STATE.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
action | "approve" | "reject" | Yes | The review decision |
rejectionNote | string | No | Explanation shown to the requester (only meaningful when action is "reject") |
Request body — approve
{
"action": "approve"
}Request body — reject with note
{
"action": "reject",
"rejectionNote": "The submitted employee ID does not match HR records. Please resubmit with the correct ID."
}Success response — approved
{
"data": {
"id": "cldreq_def789",
"status": "APPROVED",
"reviewedBy": "usr_reviewer001",
"reviewedAt": "2025-04-10T09:15:00Z",
"rejectionNote": null
},
"error": null
}Success response — rejected
{
"data": {
"id": "cldreq_def789",
"status": "REJECTED",
"reviewedBy": "usr_reviewer001",
"reviewedAt": "2025-04-10T09:18:00Z",
"rejectionNote": "The submitted employee ID does not match HR records. Please resubmit with the correct ID."
},
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Request does not exist |
VALIDATION | 400 | action is missing or not one of "approve" / "reject" |
INVALID_STATE | 409 | Request is not in PENDING status and cannot be reviewed |
Issue Credential
/api/credential-issuer/requests/[id]/issueRequires: manage:credential-issuerIssue a credential from an APPROVED request. Generates a unique issuedVcId
UUID, records it on the request, and transitions the request to ISSUED. Requests in
any status other than APPROVED are rejected with INVALID_STATE.
Request: No body required.
Success response
{
"data": {
"credentialId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
},
"error": null
}The credentialId value is the issuedVcId assigned to the request. Pass this identifier to your VC signing integration to generate the final signed credential payload.
Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Request does not exist |
INVALID_STATE | 409 | Request is not in APPROVED status. Check the current status and review the request first. |
The issuance endpoint is idempotent in the sense that an APPROVED request can be issued only once — subsequent calls return INVALID_STATE because the request is already ISSUED. If the signing step downstream fails after issuance, the credentialId remains on the request and can be retrieved via Get Request.
Verifier
The Verifier API manages selective-disclosure verification sessions. A verifier creates a challenge specifying which credential type and fields are required. The holder presents their credential at the challenge URL, and the verifier polls the challenge status to learn the result.
Create Challenge
/api/credential-issuer/verifier/challengesRequires: manage:credential-issuerCreate a new verification challenge. Returns a QR URL that the holder scans or follows to
present their credential. The challenge expires after ttlSeconds if no response
is received (default 300 seconds).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
verifierName | string | Yes | Display name of the verifying party shown to the holder |
credentialType | string | Yes | The credential type being requested (must match a template type) |
requestedFields | string[] | Yes | The specific field keys the verifier needs disclosed |
ttlSeconds | integer | No | Challenge lifetime in seconds (default: 300) |
Request body example
{
"verifierName": "Acme HR Portal",
"credentialType": "VerifiableId",
"requestedFields": ["fullName", "employeeId", "department"],
"ttlSeconds": 600
}Success response
{
"data": {
"id": "cldchall_ghi012",
"challenge": "clpwx9q2a4mnbfrt",
"verifierName": "Acme HR Portal",
"credentialType": "VerifiableId",
"requestedFields": ["fullName", "employeeId", "department"],
"qrUrl": "https://auris.altovar.net/api/credential-issuer/verify/clpwx9q2a4mnbfrt",
"status": "PENDING",
"holderDid": null,
"disclosedFields": null,
"verifiedAt": null,
"createdAt": "2025-05-01T11:00:00Z",
"expiresAt": "2025-05-01T11:10:00Z"
},
"error": null
}The qrUrl is a public endpoint — no authentication is required for the holder to present their credential at this URL. Encode it as a QR code or deep-link for wallet apps.
Error codes
| Code | HTTP | Description |
|---|---|---|
VALIDATION | 400 | verifierName, credentialType, or requestedFields is missing |
Get Challenge
/api/credential-issuer/verifier/challenges/[id]Requires: manage:credential-issuerRetrieve the current state of a verification challenge. If the challenge is in PENDING
status and the current time is past expiresAt, the status is automatically
transitioned to EXPIRED before the response is returned.
Success response — verified
{
"data": {
"id": "cldchall_ghi012",
"challenge": "clpwx9q2a4mnbfrt",
"status": "VERIFIED",
"holderDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"disclosedFields": {
"fullName": "Jane Doe",
"employeeId": "EMP-00042",
"department": "Engineering"
},
"verifiedAt": "2025-05-01T11:03:22Z",
"expiresAt": "2025-05-01T11:10:00Z"
},
"error": null
}Success response — expired
{
"data": {
"id": "cldchall_ghi012",
"challenge": "clpwx9q2a4mnbfrt",
"status": "EXPIRED",
"holderDid": null,
"disclosedFields": null,
"verifiedAt": null,
"expiresAt": "2025-05-01T11:10:00Z"
},
"error": null
}Error codes
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Challenge does not exist or belongs to a different tenant |
Poll the challenge status endpoint after presenting the QR code to the holder. A typical polling interval is 2–3 seconds. Stop polling when the status transitions to VERIFIED or EXPIRED.
List Verification History
/api/credential-issuer/verifier/historyRequires: manage:credential-issuerReturns a paginated list of all verification challenges for the tenant, including completed, expired, and pending sessions. Useful for compliance auditing and usage analytics.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page |
Success response
{
"data": {
"data": [
{
"id": "cldchall_ghi012",
"challenge": "clpwx9q2a4mnbfrt",
"verifierName": "Acme HR Portal",
"credentialType": "VerifiableId",
"requestedFields": ["fullName", "employeeId", "department"],
"status": "VERIFIED",
"holderDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"verifiedAt": "2025-05-01T11:03:22Z",
"createdAt": "2025-05-01T11:00:00Z",
"expiresAt": "2025-05-01T11:10:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
},
"error": null
}Each history item contains challenge, credentialType, holderDid, verifiedAt (ISO string or null), createdAt, and expiresAt. Status values are uppercase: PENDING, VERIFIED, EXPIRED.
Complete Workflow Example
This walkthrough shows the full issuance and verification flow.
Step 1: Create a template
POST /api/credential-issuer/templates
Content-Type: application/json
Authorization: Bearer <token>
x-tenant: acme-corp
{
"name": "Employee Badge",
"type": "EmployeeBadge",
"schemaFields": [
{ "key": "fullName", "label": "Full Name", "fieldType": "text", "required": true },
{ "key": "badgeId", "label": "Badge ID", "fieldType": "text", "required": true },
{ "key": "hireDate", "label": "Hire Date", "fieldType": "date", "required": true }
]
}Step 2: Create a request for a specific employee
POST /api/credential-issuer/requests
Content-Type: application/json
Authorization: Bearer <token>
x-tenant: acme-corp
{
"templateId": "cldtempl_abc123",
"subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"subjectData": {
"fullName": "Jane Doe",
"badgeId": "BADGE-9901",
"hireDate": "2024-09-01"
}
}Step 3: Approve the request
PATCH /api/credential-issuer/requests/cldreq_def789/review
Content-Type: application/json
Authorization: Bearer <token>
x-tenant: acme-corp
{ "action": "approve" }Step 4: Issue the credential
POST /api/credential-issuer/requests/cldreq_def789/issue
Authorization: Bearer <token>
x-tenant: acme-corpResponse:
{ "data": { "credentialId": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, "error": null }Step 5: Create a verification challenge
POST /api/credential-issuer/verifier/challenges
Content-Type: application/json
Authorization: Bearer <token>
x-tenant: acme-corp
{
"verifierName": "Building Access System",
"credentialType": "EmployeeBadge",
"requestedFields": ["fullName", "badgeId"],
"ttlSeconds": 120
}Step 6: Poll for verification result
GET /api/credential-issuer/verifier/challenges/cldchall_ghi012
Authorization: Bearer <token>
x-tenant: acme-corpPoll every 2–3 seconds until status is VERIFIED or EXPIRED.
Permissions Reference
| Permission | Description |
|---|---|
manage:credential-issuer | Full access to templates, requests, review actions, issuance, and verifier challenges |
Related
- Credential Issuer Concept — Architecture, lifecycle model, and use cases
- Roles & Permissions API — Assign
manage:credential-issuerto operators - Audit Logs API — Review credential operations in the event log