Skip to Content

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 token
  • x-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

CodeHTTPDescription
NOT_FOUND404The requested resource does not exist or belongs to a different tenant
VALIDATION400A required field is missing or has an invalid value
INVALID_STATE409The 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

GET/api/credential-issuer/templatesRequires: manage:credential-issuer

Returns a paginated list of all credential templates for the tenant. Supports full-text search across template names and descriptions.

Query parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page
searchstring—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

POST/api/credential-issuer/templatesRequires: manage:credential-issuer

Create a new credential template. The template starts active and accepts issuance requests immediately upon creation.

Request body

FieldTypeRequiredDescription
namestringYesHuman-readable template name
typestringYesCredential type label (e.g., VerifiableId, ProfessionalCertificate)
descriptionstringNoOptional description
schemaFieldsTemplateSchemaField[]NoField definitions (defaults to [])

TemplateSchemaField object

PropertyTypeDescription
keystringMachine-readable identifier used as the JSON key in subjectData
labelstringHuman-readable field label
fieldType"text" | "date" | "number" | "email"Data type for validation and display
requiredbooleanWhether 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

CodeHTTPDescription
VALIDATION400name or type is missing

Get Template

GET/api/credential-issuer/templates/[id]Requires: manage:credential-issuer

Retrieve a single credential template by its ID.

Success response — same shape as the template object in Create Template.

Error codes

CodeHTTPDescription
NOT_FOUND404Template does not exist or belongs to a different tenant

Update Template

PATCH/api/credential-issuer/templates/[id]Requires: manage:credential-issuer

Partially 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)

FieldTypeDescription
namestringNew template name
descriptionstringNew description
typestringNew credential type label
schemaFieldsTemplateSchemaField[]Replacement field definitions (replaces entire array)
isActivebooleanSet 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

CodeHTTPDescription
NOT_FOUND404Template does not exist
VALIDATION400An invalid value was provided for a field

Delete Template

DELETE/api/credential-issuer/templates/[id]Requires: manage:credential-issuer

Permanently 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

CodeHTTPDescription
NOT_FOUND404Template 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

GET/api/credential-issuer/requestsRequires: manage:credential-issuer

Returns a paginated list of credential requests for the tenant, optionally filtered by status.

Query parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger10Items per page
statusPENDING | 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

POST/api/credential-issuer/requestsRequires: manage:credential-issuer

Submit 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

FieldTypeRequiredDescription
templateIdstringYesThe ID of the credential template to issue from
subjectDidstringYesThe subject’s Decentralized Identifier (DID)
subjectDataobjectYesField 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

CodeHTTPDescription
VALIDATION400templateId, subjectDid, or subjectData is missing
NOT_FOUND404Template does not exist, belongs to a different tenant, or is inactive

Get Request

GET/api/credential-issuer/requests/[id]Requires: manage:credential-issuer

Retrieve a single credential request by its ID.

Success response — same shape as a request object in List Requests.

Error codes

CodeHTTPDescription
NOT_FOUND404Request does not exist or belongs to a different tenant

Review Request

PATCH/api/credential-issuer/requests/[id]/reviewRequires: manage:credential-issuer

Approve 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

FieldTypeRequiredDescription
action"approve" | "reject"YesThe review decision
rejectionNotestringNoExplanation 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

CodeHTTPDescription
NOT_FOUND404Request does not exist
VALIDATION400action is missing or not one of "approve" / "reject"
INVALID_STATE409Request is not in PENDING status and cannot be reviewed

Issue Credential

POST/api/credential-issuer/requests/[id]/issueRequires: manage:credential-issuer

Issue 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

CodeHTTPDescription
NOT_FOUND404Request does not exist
INVALID_STATE409Request 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

POST/api/credential-issuer/verifier/challengesRequires: manage:credential-issuer

Create 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

FieldTypeRequiredDescription
verifierNamestringYesDisplay name of the verifying party shown to the holder
credentialTypestringYesThe credential type being requested (must match a template type)
requestedFieldsstring[]YesThe specific field keys the verifier needs disclosed
ttlSecondsintegerNoChallenge 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

CodeHTTPDescription
VALIDATION400verifierName, credentialType, or requestedFields is missing

Get Challenge

GET/api/credential-issuer/verifier/challenges/[id]Requires: manage:credential-issuer

Retrieve 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

CodeHTTPDescription
NOT_FOUND404Challenge 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

GET/api/credential-issuer/verifier/historyRequires: manage:credential-issuer

Returns 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

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items 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-corp

Response:

{ "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-corp

Poll every 2–3 seconds until status is VERIFIED or EXPIRED.


Permissions Reference

PermissionDescription
manage:credential-issuerFull access to templates, requests, review actions, issuance, and verifier challenges