EUDI API
The EUDI API creates the wallet instance key and attestation, the credential keys with a key attestation, and the key-binding signatures of an EU Digital Identity wallet. See EUDI Wallet Flows for how the calls fit together and API General Principles for the shared conventions.
Target design; the endpoints are not implemented yet.
All four endpoints share these rules:
- Caller: the Wallet Integration Server, forwarding the wallet’s request unchanged.
- Path parameter:
unitId(UnitId). - Encryption: the request and the success response are encrypted (see the envelope).
- Authorization: a vault unlock and device signatures. The plaintext is a signed request, signed by every device key of the unit, whose
audis the path with the unit id filled in. The request parameters documented below are itsrequestclaim.
Every check that needs no HSM runs before the HSM is used: the signed request, the schema, the unit state, the activation status, and the checks of the proof-of-possession (PoP) JWTs. The samples show the claims of the signed request and the decrypted response.
Error Handling
Generic error codes (validation, authentication, HSM, PowerAuth Server, internal) are listed once in Possible Error Codes. Each endpoint below shows its error responses as Response 4xx and Response 5xx tabs, with a sample problem body and the possible codes.
Wallet Instance Services
post /v1/eudi/units/{unitId}/wallet-instance-keys Create the WIA Key
Creates one WALLET_INSTANCE key: the key that a wallet instance attestation (WIA) names in cnf. Each call creates a new key.
Request
Request Headers
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
The activation_id must equal the path unitId; see the envelope.
Path Params
| Param | Type | Description |
|---|---|---|
unitId* |
UnitId |
The unit id. |
Request Body
The request claim is exactly the empty object {}; any member is 400.
The Claims tab shows the claims of the signed request; the Envelope tab shows the request on the wire, the signed request encrypted in the envelope.
{
"aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/wallet-instance-keys",
"sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
"iat": 1793271600,
"jti": "q3Vb8Kx0TfW2nLc7RgJ1sA",
"request": {}
}
POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/wallet-instance-keys HTTP/1.1
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}
Response
The decrypted response; on the wire the body is an EncryptedResponse (see the envelope).
{
"publicJwk": {
"kty": "EC",
"crv": "P-256",
"x": "ZwvVwPh77m4omLkSfP6hXFxcw4MS4ylNbNbiWMAKN_s",
"y": "FwIsmFW2stvG5CN5OVQ42KH3gvAsx-Fv2XnsB_F6VU4"
},
"wrappedKey": "eyJ0eXAiOiJzay13cmFwcGVkLWtleStqd2UiLCJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidmVyIjoiMSIsImtpZCI6IjEiLCJpc3MiOiJzZWN1cmUta2V5c3RvcmUifQ..…"
}
Description of Response Params
| Attribute | Type | Description |
|---|---|---|
publicJwk |
EcPublicJwk |
The public key of the WIA key; it becomes the WIA’s cnf. |
wrappedKey |
WrappedKey |
The WIA key, purpose WALLET_INSTANCE, sealed to the unit’s device keys. |
The request is invalid or refers to an unknown object.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"code": "invalid_request"
}
Possible error codes:
invalid_request- The request fails a schema, pattern, encoding or transport rule (see Common Conventions).unit_not_found- The unit does not exist.
The caller is not authorized for this request.
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "invalid_user_authorization"
}
Possible error codes:
invalid_user_authorization- The encryption header, the signed request or the activation status refuses the request.
The request conflicts with the state of the unit, the key or the wrapped key.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "unit_revoked"
}
Possible error codes:
unit_revoked- The unit is revoked.
An internal error occurred.
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"code": "internal_error"
}
Possible error codes:
internal_error- A token or key configuration fault, or an unexpected error; no detail.
A dependency is unavailable; retry after the number of seconds in Retry-After.
{
"type": "about:blank",
"title": "Service Unavailable",
"status": 503,
"code": "hsm_unavailable"
}
Possible error codes:
hsm_unavailable- The HSM is unavailable.authorization_unavailable- PowerAuth Server is unreachable, timed out, or returned an error other than a refusal.
post /v1/eudi/units/{unitId}/wallet-instance-attestations Issue a WIA and Sign Its Proofs of Possession
Checks the unsigned client-attestation PoP JWTs, signs them with the WIA key and verifies the signatures against publicJwk. Only then does it issue a WIA (oauth-client-attestation+jwt, signed with sign-eudi-ecdsa-wia). The call succeeds completely or not at all.
Request
Request Headers
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
The activation_id must equal the path unitId; see the envelope.
Path Params
| Param | Type | Description |
|---|---|---|
unitId* |
UnitId |
The unit id. |
Request Body
The Claims tab shows the claims of the signed request (the members below are its request claim); the Envelope tab shows the request on the wire, the signed request encrypted in the envelope.
{
"aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/wallet-instance-attestations",
"sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
"iat": 1793271600,
"jti": "Hn4cR1xVt9QpZ0eKw2LmYa",
"request": {
"publicJwk": {
"kty": "EC",
"crv": "P-256",
"x": "ZwvVwPh77m4omLkSfP6hXFxcw4MS4ylNbNbiWMAKN_s",
"y": "FwIsmFW2stvG5CN5OVQ42KH3gvAsx-Fv2XnsB_F6VU4"
},
"wrappedKey": "eyJ0eXAiOiJzay13cmFwcGVkLWtleStqd2UiLCJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidmVyIjoiMSIsImtpZCI6IjEiLCJpc3MiOiJzZWN1cmUta2V5c3RvcmUifQ..…",
"walletVersion": "2.4.1",
"popJwts": ["eyJ0eXAi….eyJpc3Mi…", "eyJ0eXAi….eyJpc3Mi…"]
}
}
POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/wallet-instance-attestations HTTP/1.1
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}
Request Params
| Attribute | Type | Description |
|---|---|---|
publicJwk* |
EcPublicJwk |
Public key of the WIA key as returned by wallet-instance-keys; becomes the WIA’s cnf, rebuilt from the validated coordinates. |
wrappedKey* |
WrappedKey |
Purpose WALLET_INSTANCE of this unit; the wrapped WIA key that signs the PoPs. |
walletVersion* |
String |
The app version, MAJOR.MINOR.PATCH with 1 to 4 digits each. Becomes wallet_version; nobody verifies it. |
popJwts* |
String[] |
1 or 2 unsigned PoP JWTs, BASE64URL(header).BASE64URL(payload), each 3 to 2048 characters. One per call to the authorization server (PAR, token request). |
Each PoP is checked:
- Encoding: both segments are canonical base64url without padding, and both decode to strict UTF-8 JSON without duplicate members.
- Header:
typisoauth-client-attestation-pop+jwt,algisES256, and there is nocrit. - Payload:
issequals the configured WIA client id;audis a string of 1 to 512 characters;jtiis a string of 1 to 256 characters;iatis an integer. - Time: the keystore does not check the PoP’s time; the authorization server does.
Other members, such as the authorization server’s challenge, are allowed.
Response
The decrypted response; on the wire the body is an EncryptedResponse (see the envelope).
{
"attestation": "eyJ0eXAiOiJvYXV0aC1jbGllbnQtYXR0ZXN0YXRpb24rand0Ii….eyJzdWIi….<signature>",
"pops": ["eyJ0eXAi…(header).eyJhdWQi…(payload).G0HQt7YD…", "eyJ0eXAi….eyJhdWQi….k88XMXa-…"]
}
Description of Response Params
| Attribute | Type | Description |
|---|---|---|
attestation |
String |
The WIA: a compact JWT (typ oauth-client-attestation+jwt, ES256, x5c), valid 10 minutes by default. |
pops |
String[] |
The signed PoP JWTs (header.payload.signature), one per popJwts entry, in request order. |
WIA payload members, in order:
iss, the wallet provider (keystore.eudi.wia.issuer, required): OAuth Attestation-Based Client Authentication draft 07 requires it, and later drafts and TS3 allow it;sub,cnf;iat, rounded down to the minute, andexp;wallet_name,wallet_version,wallet_solution_certification_information;client_status.
The request is invalid or refers to an unknown object.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"code": "invalid_request"
}
Possible error codes:
invalid_request- The request fails a schema, pattern, encoding or transport rule (see Common Conventions).unit_not_found- The unit does not exist.
The caller is not authorized for this request.
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "invalid_user_authorization"
}
Possible error codes:
invalid_user_authorization- The encryption header, the signed request or the activation status refuses the request.
The request conflicts with the state of the unit, the key or the wrapped key.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "unit_revoked"
}
Possible error codes:
unit_revoked- The unit is revoked.wrapped_key_invalid- A wrapped key is tampered, of another unit or purpose, malformed inside, or does not sign for the WIA key’spublicJwk. One byte-identical body for every cause.
An internal error occurred.
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"code": "internal_error"
}
Possible error codes:
internal_error- A token or key configuration fault, or an unexpected error; no detail.
A dependency is unavailable; retry after the number of seconds in Retry-After.
{
"type": "about:blank",
"title": "Service Unavailable",
"status": 503,
"code": "hsm_unavailable"
}
Possible error codes:
hsm_unavailable- The HSM is unavailable.authorization_unavailable- PowerAuth Server is unreachable, timed out, or returned an error other than a refusal.
Credential Services
post /v1/eudi/units/{unitId}/credential-keys Create Credential Keys with a Key Attestation
Creates keyCount CREDENTIAL keys and one key attestation (key-attestation+jwt, signed with sign-eudi-ecdsa-ka) over them, bound to the issuer’s c_nonce. On request it also builds the OpenID4VCI proof JWT (proof type jwt), so no further call is needed. Keys, attestation and proof are atomic.
The key attestation’s key_storage and user_authentication members state how the keys are protected and how the user is authenticated, as certified for the wallet provider.
Request
Request Headers
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
The activation_id must equal the path unitId; see the envelope.
Path Params
| Param | Type | Description |
|---|---|---|
unitId* |
UnitId |
The unit id. |
Request Body
The Claims tab shows the claims of the signed request (the members below are its request claim); the Envelope tab shows the request on the wire, the signed request encrypted in the envelope.
{
"aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/credential-keys",
"sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
"iat": 1793271600,
"jti": "b7TzQ0aPq1Wm4sXeN8yKcg",
"request": {
"keyCount": 2,
"cNonce": "tZignsnFbp",
"proof": {"aud": "https://issuer.example.com", "iss": "https://wallet.example.com"}
}
}
POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/credential-keys HTTP/1.1
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}
Request Params
| Attribute | Type | Description |
|---|---|---|
keyCount* |
Integer |
1 to 16 and at most keystore.keys.max-count (default 16), otherwise 400 (detail keyCount: Max). |
cNonce* |
String |
1 to 512 printable ASCII characters without space, " and \; the issuer’s c_nonce, embedded verbatim as the nonce of the attestation and of the proof. |
proof |
Object |
Asks for the proof JWT. Members aud and iss, see below. |
The proof object has exactly these members:
aud(required): the Credential Issuer identifier, an absolutehttpsURL with a host and no fragment, 1 to 512 characters;iss(optional): the wallet’sclient_id, 1 to 512 printable ASCII characters.
Response
The decrypted response; on the wire the body is an EncryptedResponse (see the envelope).
{
"keys": [
{
"publicJwk": {
"kty": "EC",
"crv": "P-256",
"x": "qGo2fJa7Oo_n5CI96iVoPx-0KZVyAZ0d6QdUPBeBvoo",
"y": "iFDMVPNKcuzOIT8tp5wN8hVipiKUgtoSjL7AyREj_DE"
},
"wrappedKey": "eyJ0eXAiOiJzay13cmFwcGVkLWtleStqd2UiLCJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidmVyIjoiMSIsImtpZCI6IjEiLCJpc3MiOiJzZWN1cmUta2V5c3RvcmUifQ..…"
}
],
"keyAttestation": "eyJ0eXAiOiJrZXktYXR0ZXN0YXRpb24rand0Ii….eyJpYXQiOjE3OTMyNzE2MDAs….<signature>",
"proof": "eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2Ii….eyJhdWQiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIs….<signature>"
}
Description of Response Params
| Attribute | Type | Description |
|---|---|---|
keys |
Object[] |
1 to 16 {publicJwk, wrappedKey} objects, in the order of attested_keys. |
keyAttestation |
String |
The KA: a compact JWT (typ key-attestation+jwt, ES256, x5c), valid 10 minutes by default. |
proof |
String |
Only when requested: the compact proof JWT, signed by key 0. |
KA payload members, in order: iat, exp, nonce (the cNonce), attested_keys, key_storage, user_authentication, the certification URL, and the status entry key_storage_status.
The proof JWT has:
- header:
typopenid4vci-proof+jwt,algES256,jwk(equal toattested_keys[0]) andkey_attestation(this response’skeyAttestation); - payload:
aud,iss(if given),nonce(thecNonce) andiat(the KA’siat).
Key 0 signs the proof in the HSM before it is wrapped, and the keystore verifies the signature before it responds. One proof covers the whole batch: it carries the key attestation in its header, and OpenID4VCI 1.0 (Appendix F.1) has the issuer issue a credential for each key in the attestation’s attested_keys.
The request is invalid or refers to an unknown object.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"code": "invalid_request"
}
Possible error codes:
invalid_request- The request fails a schema, pattern, encoding or transport rule (see Common Conventions).unit_not_found- The unit does not exist.
The caller is not authorized for this request.
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "invalid_user_authorization"
}
Possible error codes:
invalid_user_authorization- The encryption header, the signed request or the activation status refuses the request.
The request conflicts with the state of the unit, the key or the wrapped key.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "unit_revoked"
}
Possible error codes:
unit_revoked- The unit is revoked.
An internal error occurred.
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"code": "internal_error"
}
Possible error codes:
internal_error- A token or key configuration fault, or an unexpected error; no detail.
A dependency is unavailable; retry after the number of seconds in Retry-After.
{
"type": "about:blank",
"title": "Service Unavailable",
"status": 503,
"code": "hsm_unavailable"
}
Possible error codes:
hsm_unavailable- The HSM is unavailable.authorization_unavailable- PowerAuth Server is unreachable, timed out, or returned an error other than a refusal.
post /v1/eudi/units/{unitId}/key-binding-signatures Sign Key-Binding JWTs
Signs 1 to 16 SHA-256 hashes the wallet computed (key-binding JWTs, mdoc device authentication) with CREDENTIAL keys. All or nothing: every wrapped key is opened and checked before any is unwrapped.
Request
Request Headers
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
The activation_id must equal the path unitId; see the envelope.
Path Params
| Param | Type | Description |
|---|---|---|
unitId* |
UnitId |
The unit id. |
Request Body
The Claims tab shows the claims of the signed request (the members below are its request claim); the Envelope tab shows the request on the wire, the signed request encrypted in the envelope.
{
"aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures",
"sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
"iat": 1793271600,
"jti": "Zx9fLm2Rk0VbT4qWc6NsPe",
"request": {
"items": [
{"wrappedKey": "eyJ0eXAiOiJzay13cmFwcGVkLWtleStqd2UiLCJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwidmVyIjoiMSIsImtpZCI6IjEiLCJpc3MiOiJzZWN1cmUta2V5c3RvcmUifQ..…", "signingInput": "mpDFaLPskYQgCKSSzGyIvcXnasdgqBJctGdctEpUXL8"}
]
}
}
POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures HTTP/1.1
Content-Type: application/json
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==", activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}
Request Params
| Attribute | Type | Description |
|---|---|---|
items* |
Object[] |
1 to 16 items, the signatures to make. |
items[].wrappedKey* |
WrappedKey |
Purpose CREDENTIAL of this unit; the credential key. |
items[].signingInput* |
Sha256Digest |
The SHA-256 digest of the KB-JWT’s JWS signing input. |
Items may contain only wrappedKey and signingInput; any other member is 400.
Response
The decrypted response; on the wire the body is an EncryptedResponse (see the envelope).
{
"signatures": ["eUQ-4L-9nH9BkEMb_4KUatehhLGbSdxr9h9xfbZpSNqAiosLoPcgsInse2kA0dPMCWVYM4gD1mK9A8xiMprNrw"]
}
Description of Response Params
| Attribute | Type | Description |
|---|---|---|
signatures |
Signature[] |
1 to 16 values, one ES256 R‖S signature per item, in request order. |
The request is invalid or refers to an unknown object.
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"code": "invalid_request"
}
Possible error codes:
invalid_request- The request fails a schema, pattern, encoding or transport rule (see Common Conventions).unit_not_found- The unit does not exist.
The caller is not authorized for this request.
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "invalid_user_authorization"
}
Possible error codes:
invalid_user_authorization- The encryption header, the signed request or the activation status refuses the request.
The request conflicts with the state of the unit, the key or the wrapped key.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "unit_revoked"
}
Possible error codes:
unit_revoked- The unit is revoked.wrapped_key_invalid- A wrapped key is tampered, of another unit, purpose, owner, key type, format version or generation, or malformed inside; in a later version also too old (maximum age). One byte-identical body for every cause.
An internal error occurred.
{
"type": "about:blank",
"title": "Internal Server Error",
"status": 500,
"code": "internal_error"
}
Possible error codes:
internal_error- A token or key configuration fault, or an unexpected error; no detail.
A dependency is unavailable; retry after the number of seconds in Retry-After.
{
"type": "about:blank",
"title": "Service Unavailable",
"status": 503,
"code": "hsm_unavailable"
}
Possible error codes:
hsm_unavailable- The HSM is unavailable.authorization_unavailable- PowerAuth Server is unreachable, timed out, or returned an error other than a refusal.