Troubleshooting
Find the symptom, then apply the fix. Each cookbook lists only the problems that are specific to its steps; everything else is collected here.
When a wallet is involved, collect its log right after the failure: in the EU Reference Implementation open Setting → Retrieve Logs, in Paradym Wallet open Settings → Export Logs. The wallet compatibility record has details for other wallets. On the EUDIPLO side, Sessions → All Sessions in the Web Client shows the status, and GET /api/session/{id} adds failureCode and errorReason. Remove tokens, keys and personal data before you share logs.
Install and connect
| Symptom | Cause | Fix |
|---|---|---|
eudiplo: command not found after the installer ran | ~/.local/bin is not on your PATH | Add export PATH="$HOME/.local/bin:$PATH" to your shell profile and open a new shell. |
| Installer stops with "npm is required" | No standalone build for this platform (for example an Intel Mac); the installer falls back to npm | Install Node.js 22.12 or later, then rerun the installer, or use npx @eudiplo/cli. |
Docker or Podman was not found | No container runtime on the PATH, or it is not running | Install and start Docker or Podman; set EUDIPLO_CONTAINER_RUNTIME=docker or podman to choose one. |
| "port is already allocated" when starting | Another EUDIPLO stack, often eudiplo demo, uses port 3000 or 4200 | Stop it, for example eudiplo down --instance local for the demo, then run eudiplo up --instance <name>. |
Unknown instance or a command acts on the wrong stack | The CLI default instance is a different one | Run eudiplo instance list and pass --instance <name>, or make it the default with eudiplo instance use <name>. |
An edit to .eudiplo.env has no effect | The running container still has the old environment | Run eudiplo up --instance <name>; Compose recreates changed containers. eudiplo restart does not reload the environment. |
| Backend exits right after start | Invalid environment, for example a malformed CORS_ORIGINS or a missing secret | Read eudiplo logs --instance <name> --service eudiplo --tail 100; the first error names the variable. |
localhost health works, the phone cannot reach the backend | The tunnel stopped, forwards to another port, or shows a confirmation page | Restart the tunnel to port 3000. Open https://<public-host>/health in the phone's browser; it must show the JSON directly. |
Login
| Symptom | Cause | Fix |
|---|---|---|
| Web Client login hangs or reports a connection error | EUDIPLO Instance points to an address the browser cannot reach, such as the prefilled http://eudiplo:3000 | Enter the backend address as your browser reaches it, for example http://localhost:3000. A trailing slash is removed automatically. |
| Login rejected with valid-looking credentials | Wrong client ID or secret | eudiplo demo uses root / root; eudiplo init generates the secret, see AUTH_CLIENT_SECRET in .eudiplo.env. Tenant clients: rotate the secret under Administration → API Clients. |
The token endpoint always answers 401 | The instance uses an external OIDC provider such as Keycloak | Use the SSO tab, or get tokens from the identity provider. See Keycloak SSO. |
| Signed in, but menu sections are missing | The client lacks the roles for them | Add the roles to the client, then sign in again; tokens keep the roles they were issued with. See Tenants and access. |
API calls return 403 | The token lacks the role of the endpoint, for example issuance:offer | Give the client the documented role. Never give a tenant client tenants:manage. |
Browser console shows a CORS error for /api/... | CORS_ORIGINS does not include the page's origin | Add the exact origin (scheme://host[:port], no trailing slash) and run eudiplo up. |
Wallet connection
| Symptom | Cause | Fix |
|---|---|---|
| Wallet cannot open the offer or request | PUBLIC_URL is not reachable from the phone, or it changed after the offer was created | Check the health endpoint on the phone. Correct PUBLIC_URL, run eudiplo up, and create a new offer. |
Offers or metadata contain localhost | PUBLIC_URL is still the default | Set PUBLIC_URL to the public HTTPS address and run eudiplo up. |
| Wallet rejects the issuer or verifier certificate | The wallet does not accept self-signed certificates, or needs registrar certificates | Follow Wallet and registrar requirements. Do not weaken verification to get around it. |
Wallet reports invalid_dpop_proof or a DPoP error | The wallet does not send a valid DPoP proof, or the proof's key differs from the one bound earlier | Use a wallet with DPoP support, or relax the DPoP setting of the issuer. See Authorization servers. |
Wallet reports a PKCE or invalid_request error at authorization | Since 9.0, authorization codes require PKCE with S256 | Use a wallet that sends an S256 code challenge. |
Issuance
| Symptom | Cause | Fix |
|---|---|---|
Session stays active | The wallet never fetched the offer | See Wallet connection. |
Session stays fetched | The credential was delivered, but the wallet sent no notification | Nothing to fix; only a credential_accepted notification moves the session to completed. |
POST /api/issuer/offer returns 400 for the claims | The claims do not match the claim fields of the credential configuration | Send every mandatory claim with the configured type. See Claims. |
Wallet gets invalid_grant or "The session has expired" | The offer outlived its lifetime | Create a new offer; adjust offerLifetimeSeconds if needed. |
| Wallet asks for a key attestation | The credential's supported proof types require one | Add proof type JWT, or use a wallet with key attestation. |
| "No key chain found with usage type ..." | The tenant has no key chain for that purpose | Create one under Cryptographic Assets → Keys. |
| Attribute provider or webhook is never called | The URL is HTTP or private, which the outbound URL policy blocks by default | Use a public HTTPS URL. For local development only, set OUTBOUND_URL_ALLOW_HTTP=true and OUTBOUND_URL_ALLOW_PRIVATE_NETWORK=true. |
| "Failed to fetch authorization server metadata" or "Failed to fetch upstream OIDC configuration" | The external authorization server or the chained server's upstream provider is on HTTP or a private address, which the outbound URL policy blocks by default; the log names the rejected URL | Use a public HTTPS URL. For local development only, set OUTBOUND_URL_ALLOW_HTTP=true and OUTBOUND_URL_ALLOW_PRIVATE_NETWORK=true. |
| The credential in the wallet shows old values | A wallet keeps the claims it was issued with | Issue a new credential after changing configuration or claims. |
Presentation
| Symptom | Cause | Fix |
|---|---|---|
| Wallet finds no matching credential | Format, VCT or claim paths differ from the issued credential, or trusted_authorities excludes its issuer | Compare the DCQL query with the credential type. See DCQL. |
| Wallet rejects the verifier | The access certificate is missing, inactive or not trusted by the wallet | Select an active access key chain in the presentation configuration; see Wallet and registrar requirements. |
| Request rejected for overasking | The registration certificate does not cover every requested credential and claim | Update the registration certificate to match the DCQL query. See Registration certificates. |
| Wallet gets "The session has expired" | The request outlived its lifetime (default 300 seconds) | Generate a new request and approve it sooner, or raise the lifetime. |
Session failed with access_denied | The user declined in the wallet | Expected; offer a new request. |
Session failed with trust_chain_not_trusted or no_trust_chain_to_root | The issuer is not on the trust lists of the credential query | Add the issuer to the trust list, or accept that the credential is rejected. See Accept only trusted issuers. |
Session failed with trust_list_unavailable | A trust list could not be loaded (also when the outbound URL policy blocks its HTTP or private URL), has expired, or lacks verification material | Check the list URL, the backend log and its verifierX509Der or verifierKey. See Trust lists. |
Session failed with "Status is not valid" | The credential is revoked or suspended | Expected after revocation. See Revocable credentials. |
| No webhook arrives | The endpoint is unreachable or blocked by the outbound URL policy; webhooks are not retried | Check the backend log, then read the result with GET /api/session/{id}. See Receive results. |
Session shows active after its expiry time | The expired status is written by a periodic cleanup job | Compare expiresAt with the current time instead of waiting for expired. |