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 compact groups shape under /api/v1/oid4vci/... is the one to reach for in most cases. 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 verbose shape under /oid4vci/sessions/... spells out the per-attribute IDK AttributeRecord / LookupKey body in full, repeating provenance on every item. Both shapes decode to the same records and are equally supported; the verbose shape suits cases where you already hold IDK records or want explicit per-item control.
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 (verbose per-attribute shape)
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, verbose 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 (verbose per-attribute shape)
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

Returns 200 OK with the session and correlation ids. 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.

The body carries the pipelineConfiguration, an optional correlationId (generated when omitted), the SESSION_INIT seed (initialAttributes, initialLookupKeys), the encryptionMode, and an optional ttlSeconds. The call only allocates the session; no phase runs yet.

POST /oid4vci/sessions

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Push Attributes

POST /api/v1/oid4vci/sessions/{correlationId}/attributes contributes attributes to a session. 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 with the session status and the list of completedPhases. 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.

The same contribution in both shapes

The compact body decodes to exactly the same AttributeRecord / LookupKey lists as the verbose body, so the two are interchangeable on the wire. The most common case (one source, one phase, several attributes) collapses into a single compact 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 have a semantic-set bundle id can use raw paths under group-level attributes[] (the escape hatch); the only difference from the verbose body is that the sourceId, phase, and timestamp are factored up into the group envelope.

Verbose endpoint

The verbose per-attribute endpoint accepts the IDK AttributeRecord / LookupKey body directly (the right-hand "Verbose body" in the comparison above). Prefer the compact /api/v1/... shape for the common case; reach for this one when you already hold IDK records or want explicit per-item provenance. Response shape and engine behaviour are identical to the compact endpoint.

This is the one pipeline command behind both shapes: the compact /api/v1 adapter decodes its groups into the flat attributes / lookupKeys lists shown here before invoking it. In process and over gRPC you call it with the flat ContributeAttributesArgs (the correlationId from the path, the phase, and the two lists).

POST /oid4vci/sessions/{correlationId}/attributes

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Read Session Attributes

Returns 200 OK with the current attribute bag, the lookup keys, the status, the completed phases, and the deferral entries. 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.

A read by correlationId (taken from the path). The result carries the attribute bag, lookup keys, status, completed phases, and deferral entries, decrypted.

GET /oid4vci/sessions/{correlationId}/attributes

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Evaluate Completeness

Returns per-credential readiness, listing which required attributes are missing for each bound credential. 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.

Evaluates per-credential readiness for the session named by the path correlationId, listing the missing required attributes and whether each credential is deferral-eligible.

GET /oid4vci/sessions/{correlationId}/completeness

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Approve

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

Records the human review decision for the session (correlationId from the path). decision = APPROVE lets a session whose binding has approvalRequired = true progress to READY; REJECT moves it to FAILED. The decision, reason, and approver are persisted with the encrypted payload.

POST /oid4vci/sessions/{correlationId}/approve

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Fail a Source

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.

Marks the sourceId failed for the session (correlationId from the path), with an optional reason. The engine stops waiting on that source; if it was required, the session moves to FAILED.

POST /oid4vci/sessions/{correlationId}/fail

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

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

The callback endpoint accepts the same compact groups body as POST /api/v1/oid4vci/sessions/{correlationId}/attributes (and a verbose per-attribute body at the unversioned path, just like the contribute endpoint). The endpoint pins the phase to DEFERRED server-side; any per-group phase value is ignored. 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.

The callback is a contribute under capability-token auth rather than a bearer token: the opaque callbackToken in the path is validated and cross-checked against the correlationId, then the same issuance.pipeline.contribute-attributes command runs with the phase pinned to DEFERRED. In process and over gRPC you invoke that command directly with ContributeAttributesArgs.

POST /oid4vci/sessions/{correlationId}/callbacks/{callbackToken}

The full request and response schema is in the API reference, or open the API reference tab to read it inline.

Tenant-Aware Paths

The IDK protocol endpoints assume tenant resolution from the host header. The EDK lib-openid-oid4vci-issuer-rest-tenant module also resolves the tenant from a path slug, so one issuer image can serve many tenants on a shared host. For an issuer whose identifier is https://<host>/{tenantSlug}, the slug sits in two different places. The protocol endpoints take it as a leading segment, but the metadata document follows the OID4VCI 1.1 §13.2 path-aware rule, which inserts /.well-known/openid-credential-issuer between the host and the issuer's path, so the slug ends up as a suffix:

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

So https://issuer.example.com/tenant1 publishes its metadata at https://issuer.example.com/.well-known/openid-credential-issuer/tenant1, not under a leading slug. 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.