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
groupsshape under/api/v1/oid4vci/...is the one to reach for in most cases. One group carries sharedsourceId/phase/timestamp/retentiononce 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 IDKAttributeRecord/LookupKeybody 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.
| Method | Path | Description |
|---|---|---|
| POST | /oid4vci/sessions | Initialise an issuance pipeline session |
| GET | /oid4vci/sessions/{correlationId}/attributes | Read the current attribute state of a session |
| POST | /api/v1/oid4vci/sessions/{correlationId}/attributes | Contribute attributes (compact groups shape) |
| POST | /oid4vci/sessions/{correlationId}/attributes | Contribute attributes (verbose per-attribute shape) |
| GET | /oid4vci/sessions/{correlationId}/completeness | Evaluate which credentials are ready to assemble |
| POST | /oid4vci/sessions/{correlationId}/completeness | Evaluate completeness with an explicit phase to re-run |
| POST | /oid4vci/sessions/{correlationId}/approve | Approve issuance for a session in AWAITING_APPROVAL |
| POST | /oid4vci/sessions/{correlationId}/fail | Mark 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):
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/oid4vci/offers | Create a credential offer with the compact preSeededGroups shape (EDK enterprise) |
| POST | /oid4vci/backend/credential/offers | Create 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/sessionsto allocate the session, then either let the wallet drive the flow orPOST /oid4vci/sessions/{correlationId}/attributesif 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 toPOST /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}/attributesfor the bag andGET /oid4vci/sessions/{correlationId}/completenessfor the per-credential readiness. - A human review step is needed for a credential whose binding has
approvalRequired = true:POST /oid4vci/sessions/{correlationId}/approvefrom the reviewer's UI. - A source is stuck and you want to abandon the issuance:
POST /oid4vci/sessions/{correlationId}/failwith 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.
- Overview
- API reference
- gRPC
- Command
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
| Field | Required | Notes |
|---|---|---|
sourceId | yes | Same string applied to every attribute and lookup key in the group (becomes AttributeRecord.sourceId / LookupKey.producedBy). |
phase | yes | Single PipelinePhase per group. A request with attributes for two phases needs two groups. |
timestamp | no | ISO-8601 instant; defaults to server clock when omitted. |
sourceDetail | no | Source-specific reference (IDV node id, endpoint URL). |
retention | no | Polymorphic by kind: ephemeral, session, or retained. Per-item override > set-level retention > group-level retention > semantic-derived > default. |
semanticAttributeSets | no | Array of per-set containers (bundleId, optional version, optional set-level retention, values, attributes). |
attributes | no | Group-level escape hatch (path-keyed items). |
lookupKeys | no | { 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:
- Verbose body
- Compact V1 body
{
"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"
}
]
}
{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"timestamp": "2026-05-18T01:31:00Z",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith"
}
}
],
"lookupKeys": [
{ "name": "employee_id", "value": "E1042" }
]
}
]
}
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 thesourceId,phase, andtimestampare factored up into the group envelope.
Verbose endpoint
The verbose per-attribute endpoint accepts the IDK
AttributeRecord/LookupKeybody 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.
- Overview
- API reference
- gRPC
- Command
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.
- Overview
- API reference
- Command
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.
- Overview
- API reference
- Command
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).
- Overview
- API reference
- gRPC
- Command
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.
- Overview
- API reference
- gRPC
- Command
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:
401if the callback token is invalid, expired, or itscorrelationIddoes not match the path.400for malformed JSON.
- Overview
- API reference
- gRPC
- Command
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 code | HTTP status |
|---|---|
ILLEGAL_ARGUMENT_ERROR | 400 |
UNAUTHORIZED_ERROR | 401 |
POLICY_DENIED | 403 |
NOT_FOUND_ERROR | 404 |
ALREADY_EXISTS_ERROR | 409 |
COMMAND_DISABLED_ERROR | 503 (a required pipeline command is not wired in this deployment) |
UNKNOWN_ERROR or other | 500 |
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-sessionand 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-attributesand the completeness command: a wider audit/support role, since the response contains decrypted attribute values.- The callback endpoint command
oid4vci.protocol.contribute-via-callbackdoes 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.