Nexiel Credentials API

Every endpoint, parameter, and response schema for Nexiel Credentials.

OpenAPI 3.1.0Version 0.2.0https://compliance.nexiel.io

Requests to a production deployment require a bearer token. Mint one via the standard OAuth2 client-credentials grant against https://auth.nexiel.io/realms/nexiel/protocol/openid-connect/token using the client_id/client_secretpair from your dashboard's API Keys page — see this API's own quickstart for the exact curl command.

GET/health

Liveness / service-mode check

Responses

200
503
GET/v1/credentials/.well-known/openid-credential-issuer

OpenID4VCI issuer metadata document

Responses

200
POST/v1/credentials/sandbox/demo-offer

Sandbox-only: creates a real, single-use OpenID4VCI offer from Nexiel's own dedicated sandbox academic issuer

Never available in production mode. Every call ensures a real, APPROVED, branded sandbox academic issuer/template/batch exist (idempotent), then issues a genuinely fresh, real, single-use offer -- the exact same @nexiel/credential-issuance offer/redemption/signing path every real institution batch uses, against synthetic data only.

Responses

200
404
POST/v1/credentials/token

OpenID4VCI pre-authorized_code token exchange

Request body

grant_type"urn:ietf:params:oauth:grant-type:pre-authorized_code"required
pre-authorized_codestringrequired

Responses

200
400
POST/v1/credentials

The OpenID4VCI credential endpoint -- verifies the access token + wallet proof-of-possession, signs, and returns the SD-JWT VC

Request body

format"dc+sd-jwt"required
proofobjectrequired
proof_type"jwt"required
jwtstringrequired

Responses

200
400
401
409
GET/v1/credentials/issue/templatesBearer auth (production mode)

List credential templates, optionally filtered by academicIssuerId/status

In production mode, results are always scoped to the caller's own academicIssuerId (tenant-scope.ts) regardless of the academicIssuerId query filter -- passing a DIFFERENT issuer's id 404s exactly like an unknown one would (never a 403, never a silently-ignored filter that could be mistaken for "this issuer just has no templates"). Omitting the filter lists only the caller's own templates, never every issuer's. Sandbox mode performs no such scoping (no real per-client identity exists there at all).

Parameters

academicIssuerId (query)string
status (query)string

Responses

200
401
404
429
POST/v1/credentials/issue/templatesBearer auth (production mode)

Propose a new credential-template draft for the caller's institution

In production mode, academicIssuerId MUST be the institution the caller's own API key was issued against (tenant-scope.ts resolves this from the caller's Keycloak client_id) -- naming a DIFFERENT (even a real, APPROVED) issuer's id 404s exactly like an unknown one would, never a 403 that would confirm that issuer exists. Sandbox mode performs no such check (no real per-client identity exists there at all).

Request body

academicIssuerIdstringrequired
credentialType"ENROLMENT" | "MODULE_COMPLETION" | "DEGREE_AWARD"required
claimsSchemaunknownrequired

A JSON Schema describing this credential's claims.

sdJwtDisclosureFrameunknownrequired

RFC 9901 disclosure-frame shape (e.g. { "_sd": ["degreeTitle", "awardDate"] }) -- claim names here must never include a protected top-level SD-JWT VC claim (iss/vct/iat/exp/nbf/cnf/sub/status).

Responses

200
400
401
404
409
429
POST/v1/credentials/issue/templates/{id}/submitBearer auth (production mode)

Submits a DRAFT template for Nexiel staff review (DRAFT -> PENDING_REVIEW)

In production mode, only submittable by the template's OWN issuer's API key -- a template that exists but belongs to a different issuer 404s exactly like an unknown template id would (tenant-scope.ts + templates.ts's submitTemplateForReview, same IDOR-safe convention as every other scoped route in this app). Sandbox mode performs no such check.

Parameters

id (path)stringrequired

Responses

200
401
404
409
429
POST/v1/credentials/issue/templates/{id}/reviewBearer auth (production mode)

Nexiel-staff-only: approve or reject a PENDING_REVIEW template. Never callable with an institution's own API key.

Parameters

id (path)stringrequired

Request body

decision"APPROVED" | "REJECTED"required
notesstring

Responses

200
400
401
404
409
POST/v1/credentials/issue/batchesBearer auth (production mode)

Submit a new credential issuance batch (CSV or JSON rows) for maker-checker review

In production mode, academicIssuerId MUST be the institution the caller’s own API key was issued against (same cross-tenant scoping as the template routes) -- naming a different issuer 404s exactly like an unknown one would. Requires the credentials-api-issuer capability role, not just credentials-api-client.

Request body

academicIssuerIdstringrequired
credentialTemplateIdstringrequired
preparedBySignatoryIdstringrequired
submissionMethod"CSV_UPLOAD" | "API"required
csvContentstring

Required (and only meaningful) when submissionMethod is "CSV_UPLOAD".

rowsobject[]

Required (and only meaningful) when submissionMethod is "API".

studentFullNamestringrequired
studentEmailstringrequired
studentDobstring
institutionStudentIdstring
claimValuesobject<string, unknown>required

Responses

200
400
401
404
409
429
GET/v1/credentials/issue/batches/{id}Bearer auth (production mode)

Full batch detail, including every item and its sample/recheck status

Parameters

id (path)stringrequired

Responses

200
401
404
429
POST/v1/credentials/issue/batches/{id}/items/{itemId}/recheckBearer auth (production mode)

A DIFFERENT signatory (the checker) re-keys a sampled item’s claim values for server-side comparison

Parameters

id (path)stringrequired
itemId (path)stringrequired

Request body

recheckingSignatoryIdstringrequired
claimValuesobject<string, unknown>required

Responses

200
400
401
404
409
429
POST/v1/credentials/issue/batches/{id}/approveBearer auth (production mode)

Step-up-gated maker-checker approval -- transitions PENDING_APPROVAL to APPROVED, then hands off to the real background issuance worker

Requires a real, freshly-verified step-up assertion in the x-credentials-step-up-assertion header (see step-up.ts). The dashboard UI that produces this via a genuine Keycloak re-authentication round trip is Phase 5’s job -- this route and approveCredentialBatch both independently verify freshness server-side.

Parameters

id (path)stringrequired

Request body

approvedBySignatoryIdstringrequired

Responses

200
400
401
404
409
429
GET/v1/credentials/verify/sessionsBearer auth (production mode)

List credential verification sessions for the calling employer

Scoped to the calling employer: in production, the bearer token's authenticated identity -- `clientId` in the query string is IGNORED in that case. Never consumes disclosedClaims' one-time delivery, and never triggers a live revocationStatus re-check per row (reports each decision's own last-observed value instead) -- both deliberate "a bulk read must never have expensive/consuming side effects" choices, mirroring apps/verify-api's own LIST route precedent for the AML evidence pack.

Parameters

clientId (query)string
limit (query)string

Responses

200
400
401
POST/v1/credentials/verify/sessionsBearer auth (production mode)

Initiate an EUDI wallet academic-credential verification session (OpenID4VP)

Requires a credentials-api-verifier-scoped API key (an ordinary employer verification key, gated by KYB approval -- never a credentials-api-client or credentials-api-issuer key, both of which are issuer-side and structurally rejected here). Reuses the exact same OpenID4VP session pattern @nexiel/verify already implements for Nexiel Verify, parameterized by checkType (academic-credential | academic-enrolment-status).

Request body

clientIdstring
checkType"academic-credential" | "academic-enrolment-status"required

Responses

200
400
401
409
429
POST/v1/credentials/verify/sessions/{sessionId}/responses

Wallet's direct_post callback for an academic-credential verification session

This route's real authorization is the session's own unguessable state/nonce (OpenID4VP direct_post) -- a wallet holds no Nexiel API key, so no bearer token is required or checked here, exactly like apps/verify-api's identical wallet-callback route.

Parameters

sessionId (path)stringrequired

Request body

statestringrequired
vp_tokenobject<string, string | string[]>required

Responses

200
400
404
409
410
GET/v1/credentials/verify/sessions/{sessionId}Bearer auth (production mode)

Read back a credential verification session's status and (if completed) its decision

The real delivery channel for disclosedClaims (never the wallet's own POST .../responses callback reply) -- delivered at most once (see `claimsDelivery` query parameter). revocationStatus is re-derived LIVE on every single read of THIS route, never cached -- see @nexiel/verify's status-list.ts.

Parameters

claimsDelivery (query)string

Controls whether THIS request may consume the session's one-time disclosed-claims delivery -- mirrors apps/verify-api's identical `evidencePackDelivery` query parameter exactly. "claim" (the default) is the real, client-facing delivery path. "peek" NEVER claims it: `decision.disclosedClaims` is always null in this mode, and the session's one-time delivery window is left untouched.

sessionId (path)stringrequired

Responses

200
401
404