Skip to main content
Version: v0.25.0 (Latest)

REST API

The OID4VCI protocol endpoints (/.well-known/openid-credential-issuer, /oid4vci/credential, /oid4vci/deferredCredential, /oid4vci/nonce, /oid4vci/notification, /oid4vci/credentials/offers/{offerId}) come from the IDK and are documented in the IDK issuer guide. They are the wallet-facing surface and you do not call them from your own backend.

The EDK adds a second surface under /oid4vci/sessions/... that backend code, admin tooling, and async upstream systems use to drive an issuance pipeline session: create it, push attributes into it, evaluate its completeness, approve it, mark a source failed, and accept callbacks from async sources. This page documents that surface. The pipeline behaviour these endpoints expose is covered in Attribute Pipeline; read that first if you are new to the EDK pipeline model.

All endpoints below mount under the issuer service's /oid4vci adapter base. The full path is shown for each endpoint. Authentication is the OIDC bearer token (except the callback endpoint, which uses an opaque capability token in the path). The tenant is resolved by the EDK Layer 1 tenant pipeline from the validated JWT, the Host header, or the configured default; it is never read from a client-supplied wire header.

Quick Reference

The EDK exposes the pipeline endpoints in two parallel shapes:

  • The versioned compact groups shape under /api/v1/oid4vci/... is the recommended surface for new integrations. One group carries shared sourceId / phase / timestamp / retention once for every attribute and lookup key inside it, and integrators may reference an OCA bundle by id so the server derives the per-attribute path, value kind, data classification, legal basis, and retention.
  • The legacy unversioned shape under /oid4vci/sessions/... keeps the per-attribute IDK AttributeRecord / LookupKey body verbatim and is preserved for IDK callers.
MethodPathDescription
POST/oid4vci/sessionsInitialise an issuance pipeline session
GET/oid4vci/sessions/{correlationId}/attributesRead the current attribute state of a session
POST/api/v1/oid4vci/sessions/{correlationId}/attributesContribute attributes (compact groups shape)
POST/oid4vci/sessions/{correlationId}/attributesContribute attributes (legacy unversioned shape, preserved for IDK callers)
GET/oid4vci/sessions/{correlationId}/completenessEvaluate which credentials are ready to assemble
POST/oid4vci/sessions/{correlationId}/completenessEvaluate completeness with an explicit phase to re-run
POST/oid4vci/sessions/{correlationId}/approveApprove issuance for a session in AWAITING_APPROVAL
POST/oid4vci/sessions/{correlationId}/failMark a source contribution as failed
POST/api/v1/oid4vci/sessions/{correlationId}/callbacks/{callbackToken}Async-source callback ingress, compact body (capability-token authenticated)
POST/oid4vci/sessions/{correlationId}/callbacks/{callbackToken}Async-source callback ingress, legacy body (capability-token authenticated)

Backend-facing credential-offer endpoints (the EDK rest-tenant module overrides the IDK offer endpoints to be tenant-path-aware, and adds a compact V1 variant that accepts preSeededGroups):

MethodPathDescription
POST/api/v1/oid4vci/offersCreate a credential offer with the compact preSeededGroups shape (EDK enterprise)
POST/oid4vci/backend/credential/offersCreate a tenant-bound credential offer (legacy unversioned, preserved for IDK callers)
GET/oid4vci/backend/credential/offers/{correlationId}Read the status of an offer-creation request

When to Call Which

Most flows do not need every endpoint. The decision tree:

  • A backend system (HR portal, enrolment app) is triggering the issuance: POST /oid4vci/sessions to allocate the session, then either let the wallet drive the flow or POST /oid4vci/sessions/{correlationId}/attributes if you want to push attributes that the IDK protocol handlers will pick up.
  • An upstream system is slow and is configured as ASYNC_CALLBACK: it calls back to POST /oid4vci/sessions/{correlationId}/callbacks/{callbackToken} with the answer when ready.
  • An admin UI needs to show the current state of an in-flight issuance: GET /oid4vci/sessions/{correlationId}/attributes for the bag and GET /oid4vci/sessions/{correlationId}/completeness for the per-credential readiness.
  • A human review step is needed for a credential whose binding has approvalRequired = true: POST /oid4vci/sessions/{correlationId}/approve from the reviewer's UI.
  • A source is stuck and you want to abandon the issuance: POST /oid4vci/sessions/{correlationId}/fail with the source id.

The wallet-facing OID4VCI protocol endpoints continue to work without any of these calls; the pipeline runs on its own as the protocol moves. You only call the pipeline endpoints when you need to inject input from outside the protocol path or to observe and intervene in a session.

Initialise a Session

POST /oid4vci/sessions
Authorization: Bearer <token>
Content-Type: application/json
{
"pipelineConfiguration": {
"pipelineId": "employee-credential-v1",
"sourceBindings": [ ... ],
"claimsBindings": [ ... ],
"expectedInitialLookupKeys": ["invitation_token"]
},
"correlationId": "ord-9af3e2",
"initialAttributes": [],
"initialLookupKeys": [
{
"name": "invitation_token",
"value": "inv-7c1a...",
"type": "BUSINESS_KEY",
"producedBy": { "value": "hr-backend" },
"phase": { "value": "session_init" },
"timestamp": "2026-05-18T01:30:00Z"
}
],
"encryptionMode": { "type": "platform-encrypted" },
"ttlSeconds": 3600
}

Returns 200 OK with the session and correlation ids:

{
"sessionId": "sess_01J...",
"correlationId": "ord-9af3e2"
}

correlationId is the join key with the IDK issuer-core session. When you omit it, the command generates one. initialAttributes and initialLookupKeys are the SESSION_INIT seed; the engine does not run any phase here, the call only allocates the session.

encryptionMode is one of { "type": "plaintext" }, { "type": "platform-encrypted" }, or { "type": "client-bound", "fallbackToPlatform": false }. See Persistence for the trade-offs.

Errors: 400 for malformed input, 409 if a session already exists for the correlation id.

Push Attributes

POST /api/v1/oid4vci/sessions/ord-9af3e2/attributes
Authorization: Bearer <token>
Content-Type: application/json

The compact body carries a list of groups. Every group factors out the shared sourceId, phase, timestamp, sourceDetail, and retention once, then contributes attributes through any of three notations and any lookup keys produced by the same source in the same phase. The HTTP adapter decodes groups into the existing List<AttributeRecord> / List<LookupKey> before delegating to the IDK service command, so the pipeline semantics (Attribute Pipeline) are unchanged.

Notations inside a group

Per-set values map — the recommended notation. Pass an OCA bundle id and a term -> value map; the server resolves the SemanticAttributeDefinition for each term and derives path, valueKind, dataClassification, legalBasis, retentionPolicy, sensitive, and sdPolicy from the bundle. The integrator never spells out a JSON Pointer path or repeats classification metadata.

{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith",
"birth_date": "1990-04-01"
}
}
]
}
]
}

Per-set attributes[] with overrides — same semantic-set container, but every entry can override retention, priority, assurance, verified, sourceDetail, or sdPolicy at the per-attribute level. term is required and path is rejected (the surrounding set supplies the schema):

{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"attributes": [
{ "term": "given_name", "value": "Alice" },
{ "term": "family_name", "value": "Smith" },
{ "term": "nationality", "value": "NL",
"retention": { "kind": "ephemeral" } }
]
}
]
}
]
}

Group-level attributes[] escape hatch — for ad-hoc attributes that are NOT scoped to any semantic set. Every entry is keyed by a raw JSON Pointer path; the decoder skips semantic resolution and uses group-level metadata to fill the envelope. path is required and term is rejected here:

{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"attributes": [
{ "path": "custom.shoe_size", "value": 42 }
]
}
]
}

semanticAttributeSets, group-level attributes, and lookupKeys are independent; any combination (including only lookupKeys) is a valid group.

Realistic multi-set example

A group can carry multiple semantic sets, a group-level escape-hatch attribute, and lookup keys side-by-side. This matches the OpenAPI multiSetWithEscapeHatch example:

{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"retention": { "kind": "session" },
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith"
}
},
{
"bundleId": "org.acme.hr.1",
"values": {
"department_code": "ENG-42",
"employee_status": "ACTIVE"
}
}
],
"attributes": [
{ "path": "custom.shoe_size", "value": 42 }
],
"lookupKeys": [
{ "name": "employee_id", "value": "E1042" },
{ "name": "email", "value": "alice@acme.io" }
]
}
]
}

Group-level fields

FieldRequiredNotes
sourceIdyesSame string applied to every attribute and lookup key in the group (becomes AttributeRecord.sourceId / LookupKey.producedBy).
phaseyesSingle PipelinePhase per group. A request with attributes for two phases needs two groups.
timestampnoISO-8601 instant; defaults to server clock when omitted.
sourceDetailnoSource-specific reference (IDV node id, endpoint URL).
retentionnoPolymorphic by kind: ephemeral, session, or retained. Per-item override > set-level retention > group-level retention > semantic-derived > default.
semanticAttributeSetsnoArray of per-set containers (bundleId, optional version, optional set-level retention, values, attributes).
attributesnoGroup-level escape hatch (path-keyed items).
lookupKeysno{ name, value, type?, metadata?, promotedToAttributePath? }. Provenance fields are filled from the group.

retention shape

{ "kind": "ephemeral" }
{ "kind": "session", "encryptionRequired": true }
{ "kind": "retained", "retentionDays": 30,
"legalBasis": "gdpr_art6_1a_consent",
"regulatoryFramework": "eidas2",
"territoryId": "EU",
"encryptionRequired": true,
"consentObtained": true }

legalBasis, regulatoryFramework, and territoryRestriction accept any string; the IDK value classes carry the well-known constants but jurisdiction-specific values round-trip cleanly.

Response

Returns 200 OK:

{
"sessionId": "sess_01J...",
"status": "PHASE_COMPLETED",
"completedPhases": [
{ "value": "session_init" },
{ "value": "oid4vci_authorization" },
{ "value": "oid4vci_token" },
{ "value": "oid4vci_credential_request" }
]
}

The endpoint runs the named phase end-to-end: decodes the groups into per-attribute records, feeds them in as phase input, runs every source bound to that phase in dependency order, folds the result back into the session bag, and persists. Calling this for the same phase more than once is supported; the engine re-runs the phase with the new input.

If a required source for the phase is missing a lookup key, or if a bundleId cannot be resolved or a term is unknown in the resolved bundle, the call fails with a 400 listing every unresolved item across all sets and the session is unchanged.

Migrating from the legacy shape

The compact body decodes to exactly the same AttributeRecord / LookupKey lists the legacy endpoint produces, so the migration is a wire-format swap. The most common case (one source, one phase, several attributes) collapses into a single group:

{
"phase": { "value": "oid4vci_credential_request" },
"attributes": [
{
"path": "given_name",
"value": { "data": "Alice" },
"sourceId": { "value": "hr-backend" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:00Z"
},
{
"path": "family_name",
"value": { "data": "Smith" },
"sourceId": { "value": "hr-backend" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:00Z"
}
],
"lookupKeys": [
{
"name": "employee_id",
"value": "E1042",
"producedBy": { "value": "hr-backend" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:00Z"
}
]
}

Two integrations worth noting:

  • A request that pushes attributes from two different sources (HR plus a compliance system) needs two groups: one per sourceId. The decoder fans out automatically.
  • An integration that does not yet have a semantic-set bundle id can keep using raw paths under group-level attributes[] (the escape hatch); the only difference from the legacy body is that the sourceId, phase, and timestamp are factored up into the group envelope.

Legacy unversioned endpoint

Legacy unversioned endpoint, preserved for IDK callers. New integrations should use the /api/v1/... shape above.

POST /oid4vci/sessions/ord-9af3e2/attributes
Authorization: Bearer <token>
Content-Type: application/json
{
"phase": { "value": "oid4vci_credential_request" },
"attributes": [
{
"path": "department_code",
"value": { "data": "ENG-42" },
"sourceId": { "value": "hr-backend" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:00Z"
}
],
"lookupKeys": [
{
"name": "employee_id",
"value": "E1042",
"producedBy": { "value": "hr-backend" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:00Z"
}
]
}

Response shape and engine behaviour are identical to the V1 endpoint above.

Read Session Attributes

GET /oid4vci/sessions/ord-9af3e2/attributes
Authorization: Bearer <token>

Returns 200 OK with the current attribute bag, the lookup keys, the status, the completed phases, and the deferral entries:

{
"sessionId": "sess_01J...",
"status": "AWAITING_DEFERRED",
"completedPhases": [ ... ],
"currentPhase": { "value": "oid4vci_credential_request" },
"attributes": [
{
"path": "given_name",
"value": { "data": "Ada" },
"sourceId": { "value": "database" },
"phase": { "value": "oid4vci_credential_request" },
"timestamp": "2026-05-18T01:31:02Z"
}
],
"lookupKeys": [ ... ],
"deferralEntries": {
"credential-EmployeeCredential": {
"sourceId": { "value": "http" },
"reason": "Upstream HR API timed out",
"createdAt": "2026-05-18T01:31:10Z"
}
}
}

The response decrypts the sensitive payload before returning, so any caller with bearer-token access to this endpoint sees the cleartext attributes. Restrict access through the EDK authorization policy.

Evaluate Completeness

GET /oid4vci/sessions/ord-9af3e2/completeness
Authorization: Bearer <token>

Returns per-credential readiness, listing which required attributes are missing for each bound credential:

{
"sessionId": "sess_01J...",
"perCredential": [
{
"credentialBindingId": "EmployeeCredential",
"ready": false,
"missingAttributes": ["start_date"],
"deferralEligible": true
}
]
}

The POST variant accepts a body that names a phase to re-run before evaluating; useful when external input has arrived and you want to force re-evaluation rather than waiting for the next wallet poll.

Approve

POST /oid4vci/sessions/ord-9af3e2/approve
Authorization: Bearer <token>
Content-Type: application/json
{
"approverPrincipalId": "manager@example.com",
"credentialBindingId": "EmployeeCredential",
"decision": "APPROVED",
"reason": "Reviewed and confirmed against case ABC-123"
}

Returns 200 OK with the resulting session status. For a credential binding with approvalRequired = true, the session does not progress to READY until this is called with decision = APPROVED. A decision = REJECTED moves the session to FAILED. The approver identity and reason are recorded in the session's ApprovalState and persisted with the sensitive payload (encrypted at rest).

Fail a Source

POST /oid4vci/sessions/ord-9af3e2/fail
Authorization: Bearer <token>
Content-Type: application/json
{
"sourceId": { "value": "http" },
"reason": "Upstream HR API has been down for 30 minutes"
}

Records that the named source has failed for the session and stops the engine from waiting on it. If the source was required, the session moves to FAILED. Use this from operator tooling when a stuck flow needs to be abandoned.

Create Credential Offer (Compact)

POST /api/v1/oid4vci/offers
Authorization: Bearer <token>
Content-Type: application/json

The EDK enterprise offer endpoint accepts the IDK CreateCredentialOfferArgs fields (issuer id, credential configuration ids, grant flags, offer TTL, scheme, URI lifecycle) plus a preSeededGroups field that takes the same group shape as POST /api/v1/oid4vci/sessions/{correlationId}/attributes. Use it to create an offer and seed attributes in one round-trip:

{
"issuerId": "pid-issuer",
"credentialConfigurationIds": ["PidCredential"],
"preAuthorizedCodeGrant": true,
"authorizationCodeGrant": false,
"txCodeRequired": false,
"offerTtlSeconds": 600,
"preSeededGroups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith",
"birth_date": "1990-04-01"
}
}
]
}
]
}

Returns 201 Created with the offer URL, correlation id, and any minted transaction code.

Behind the scenes the decoder runs the same expansion as ContributeAttributes, then flattens the result into the IDK Map<String, JsonElement> preSeededAttributes shape and hands it to the existing IDK CreateCredentialOfferCommand. The original preSeededGroups JSON is retained on the EDK session record as read-only audit material; the IDK boundary itself sees only the flattened map.

preAuthorizedCodeGrant, authorizationCodeGrant, and txCodeRequired are flat booleans on the compact body. uriLifecycle accepts the strings "SINGLE_USE" (default) or "REUSABLE_FRESH_PER_FETCH" for static-offer scenarios. initialLookupKeys and rateLimit are not on this endpoint: initial lookup keys flow through preSeededGroups[i].lookupKeys; rate-limit configuration sits on the underlying IDK shape only.

Errors: 400 for validation problems including unresolved semantic terms (one envelope lists every miss across all sets).

Async-Source Callback

POST /api/v1/oid4vci/sessions/ord-9af3e2/callbacks/eyJhbGciOiJFUzI1NiIsImtpZCI6Im...
Content-Type: application/json

The callback endpoint accepts the same compact groups body as POST /api/v1/oid4vci/sessions/{correlationId}/attributes. The endpoint pins the phase to DEFERRED server-side; any per-group phase value is ignored, matching the legacy callback behaviour.

{
"groups": [
{
"sourceId": "http",
"phase": "oid4vci_deferred",
"semanticAttributeSets": [
{
"bundleId": "org.acme.hr.1",
"values": {
"start_date": "2026-01-15"
}
}
]
}
]
}

The legacy unversioned callback at POST /oid4vci/sessions/{correlationId}/callbacks/{callbackToken} is preserved for IDK callers and accepts the legacy per-attribute body:

{
"attributes": [
{
"path": "start_date",
"value": { "data": "2026-01-15" },
"sourceId": { "value": "http" },
"phase": { "value": "oid4vci_deferred" },
"timestamp": "2026-05-18T01:32:45Z"
}
],
"lookup_keys": []
}

Returns 200 OK with the updated session status.

This endpoint does not use a bearer token. Instead, the path's callbackToken is an opaque capability artefact the EDK minted when the async source was dispatched. The token-binding cross-check requires the token's embedded correlationId claim to match the path's correlation id. Token validation is handled by the EDK CallbackTokenService (a JWS-token-based implementation ships out of the box; deployments can swap it).

When a CallbackCoordinator is on the classpath, a successful callback also notifies any /credential request that is currently holding open within the source's syncWaitWindow. If the wallet's poll has not yet timed out, the wallet gets the credential synchronously instead of falling through to a deferred response. This is the path that turns slow upstream systems into responsive issuance: the wallet sees a synchronous credential rather than a deferral whenever the callback arrives within a few seconds.

Errors:

  • 401 if the callback token is invalid, expired, or its correlationId does not match the path.
  • 400 for malformed JSON.

Tenant-Aware Paths

The IDK protocol endpoints assume tenant resolution from the host header. The EDK lib-openid-oid4vci-issuer-rest-tenant module also accepts a leading tenant slug:

/{tenantSlug}/oid4vci/credential
/{tenantSlug}/.well-known/openid-credential-issuer

The slug lookup is additive: an as-is host match still wins. Host-only deployments are unaffected by including this module. The tenant slug is resolved by the EDK tenant-resolution layer through RoutableSlugLookup, so the same slug works across all the issuer's tenant-aware adapters.

The tenant_public_endpoint registration (PUT /api/v1/tenants/{tenantId}/public-endpoints/OID4VCI_ISSUER, the tenant admin API documented under tenant administration) is what tells the metadata endpoint which host and path each tenant publishes its issuer on. When a binding exists, the /.well-known/openid-credential-issuer response and the URLs in created offers use the registered host instead of the inbound Host header. This is what lets a single EDK issuer image serve multiple tenants under distinct hostnames.

Error Envelope

Failures use the standard EDK IdkError envelope:

{
"code": "NOT_FOUND_ERROR",
"message": "Pipeline session not found: ord-9af3e2"
}

Mapping:

Error codeHTTP status
ILLEGAL_ARGUMENT_ERROR400
UNAUTHORIZED_ERROR401
POLICY_DENIED403
NOT_FOUND_ERROR404
ALREADY_EXISTS_ERROR409
COMMAND_DISABLED_ERROR503 (a required pipeline command is not wired in this deployment)
UNKNOWN_ERROR or other500

The OID4VCI protocol endpoints continue to use the spec-defined error responses (invalid_credential_request, invalid_proof, and so on) from the IDK; that surface is unchanged.

Authorization

Every pipeline endpoint passes through the EDK PolicyCommandExtension, so authorization policies are evaluated per command. Typical policies:

  • issuance.pipeline.init-session and the create-offer commands: tenant administrators or backend service principals only.
  • issuance.pipeline.contribute-attributes, issuance.pipeline.approve-session, issuance.pipeline.fail-source: roles that match your business model (HR back-office, compliance reviewer, operator).
  • issuance.pipeline.get-session-attributes and the completeness command: a wider audit/support role, since the response contains decrypted attribute values.
  • The callback endpoint command oid4vci.protocol.contribute-via-callback does not go through policy; it is authenticated by the capability token alone, which encodes the scope (this specific session, this specific source).

All command IDs follow the issuance.pipeline.* or oid4vci.protocol.* namespace and are stable contracts for policy.