API General Principles
This page describes the conventions shared by all Secure Keystore REST endpoints: transport rules, authentication, the encryption envelope, error codes and common types. Terms are explained in the Glossary.
The REST API is the target design. The endpoints are not implemented yet.
Endpoints:
- PowerAuth Unit API (
/v1/pa) - EUDI API (
/v1/eudi) - Backend Keys API (
/v1/backend)
Common Conventions
| Rule | Detail |
|---|---|
| Base path | Every endpoint is under /v1. All operations are POST except GET /v1/backend/keys/{keyId}. |
| Media type | Requests and responses are application/json; errors are application/problem+json. An unknown route or method, a POST without Content-Type: application/json, or an unsupported Accept is 400 invalid_request. |
| Request hygiene | Host must be in keystore.api.allowed-hosts. An Origin outside keystore.api.allowed-origins, or Sec-Fetch-Site: cross-site, is 400 invalid_request. |
| No retries | The Wallet Integration Server must not retry a user call: each signed request can be used only once (jti). |
| Lost responses | The keystore stores no responses. See If a Response Is Lost. |
| Strict JSON | Unknown members, duplicate members, trailing data and type coercion (such as a number for a string) are 400 invalid_request. |
| Body limit | 96 KB (keystore.api.max-body-size), measured on the encrypted body where there is one. A larger body is refused before it is read (detail request body too large). |
| Response headers | Every response carries Cache-Control: no-store. Every /v1 response, errors included, also carries a random X-Request-Id, which is written to the request log. |
| Binary encoding | base64url without padding (JOSE) for the signed request and the values of the unit and EUDI endpoints. Standard Base64 (PowerAuth) for the user-signing public keys and all backend-key values. |
If a Response Is Lost
- Registration: revoke the unit and start a new activation.
- Unit revocation: repeat it;
409 unit_revokedmeans the revocation succeeded. - User call: send a new signed request, with a new
jti. - Backend key creation: the
keyIdis generated by the keystore and no lookup endpoint exists, so a lost response leaves a resident key that cannot be addressed through the API. Retrying creates another key; ask the operator to remove the orphaned key from the HSM.
Authentication
Signed Request (User Endpoints)
The plaintext of every encrypted user request is a signed request: a JWS in the general JSON serialization (RFC 7515, section 7.2.1) over one payload. It carries one signature per device key stored for the unit: ES384 for a classical activation, and ES384 plus ML-DSA-65 or ML-DSA-87 for a hybrid one. The PowerAuth mobile SDK builds exactly this object in one call, after one vault unlock (PIN or biometry); see Create JSON Web Signature with Device Private Key for Android and iOS:
calculateJwsSignature(context, authentication, claims, "sk-request+jwt", false, PowerAuthSignatureKeyId.DEVICE, listener)
In this call:
DEVICEsigns with every device key of the activation;falseselects the JSON serialization, because the compact form allows only one signature;"sk-request+jwt"becomes the header’styp.
The SDK writes the header as exactly typ and alg, and an ES384 signature as raw R‖S.
{
"payload": "<base64url of the claims>",
"signatures": [
{"protected": "<base64url of {\"typ\":\"sk-request+jwt\",\"alg\":\"ES384\"}>", "signature": "<base64url>"},
{"protected": "<base64url of {\"typ\":\"sk-request+jwt\",\"alg\":\"ML-DSA-87\"}>", "signature": "<base64url>"}
]
}
Claims:
{
"aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures",
"sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
"iat": 1793271600,
"jti": "q3Vb8Kx0TfW2nLc7RgJ1sA",
"request": {"items": ["…"]}
}
| Part | Field | Rule |
|---|---|---|
| protected header | typ |
Exactly sk-request+jwt. |
| protected header | alg |
ES384 (raw R‖S) for the ECDSA device key; ML-DSA-65 or ML-DSA-87 (pure ML-DSA) for the ML-DSA device key. No other header member is allowed. |
| signatures | Exactly one per device key stored for the unit, each algorithm once. Each is verified with the unit’s stored device key of that algorithm. | |
| claims | aud |
The called route with the unit id filled in, for example /v1/eudi/units/2a7d9c14-…/key-binding-signatures. |
| claims | sub |
The unit id. Must equal the path unitId and the activation of the envelope’s temporary key. |
| claims | iat |
Unix seconds of the signing. Accepted from 5 minutes before to 1 minute after the keystore’s clock (defaults). |
| claims | jti |
At least 16 random bytes, base64url, 22 to 64 characters. Single use per unit. |
| claims | request |
The operation’s request members, documented per endpoint. |
Notes on the claims:
- No other claim is allowed, and the JSON is strict (no duplicate members).
- The wallet sets
iatitself, in seconds, because the SDK’s JWS signing does not add or correct it. Take it from the SDK’s synchronized time (getTimeSynchronizationService().getCurrentTime(), milliseconds, so divide by 1000) and synchronize the time before signing; an unsynchronized SDK falls back to the phone clock, and a wrongiatis rejected as403 invalid_user_authorization. - The keystore remembers each
jti, shared by all instances, until itsiatis no longer accepted, and refuses a repeated one.
A signed request that does not parse, a missing, extra or invalid signature, a claim that does not match, an iat outside the window, or a repeated jti is 403 invalid_user_authorization. A schema violation inside request is 400 invalid_request. After these checks, the keystore asks PowerAuth Server for the activation status; see Request Checks. Unit registration and revocation carry no signed request.
Backend Callers: Bearer Token, Later Mutual TLS
The Wallet Integration Server’s unit calls and the backend components’ key calls carry no user authorization. Instead, the keystore authenticates the backend caller: with a bearer token per client now, and with a mutual TLS client certificate in production later. Each client is configured with its domains and whether it may register and revoke units.
- A call without valid credentials gets
401 backend_unauthenticated. - A call for a domain of another client gets
403 backend_forbidden. - A unit call by a client that may not manage units gets
403 backend_forbidden.
End-to-End Encryption Envelope
The envelope encrypts the user requests and their responses: pa/units/{unitId}/signatures, wallet-instance-keys, wallet-instance-attestations, credential-keys and key-binding-signatures. It carries the signed request; the members documented per endpoint are the signed request’s request claim.
Encryption header. Every encrypted request carries PowerAuth’s encryption header. It names the activation whose temporary key encrypted the body. The SDK’s encryptor sets it for activation-scope encryption (requestHeaders):
X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==",
activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
- A missing or empty header, or one longer than 1024 characters, is
400 invalid_request. - A header that does not parse, or an
activation_idother than the pathunitId, is403 invalid_user_authorization. - An application key of another application is refused by PowerAuth Server when it decrypts:
400 invalid_request, detailbody: encryption.
Encrypted request (EncryptedRequest, exactly these four members):
| Member | Type | Constraints | Meaning |
|---|---|---|---|
temporaryKeyId |
string | 1 to 64 of [0-9A-Za-z-]; a UUID in practice |
The temporary key the wallet obtained for this activation from the Wallet Integration Server’s /pa/v4/keystore/create (PowerAuth RESTful integration), which calls PowerAuth Server’s POST /rest/v4/keystore/create; unrelated to Secure Keystore. |
encryptedData |
string | standard Base64 | PowerAuth AEAD output, in this order: the 12-byte nonce, the 32-byte tag, then the ciphertext. Its length is 4 × ceil((44 + plaintext bytes) / 3). |
nonce |
string | standard Base64, at most 64 characters | The envelope nonce; single use at PowerAuth Server. |
timestamp |
integer (int64) | at least 1 | Unix milliseconds of the encryption. |
Encrypted response (EncryptedResponse, only on 2xx) has two members:
encryptedData(string, standard Base64): the response, encrypted under the request’s temporary key and nonce;timestamp(integer, int64): Unix milliseconds of the encryption.
Freshness. A request is fresh while its temporary key is valid (powerauth.service.temporaryKey.validity, default 5 minutes, by PowerAuth Server’s clock) and its signed request’s iat is within the keystore’s window. An expired, unknown or foreign temporary key is refused by PowerAuth Server.
Sample (a key-binding-signatures request with one item):
POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures HTTP/1.1
X-PowerAuth-Encryption: PowerAuth version="4.0", …(as above)
{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8JUVt+VOdOUZHYmvZ8TDRL…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}
HTTP/1.1 200 OK
{"encryptedData":"r7Kq0VtM3yHc9XwB2nE6LdA4sZpJ8uGf…","timestamp":1793271600187}
Possible Error Codes
Every error response is an application/problem+json body (RFC 9457). The members come in the order of the table below; absent members are omitted.
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "invalid_user_authorization"
}
| Attribute | Type | Description |
|---|---|---|
type* |
String |
Always about:blank. |
title* |
String |
The HTTP reason phrase of the status, for example Forbidden. |
status* |
Integer |
The HTTP status code. |
detail |
String |
Only for some errors: the failing field and rule, never a value. |
code* |
String |
The error code, which tells the cases apart (see the table below). |
503 responses carry Retry-After in whole seconds, at least 1 (keystore.api.retry-after, default 5 seconds). Error responses are never encrypted.
The codes below are generic: an endpoint returns those its page lists. Codes that only some endpoints return (unit_not_found, unit_already_exists, unit_revoked, activation_state_invalid, wrapped_key_invalid, key_not_found, key_revoked) are listed in the error tabs of each endpoint: PowerAuth Unit API, EUDI API, Backend Keys API.
| HTTP | Error Code | Description |
|---|---|---|
| 400 | invalid_request |
The request breaks a schema, size or transport rule. See the list below. |
| 401 | backend_unauthenticated |
Unit and backend-key calls: a missing, malformed or unknown bearer token. Sent with WWW-Authenticate: Bearer, before the body is read. |
| 403 | invalid_user_authorization |
The user request is not authorized. See the list below. |
| 403 | backend_forbidden |
A backend-key call for a domain of another client, or a unit call by a client that may not manage units. |
| 500 | internal_error |
A token or key configuration fault, or an unexpected error; no detail. |
| 503 | hsm_unavailable |
The HSM is unavailable. |
| 503 | authorization_unavailable |
PowerAuth Server is unreachable, timed out, or returned an error other than a refusal. |
400 invalid_request covers:
- a schema or pattern violation, or an unknown member;
- a body over the limit;
- a request refused by the common conventions (route, method,
Content-Type,Accept,Host,Origin,Sec-Fetch-Site); - a refused envelope;
- a refused
publicJwk, PoP orproof; - a
keyCountabove the cap, or a key type the domain does not accept.
403 invalid_user_authorization covers:
- an encryption header that names another activation;
- a signed request that does not parse, a missing, extra or invalid signature, an
audorsubthat does not match, aniatoutside the window, or a reusedjti; - device keys of the unit that the device-key policy refuses;
- on user signing, stored device keys that differ from those sealed in a wrapped key;
- an activation that PowerAuth Server reports as not
ACTIVE, of another application, with other device keys, or that it refuses.
Common Types
| Type | JSON type | Constraints | Meaning |
|---|---|---|---|
UnitId |
string | canonical lowercase UUID (36 characters) | The unit id, equal to the PowerAuth activation id. |
WrappedKey |
string | 1 to 1366 characters; compact JWE with an empty second segment | An EUDI wrapped key (format), P-256, without kty. Store and send it unchanged; never parse it. |
UserSigningWrappedKey |
string | 1 to 9605 characters; compact JWE with an empty second segment | A user-signing wrapped key, whose kty header is its key type. At most 1383 characters for ECDSA_P384. |
UserSigningKeyType |
string | ECDSA_P384, MLDSA_65, MLDSA_87 |
Key type of a user-signing key, named like PowerAuth’s key types. |
Sha256Digest |
string | 43 base64url characters, canonical (padding bits zero) | A 32-byte SHA-256 that the wallet computed over content it showed to the user. |
Signature |
string | 86 base64url characters | ES256 signature, raw R‖S, base64url. |
EcPublicJwk |
object | exactly kty (EC), crv (P-256), x and y (43 base64url characters each); a point on the curve |
A public P-256 key. A private member such as d is refused. |