OEM integrator API · European Digital Identity Wallet backend

Wallet API — embed the CodeB Web Wallet backend in your native mobile app.

Twenty-five documented endpoints across four capability groups (A: 10, B: 2, C: 3, D: 10). OEM integrators building native iOS and Android applications call the same backend that powers our browser-based European Digital Identity Wallet (web-wallet.html) — the difference is that the native app implements the client-side crypto with the platform Keychain / Keystore instead of WebCrypto, and manages its own persistence instead of IndexedDB. The wire protocol is identical.

Hosting. Default OEM tier is shared managed hosting on https://phone.aloaha.com/. Each OEM receives a dedicated OIDC client_id that scopes their app's users and credentials from every other tenant on the platform. Per-OEM subdomain and on-premise deployments are available as tiered options — contact info@aloaha.com for terms.
Auth model. Every non-public endpoint requires an OIDC access token in the Authorization: Bearer <token> header. The token is introspected against /oidc.ashx?action=introspect; the resolved sub keys the user's per-app credentials and vault. The OEM's client_id appears in the token's azp claim and is included in the storage-key hash for the vault (D2/D3), providing cross-OEM isolation on shared managed hosting as of 2026-08-12.
Wallet Attestation (WA / WIA). The /wallet-attestation.ashx endpoint (C3) mints a HAIP §5.11 wallet attestation: a JWT with typ=wallet-attestation+jwt in the header, per-tenant HS256-signed, binding the wallet's public JWK to the acting OIDC sub, 24-hour expiry. The OEM app presents it to verifiers as the OAuth-Client-Attestation header.
Native-parity gaps — what your app must implement locally. The web wallet uses several browser primitives that native apps must substitute with platform equivalents. Summarised inline at each endpoint; a summary at the bottom of the page lists them together.
Related artefacts. Cookbooks (in preparation): native credential import, native credential presentation, WA onboarding, native vault sync, self-issue from OIDC. Machine-readable OpenAPI 3.1 spec and Postman collection follow. See wallet-oem.html for the OEM licensing landing page.

A. Credential import — OID4VCI holder side

Fetching a credential from an issuer (either an external one like issuer.eudiw.dev, or our own vci.ashx). Ten endpoints wrap the OID4VCI 1.0 FINAL flow with pre-auth grant, authorization-code grant + PAR + PKCE, and RFC 7591 dynamic client registration.

GET <issuer>/.well-known/openid-credential-issuer Open #

Discovers the issuer's credential_endpoint, nonce_endpoint, credential_configurations_supported, authorization_servers. Public per OID4VCI §7.

Response (excerpt)

{
  "credential_issuer": "https://issuer.example",
  "credential_endpoint": "https://issuer.example/credential",
  "nonce_endpoint": "https://issuer.example/nonce",
  "authorization_servers": ["https://issuer.example"],
  "credential_configurations_supported": {
    "eu.europa.ec.eudi.pid_mdoc": { "format": "mso_mdoc", "doctype": "eu.europa.ec.eudi.pid.1" },
    "urn:eudi:pid:1":              { "format": "dc+sd-jwt", "vct": "urn:eudi:pid:1" }
  }
}
Native parity: a plain HTTPS fetch. No local crypto involved.

GET <as>/.well-known/oauth-authorization-server Open #

RFC 8414 authorization-server metadata: authorization_endpoint, token_endpoint, pushed_authorization_request_endpoint, registration_endpoint. Fallback if absent: /.well-known/openid-configuration.

Native parity: plain HTTPS fetch. Prefer OAuth AS metadata over OIDC metadata for spec-strict clients.

POST <as>/register Openexternal issuers only #

RFC 7591 dynamic client registration. Present at external issuers (e.g. issuer.eudiw.dev) whose registration_endpoint metadata field advertises this URL. Not exposed on CodeB's own vci.ashx — for OEM apps against our issuer, the OEM's OIDC client_id is issued during onboarding and does not need to be re-registered per install. Discover via A2 metadata; if the field is absent, the issuer does not support dynamic registration.

Request body (when supported by the external issuer)

{
  "client_name": "<OEM app name>",
  "redirect_uris": ["<custom-scheme>://cb-oidc"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "application_type": "native"
}
Native parity: use your app's custom URL scheme as redirect_uris[0]. Persist the returned client_id in secure preferences (Keychain on iOS, EncryptedSharedPreferences on Android). Web wallet caches in localStorage per issuer host.

POST <as>/par Open (PKCE) #

RFC 9126 Pushed Authorization Request. Public with PKCE + client_id. Fallback path if issuer omits PAR: <issuer>/vci.ashx?par.

Form body

client_id=<client>
response_type=code
redirect_uri=<scheme>://cb-oidc
code_challenge=<base64url(sha256(code_verifier))>
code_challenge_method=S256
scope=openid
credential_configuration_ids=<credConfigId>
state=<random>
issuer_state=<from-offer>
authorization_details=<json>

Response

{ "request_uri": "urn:ietf:params:oauth:request_uri:...", "expires_in": 60 }
Native parity: generate code_verifier and state as CSPRNG-random URL-safe strings; SHA-256 the verifier for the challenge. Store the verifier under state in in-memory app state (web wallet uses sessionStorage). After PAR returns, redirect to <authz_endpoint>?client_id=...&request_uri=... using ASWebAuthenticationSession (iOS) or Custom Tabs (Android).

POST <issuer>/token Open (PKCE) #

Two grant flavours. Pre-authorized code: grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code, pre-authorized_code, tx_code?. Authorization code: grant_type=authorization_code, code, redirect_uri, code_verifier, client_id.

Response

{ "access_token": "ey...", "token_type": "Bearer", "expires_in": 300, "c_nonce": "..." }
Native parity: HTTPS POST with form body. Keep access_token in a KeychainAccessible / Keystore-protected value, never plain UserDefaults / SharedPreferences.

POST <issuer>/nonce Open #

OID4VCI 1.0 FINAL nonce endpoint. Public. Returns a fresh c_nonce for the credential proof.

Native parity: straightforward. Some legacy issuers return the initial c_nonce on the token response — treat this endpoint as authoritative but fall back to the token response if absent.

POST <issuer>/credentialSD-JWT VC Bearer #

Issue an SD-JWT credential. Body carries the wallet's proof_type=jwt containing an OpenID4VCI proof-JWT signed by the wallet key.

Request body

{
  "format": "dc+sd-jwt",
  "vct": "urn:eudi:pid:1",
  "proof": {
    "proof_type": "jwt",
    "jwt": "<openid4vci-proof-jwt: ES256 signed by wallet key, contains { aud, iat, nonce }>"
  }
}

Response

{ "credential": "<sd-jwt string>", "format": "dc+sd-jwt" }
Native parity: the proof-JWT is signed on-device with the wallet's ECDSA-P256 key. Generate keypair in SecKeyCreateRandomKey(kSecAttrTokenIDSecureEnclave) on iOS (or software-backed EC key + Keychain), or KeyPairGenerator.getInstance("EC", "AndroidKeyStore") with KeyGenParameterSpec.Builder on Android. Header MUST include typ=openid4vci-proof+jwt, alg=ES256, jwk=<public JWK>. Payload MUST include iss=client_id, aud=credential_issuer, iat, nonce=c_nonce.

POST <issuer>/credentialmDoc Bearer #

Same endpoint as A7 but with format=mso_mdoc. Response is a CBOR-encoded IssuerSigned structure (base64-url).

Request body

{
  "format": "mso_mdoc",
  "doctype": "eu.europa.ec.eudi.pid.1",
  "proof": { "proof_type": "jwt", "jwt": "..." }
}
Native parity: the returned CBOR must be persisted verbatim; do not re-encode. Native mDoc libraries (e.g. iOS Sparkle-Motion CBOR, Android Cbor4j) can parse the IssuerSigned map. Web wallet stores as base64url string; native can store as raw Data/byte[].

GET <credential_offer_uri> Open #

Fetch by-reference credential offer when the deep-link uses credential_offer_uri= instead of an inline credential_offer=. Returns the offer JSON.

Native parity: straight fetch. The deep-link arrives via UIApplicationDelegate.openURL (iOS) or intent filter (Android). Parse both haip-vci://, openid-credential-offer://, or the offer's canonical scheme.

GET <issuer>/.well-known/jwt-vc-issuer + jwks_uri Open #

Used when verifying the issuer signature on a persisted SD-JWT. The issuer's JWT-VC issuer document + JWKS.

Native parity: verify the SD-JWT issuer signature on-device before storing. Reject if the issuer JWKS does not contain the kid or x5c from the SD-JWT header.

B. Self-issue from OIDC identity

The signed-in user's OIDC profile drives an on-server minting: no external issuer needed. Two endpoints on our own vci.ashx.

GET /vci.ashx?available-attributes=1 Bearer #

Lists which credential types the signed-in user is eligible for, based on their OIDC profile completeness and tenant configuration.

Response

{
  "subject": "<OIDC sub>",
  "credentials": [
    { "vct": "urn:eudi:pid:1", "ready": true,  "missing": [],           "display": "Personal Identification Data" },
    { "vct": "https://aloaha.com/vc/employee_id",
                                "ready": false, "missing": ["employee_number"], "display": "Employee ID" }
  ]
}
Native parity: after OIDC login, call once to enumerate available VCs. Show only ready:true as import candidates in your UI. Use missing (list of missing claims) to explain the block to the user.

POST /vci.ashx?issue-in-session=1 Bearer #

Server mints a credential using the signed-in user's OIDC profile as source. Requires the wallet's public JWK in cnf.jwk so the issued credential is bound to the wallet key. Returns an SD-JWT with disclosures.

Request body

{ "vct": "urn:eudi:pid:1", "cnf": { "jwk": <wallet public JWK> } }

Response

{
  "credentials": [ { "credential": "<sd-jwt-with-disclosures>" } ],
  "credential":  "<sd-jwt-with-disclosures>",
  "vct":         "urn:eudi:pid:1",
  "format":      "dc+sd-jwt"
}
Native parity: generate the wallet keypair (Secure Enclave / Android Keystore); export the public JWK for cnf.jwk; persist the private key handle keyed by credential id. credentials is an array of objects each carrying credential (OID4VCI 1.0 FINAL §8.3 shape); credential as a scalar is emitted alongside for backwards compatibility.

C. Credential presentation — OID4VP holder side

Responding to a Verifier's presentation request. Three endpoints cover the JAR fetch (browsers only), the direct-post response, and Wallet Attestation issuance.

GET /oidc.ashx?action=jar-proxy&url=<request_uri> Session (browser-only) #

Same-origin proxy that returns the verifier's signed JAR (application/oauth-authz-req+jwt). Exists to bypass browser CORS on cross-origin request_uri fetches. Auth is same-origin session (browser cookies); no Bearer required today. Server-side hardening (as of 2026-08-12): caller must be HTTPS; target must be HTTPS and public-routable (host string check + DNS-resolved IP check); HTTP redirects refused (single-hop only, no auto-follow); upstream error bodies logged internally but NOT returned to the caller.

Native parity: do NOT use. Native apps have no CORS — fetch request_uri directly. This endpoint exists solely for the browser wallet.

POST <verifier response_uri> Open (form) + #

Two response shapes, selected by the JAR's response_mode:

  • direct_post: form fields vp_token, state, optional mdoc_generated_nonce.
  • direct_post.jwt: form field response = JWE (ECDH-ES + A128GCM) whose plaintext is the same JSON. Optional mdoc_generated_nonce lives in the JWE's apu header per OID4VP 1.0 FINAL §8.1.

Optional header OAuth-Client-Attestation: <WA JWT> when the verifier requires HAIP §5.11 wallet attestation.

Native parity: for direct_post.jwt, generate an ephemeral P-256 keypair on-device; do ECDH-ES with the verifier's ephemeral public key (from JAR client_metadata.jwks); derive the CEK via Concat KDF (NIST SP 800-56A); encrypt with AES-128-GCM. Same wire format as web wallet uses via WebCrypto. Native libraries: iOS CryptoKit.HKDF + AES.GCM; Android javax.crypto.

POST /wallet-attestation.ashx Bearer #

Mints a Wallet Attestation JWT (WIA / WA per HAIP §5.11) bound to the wallet's public key. Cached client-side for 23 hours. Under the OEM tier, the issuer key is the OEM's own KYB-attested key.

Request body

{
  "walletPublicJwk": { "kty":"EC","crv":"P-256","x":"...","y":"..." },
  "nonce": "<from verifier request>"
}

Response

{ "wallet_attestation": "<JWT>", "expires_in": 86400 }
Native parity: attach the platform app-attestation (Apple App Attest assertion / Google Play Integrity token) as a Bearer companion or in a header your OEM tenant is configured to expect. The KYB step wires which app-attestation your OEM tenant will accept.

D. Wallet operations

Vault sync (encrypted server backup), narrow-scope session tokens, mDoc verification, and tenant DID discovery.

POST /wallet-session-token.ashx Bearer #

Mints a narrow-scope HS256-signed token used only for vault push / pull / delete. Trades the full OIDC access token for a short-lived storage-scoped token whose payload carries the caller's OEM azp so wallet-backup can preserve per-OEM isolation.

Response

{
  "access_token": "<HS256 JWT>",
  "token_type":   "Bearer",
  "expires_in":   600,
  "aud":          "wallet-backup",
  "iat":          <unix>,
  "exp":          <unix>
}

The minted JWT's payload additionally includes iss=codeb-wallet-session, sub, tenant, jti, azp, scope="wallet-backup:save wallet-backup:load wallet-backup:delete".

Native parity: straightforward. Refresh before each vault sync attempt or on 401. The narrow token is HS256 to a per-tenant HMAC key that only wallet-backup.ashx knows; do not attempt to introspect it against your OIDC provider.

POST /wallet-backup.ashx?action=save Bearer #

Push the encrypted vault blob to the server. The server stores it opaquely and never sees the plaintext.

Request body (opaque to server)

{
  "v": 1,
  "wrappedDeks": [ { "kek_id": "...", "dek_cipher": "..." } ],
  "cipher": "<AES-GCM ciphertext of the vault contents>",
  "updatedUtc": "2026-08-12T18:30:00Z",
  "hint": "<optional device hint for conflict resolution>"
}
Native parity: the KEK is derived from a device-attested factor. Web wallet uses WebAuthn PRF + device-bound HKDF; native app substitutes with iOS LAContext-gated key from Secure Enclave (via SecAccessControl(.biometryCurrentSet)), or Android BiometricPrompt-gated key from Keystore. Never decrypt on-server; the server has no keys.

GET /wallet-backup.ashx?action=load Bearer #

Pull the encrypted vault blob. Response is the same opaque JSON persisted at save time. 404 {"error":"not_found"} if the user has never saved a vault.

Native parity: after fetch, decrypt using the same KEK the local device holds. Merge with local state via updatedUtc comparison. Do not overwrite a newer local vault silently — surface a conflict prompt.

POST /oidc.ashx?action=mdoc-cred-verify Bearer #

Server-side verification of a stored mDoc credential. Used by the wallet to display trust indicators without shipping the mDoc verify stack into the client.

Request body

{
  "issuerSigned": "<base64url CBOR IssuerSigned>",
  "docType": "eu.europa.ec.eudi.pid.1"
}

Response

{
  "ok": true,
  "sig_ok": true,
  "chain_ok": true,
  "issuer_cn": "...",
  "doc_type": "eu.europa.ec.eudi.pid.1",
  "mso_valid_from": "2026-08-01T00:00:00Z",
  "mso_valid_until": "2027-08-01T00:00:00Z"
}
Native parity: cache results 5 minutes per credential id, keyed by sha256(issuerSigned). Native apps that ship an mDoc library MAY verify locally instead; the endpoint remains available as a fallback.

GET /business-wallet.ashx?action=identity Open (same-origin) #

Tenant DID discovery: did_web, did_key, did_jwk, did_webvh, and wallet_id. Populates the tenant identity chips in the wallet UI.

Native parity: use for tenant-branding chips in your UI. Cache aggressively; the DIDs rotate rarely.

GET /oidc.ashx?action=end_session&post_logout_redirect_uri=<url> Open #

RP-initiated logout. Sweeps the server-side session and returns 200 text/html containing a Clear-Site-Data header plus a JS/meta redirect to post_logout_redirect_uri. Not a 302 — the response has to reach the browser so Clear-Site-Data can wipe cookies/storage before navigation.

Native parity: in-app browser (ASWebAuthenticationSession / Custom Tabs) opens this URL. Native apps CANNOT rely on Clear-Site-Data to wipe local storage — on sign-out, ALSO clear the wallet's own vault + WA cache + credential-key handles locally, matching the two-sided sweep pattern in memory codeb-signout-two-sided-sweep.

POST /wallet-backup.ashx?action=delete Bearer #

Delete the server-side vault blob. Requires the caller to POST {"confirm":"delete"} to reduce accidental wipes. Symmetric with save/load; keyed on the same (sub, tenant, azp).

Native parity: expose as a “delete cloud backup” option in the wallet's settings screen. Do NOT chain to local-vault delete without an explicit confirm dialog.

POST /vci.ashx?self-offer Bearer #

Member-facing offer mint: builds an OID4VCI credential-offer URL that the signed-in user can consume from another device (e.g. desktop generates the QR, phone wallet scans it). Complement to B2 (which issues in-session on the same device).

Native parity: use to hand off issuance from a desktop or web session to a native wallet on the phone.

POST /oidc.ashx?action=data-deletion-request Bearer #

ARF 3.0 TS7 data-deletion right endpoint. The signed-in wallet user can request server-side deletion of their PII footprint (session logs, vault, cached credentials, WA issuance history).

Native parity: surface in the wallet's privacy settings screen. Required for GDPR / ARF conformance.

GET /oidc.ashx?action=dpa-report Bearer (operator) #

ARF 3.0 TS8 DPA-report endpoint. Operator-only (superuser); produces a Data Protection Authority-facing report of user activity with a 24-month cap. Not called from the wallet; documented for OEM operators.

Native parity: N/A — consumed by tenant admins from a dashboard, not the wallet itself.

Native-parity summary — what your app must implement locally

The web wallet uses browser primitives; native apps substitute with platform equivalents. This inventory maps each substitution.

WebCrypto crypto.subtle substitutions

  • ECDSA-P256 keygen + sign (proof-JWT, KB-JWT, WA signing) → iOS SecKeyCreateRandomKey + SecKeyCreateSignature(.ecdsaSignatureMessageX962SHA256); Android KeyPairGenerator("EC","AndroidKeyStore") + Signature.getInstance("SHA256withECDSA").
  • SHA-256 (PKCE challenge, KB-JWT hash) → iOS CryptoKit.SHA256; Android MessageDigest.getInstance("SHA-256").
  • ECDH-ES + A128GCM (direct_post.jwt encryption) → iOS CryptoKit.KeyAgreement.ecdhKeyAgreement + Concat KDF + AES.GCM.SealedBox; Android KeyAgreement.getInstance("ECDH") + Cipher.getInstance("AES/GCM/NoPadding").
  • PKCE code_verifier / state → CSPRNG via SecRandomCopyBytes / SecureRandom.

Storage substitutions

  • IndexedDB (vault, credentials, WA cache, settings) → iOS Core Data or SQLite with SQLCipher; Android Room with SQLCipher. Keys wrapped by Keychain / Keystore-held KEKs.
  • localStorage (dynamic-client-reg cache) → iOS UserDefaults (non-secret) or Keychain (if you consider client_id sensitive); Android EncryptedSharedPreferences.
  • sessionStorage (PKCE verifier) → in-memory app state; drop on backgrounding.

Browser-only endpoints your app should skip

  • /oidc.ashx?action=jar-proxy (C1) — only for browser CORS bypass. Fetch request_uri directly.
  • Any use of navigator.credentials (WebAuthn PRF) — substitute with platform biometric-gated Keychain / Keystore keys.
  • Service-worker offline caching — substitute with your app's own resource cache.

Common headers to send from your native client

  • User-Agent: <OEM app name>/<version> (native; iOS 17.4; iPhone15,3) — helps our support triage and lets us apply per-OEM rate limits accurately.
  • X-CodeB-Client-Id: <your OEM OIDC client_id> — optional but recommended for support diagnostics.
  • Accept-Language: <user locale> — some error responses localise error_description.

OEM landing page → CSC v2 signing API Contact for OEM tenant + WA KYB