Nexiel Credentials lets a Nexiel-vetted institution issue verifiable academic credentials (enrolment proofs, module completions, degree awards) into a student's EUDI wallet. This page walks through the single-credential path end to end: propose a claim template, get it approved, then issue one real credential over OpenID4VCI. Read how credential issuance works first for the concepts (what the credential is, the institution-vetting gate, and what trust it carries) if you have not already.
compliance.nexiel.io is serving /v1/credentials/* in production. Every request and response below is real, executed against a sandbox-mode instance seeded with an approved example institution, so you can see the exact shape you will integrate against before your own institution has cleared vetting. Run your own instance locally to try these same requests today, or issue against production sandbox mode once your API key is provisioned.
Every route below acts on behalf of one academic issuer (institution), and that issuer must already be APPROVEDthrough Nexiel's institution-vetting review, described on how credential issuance works. There is no self-serve API for that step. The examples below use a fake, already-approved example institution (“Nexiel Sandbox University”, obviously not real) seeded directly for this walkthrough.
A template defines one credential type's claim shape for one institution: credentialType (ENROLMENT, MODULE_COMPLETION, or DEGREE_AWARD), a JSON Schema for its claims, and which of those claims are individually disclosable (sdJwtDisclosureFrame). It starts as a DRAFT:
curl -s -X POST https://compliance.nexiel.io/v1/credentials/issue/templates \
-H "Content-Type: application/json" \
-d '{
"academicIssuerId": "7a6dfd13-15ed-4e44-9721-6e60f1369a1a",
"credentialType": "DEGREE_AWARD",
"claimsSchema": {"type": "object", "properties": {"degreeTitle": {"type": "string"}, "awardDate": {"type": "string"}}},
"sdJwtDisclosureFrame": {"_sd": ["degreeTitle", "awardDate"]}
}'{
"id": "46c81db0-8acf-4fa7-8f12-ba9db2ed07b9",
"academicIssuerId": "7a6dfd13-15ed-4e44-9721-6e60f1369a1a",
"credentialType": "DEGREE_AWARD",
"vct": "urn:nexiel:credential:degree-award:v1",
"version": 1,
"claimsSchema": {
"type": "object",
"properties": {
"awardDate": {
"type": "string"
},
"degreeTitle": {
"type": "string"
}
}
},
"sdJwtDisclosureFrame": {
"_sd": [
"degreeTitle",
"awardDate"
]
},
"status": "DRAFT",
"reviewedBy": null,
"reviewedAt": null,
"reviewNotes": null,
"createdAt": "2026-08-25T18:51:55.846Z",
"updatedAt": "2026-08-25T18:51:55.846Z"
}vct is assigned automatically from credentialType: every institution proposing a DEGREE_AWARD template gets the same urn:nexiel:credential:degree-award:v1value, so a verifier's query works against any Nexiel-vetted institution unchanged. A repeat proposal for the same institution and credential type becomes version: 2, never an in-place edit of this row.
curl -s -X POST https://compliance.nexiel.io/v1/credentials/issue/templates/46c81db0-8acf-4fa7-8f12-ba9db2ed07b9/submit{
"id": "46c81db0-8acf-4fa7-8f12-ba9db2ed07b9",
"academicIssuerId": "7a6dfd13-15ed-4e44-9721-6e60f1369a1a",
"credentialType": "DEGREE_AWARD",
"vct": "urn:nexiel:credential:degree-award:v1",
"version": 1,
"claimsSchema": {
"type": "object",
"properties": {
"awardDate": {
"type": "string"
},
"degreeTitle": {
"type": "string"
}
}
},
"sdJwtDisclosureFrame": {
"_sd": [
"degreeTitle",
"awardDate"
]
},
"status": "PENDING_REVIEW",
"reviewedBy": null,
"reviewedAt": null,
"reviewNotes": null,
"createdAt": "2026-08-25T18:51:55.846Z",
"updatedAt": "2026-08-25T18:51:58.112Z"
}A Nexiel reviewer, never the institution's own API key, approves or rejects a PENDING_REVIEW template. Rejecting requires non-empty notes, so the institution gets concrete feedback to revise the claim shape:
curl -s -X POST https://compliance.nexiel.io/v1/credentials/issue/templates/46c81db0-8acf-4fa7-8f12-ba9db2ed07b9/review \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <reviewer_access_token>" \
-d '{"decision": "APPROVED"}'{
"id": "46c81db0-8acf-4fa7-8f12-ba9db2ed07b9",
"academicIssuerId": "7a6dfd13-15ed-4e44-9721-6e60f1369a1a",
"credentialType": "DEGREE_AWARD",
"vct": "urn:nexiel:credential:degree-award:v1",
"version": 1,
"claimsSchema": {
"type": "object",
"properties": {
"awardDate": {
"type": "string"
},
"degreeTitle": {
"type": "string"
}
}
},
"sdJwtDisclosureFrame": {
"_sd": [
"degreeTitle",
"awardDate"
]
},
"status": "APPROVED",
"reviewedBy": "sandbox-reviewer",
"reviewedAt": "2026-08-25T18:52:00.765Z",
"reviewNotes": null,
"createdAt": "2026-08-25T18:51:55.846Z",
"updatedAt": "2026-08-25T18:52:00.765Z"
}Only an APPROVED template can ever be used to issue a real credential. In sandbox mode this route needs no token at all, and reviewedBy is recorded as "sandbox-reviewer". Against a production deployment it requires a Nexiel-staff Keycloak token carrying a compliance-reviewerrole, which an institution's own API key can never satisfy.
GET /v1/credentials/.well-known/openid-credential-issuer is the OpenID4VCI issuer-metadata document a wallet fetches before redeeming any offer. It lists one entry per credential type with at least one APPROVED template anywhere, never one per institution:
curl -s https://compliance.nexiel.io/v1/credentials/.well-known/openid-credential-issuer{
"credential_issuer": "https://compliance.nexiel.io",
"credential_endpoint": "https://compliance.nexiel.io/v1/credentials",
"token_endpoint": "https://compliance.nexiel.io/v1/credentials/token",
"credential_configurations_supported": {
"nexiel-degree-award": {
"format": "dc+sd-jwt",
"vct": "urn:nexiel:credential:degree-award:v1",
"cryptographic_binding_methods_supported": [
"jwk"
],
"credential_signing_alg_values_supported": [
"ES256"
],
"proof_types_supported": {
"jwt": {
"proof_signing_alg_values_supported": [
"ES256"
]
}
}
}
}
}Once a specific credential is ready to hand to a specific student, Nexiel creates an OpenID4VCI offer scoped to it and delivers a redemption link or QR code. Batch issuance (section 6 below) is the real, public way to reach this point for many students at once; this walkthrough starts from a real offer already created for one student to show the redemption side on its own first. The two routes below are real and public: a wallet exchanges the offer's pre-authorized code for an access token, then presents proof it holds the matching private key to receive the signed credential.
curl -s -X POST https://compliance.nexiel.io/v1/credentials/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code" \
--data-urlencode "pre-authorized_code=KZ7Vaw7tNoWnHX8lllxG6hx_zWc1EZaTR6TAQN39_gY"{
"access_token": "lEZLd9KVgvz4jSkhlMKWTP31K4RNwvyURlxw6Wy383s",
"token_type": "bearer",
"expires_in": 300,
"c_nonce": "xYzXH-Ub_byaswqgRnqO5JNzGoaDui8Su4z6xUIx7yM",
"c_nonce_expires_in": 300
}A pre-authorized code is single-use. Presenting an unknown or already-redeemed one returns 400 with the OAuth2 shape {"error":"invalid_grant","error_description":"..."}, never this app's ordinary {"error","message"} shape.
A real EUDI wallet builds a small JWT here, signed with the private key half of the same keypair it will bind the credential to, echoing the c_nonce above. A bare curl client cannot produce that signature itself, so the request below used a real ES256 keypair generated for this walkthrough, the same way a wallet would:
curl -s -X POST https://compliance.nexiel.io/v1/credentials \
-H "Content-Type: application/json" \
-H "Authorization: Bearer lEZLd9KVgvz4jSkhlMKWTP31K4RNwvyURlxw6Wy383s" \
-d '{"format": "dc+sd-jwt", "proof": {"proof_type": "jwt", "jwt": "<wallet-signed proof JWT>"}}'{
"credential": "eyJ0eXAiOiJkYytzZC1qd3QiLCJraWQiOiJkb2NzLWV4YW1wbGUta2V5LTEiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NhbmRib3gtdW5pdmVyc2l0eS5uZXhpZWwtZG9jcy5leGFtcGxlIiwidmN0IjoidXJuOm5leGllbDpjcmVkZW50aWFsOmRlZ3JlZS1hd2FyZDp2MSIsImlhdCI6MTc4NzY4Mzk0NSwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsIngiOiJfZ3I2TER6SlhwZ0dhMzUtelQ2RGQtdUptT1F5WU9tNm9xUWNxMjR6WHE0IiwieSI6IjJnbFduY2tReUNzYnlGVUFTWWUxQjJfeVQ4VmZ4Q0NUdk1nMDVJbzZHc2siLCJjcnYiOiJQLTI1NiJ9fSwic3R1ZGVudEZ1bGxOYW1lIjoiU2FuZGJveCBEb2NzIEdyYWR1YXRlIiwiX3NkIjpbImEwVHVvVC1uWFBpR0lGYkZUUC03VzdBV2ZZZmpwVzRDZU1OVjQ5NVN5N2ciLCJwa1hYYVROM0VGZTd1YUR4MHc3WjFLTF92OXF2b0RpR2ZIWWJQZHJxUG1jIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.ANzyavNtGQIwoqQlmn6CX34lcjaUXh9eSt29FIYc9SWPLL4r99cj2NStqqdFimQeBnVWqlPFBwst-TmdENvdqA~WyJVSUItQnRTSzBtM1lfRWJuLU5tc2hBIiwiYXdhcmREYXRlIiwiMjAyNi0wNi0xNSJd~WyJaczZSblJ5RkljZGxqVjczUjV6SjBRIiwiZGVncmVlVGl0bGUiLCJCU2MgQ29tcHV0ZXIgU2NpZW5jZSAoZG9jcyBleGFtcGxlKSJd~"
}credential is the compact SD-JWT VC: a signed JWS, followed by one ~-separated disclosure per selectively-disclosable claim. This exact string independently verified with openssl dgst -verifyagainst the issuing institution's own public key, outside this codebase entirely, using nothing but the kid in its header. Decoded, its signed body carries iss(the institution's own verified domain), vct, cnf(the wallet's public key), and two _sd digests. Its two disclosures, delivered alongside the signed body, carry the actual degreeTitle and awardDate values.
An access token is single-use for issuance. Presenting the same one again returns 401: {"error":"invalid_token","message":"credential offer \"...\"'s access token has already been used to issue a credential"}.
POST /v1/credentials/issue/batches submits many students for issuance in one call, gated by the same maker-checker discipline as institution vetting: the signatory who prepares a batch can never be the one who approves it. A preparing signatory submits either raw rows or a csvContent string, and Nexiel samples roughly 10 percent of the valid rows for a second signatory to independently re-key and confirm before approval is even possible:
curl -s -X POST https://compliance.nexiel.io/v1/credentials/issue/batches \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <issuer_access_token>" \
-d '{
"academicIssuerId": "7a6dfd13-15ed-4e44-9721-6e60f1369a1a",
"credentialTemplateId": "46c81db0-8acf-4fa7-8f12-ba9db2ed07b9",
"preparedBySignatoryId": "c1c9a6a1-2e11-4a0a-9b1a-1e6a6c9a1a11",
"submissionMethod": "API",
"rows": [
{
"studentFullName": "Sandbox Docs Graduate",
"studentEmail": "graduate@sandbox-university.nexiel-docs.example",
"claimValues": { "degreeTitle": "BSc Computer Science (docs example)", "awardDate": "2026-06-15" }
}
]
}'The response reports each row's status, whether it was sampled for a recheck, and the batch's own status (SUBMITTED, moving to PENDING_APPROVAL once every sampled item is rechecked). A second signatory calls POST .../batches/:id/items/:itemId/recheck for each sampled item, then POST .../batches/:id/approve, which requires a fresh step-up re-authentication assertion and refuses outright if the approving signatory is the same person who prepared the batch, if fewer than two signatories are active for the institution at that exact moment, or if any sampled item was never rechecked or came back mismatched. Approval hands off to a background worker that issues each item's offer the same way the single-credential path above does.
Real and tested end to end, including a real signed credential redeemed by a wallet at the end of a full submit-recheck-approve-issue run. There is no self-serve dashboard workflow for this yet. Today it is API-only, for an institution's own backend to call directly.
POST /v1/credentials/issue/templates honors an Idempotency-Key header: resending the exact same request with the same key returns the original cached response rather than creating a second template. Use a fresh key for a genuinely new proposal.
In sandbox mode, none of the template routes require a token. Against a production deployment, POST/GET .../templates and POST .../templates/:id/submitrequire a Keycloak client-credentials token for the institution's own API key (audience credentials-api, role credentials-api-client), validated per-request via RFC 7662 introspection so a revoked key is rejected immediately. POST .../templates/:id/review requires a separate Nexiel-staff token (role compliance-reviewer) instead, never the institution's own key. Naming an academicIssuerId that exists but is not yours returns the same 404 an unknown id would, never a 403 that would confirm it exists. Authenticated template requests are also rate limited and evaluated for the same volume/geography/behavior-shift anomaly signals as every other Nexiel API. See the Screen API quickstart's Authentication section for exactly how that works. The wallet-facing routes above (POST /v1/credentials/token, POST /v1/credentials) need no Nexiel API key at all: the offer's own single-use secret is the authorization.