> For the complete documentation index, see [llms.txt](https://docs.empe.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.empe.io/develop/wallet-server/error-handling-and-troubleshooting.md).

# Error Handling and Troubleshooting

The service returns errors through a single global exception filter, so every error response has the same shape regardless of which endpoint produced it. This page describes that shape, then catalogs the errors you are most likely to encounter, organized by HTTP status code, and finishes with startup and connectivity issues.

## Error response shape

All errors are returned as JSON with a consistent envelope:

```json
{
  "statusCode": 403,
  "message": "Insufficient role",
  "path": "/agent/dids",
  "timestamp": "2026-06-22T10:15:30.123Z"
}
```

* **statusCode** — the HTTP status code, also reflected in the response status line.
* **message** — a human-readable description; for validation errors this may be an array of messages.
* **path** — the request path that produced the error.
* **timestamp** — ISO-8601 time the error was generated.

Unhandled server errors are returned as `500` with a generic `Internal server error` message in production; details are not leaked to the client.

## 400 Bad Request

Returned when the request body, query, or URI is malformed or semantically invalid.

* **Validation failure** — a required field is missing or has the wrong type. For example, creating a schema without `name`/`version`, or a credential offer with an empty `credentialConfigurationIds` array. The `message` lists the specific fields that failed.
* **Unknown URI protocol** (`POST /holders/:holderId/process`) — the `uri` you submitted is neither a credential offer nor an authorization request. The message is `Unknown URI protocol. Expected openid-credential-offer:// or openid4vp://`. Pass the full `openid-credential-offer://...` URI returned by a credential offer, or the `openid4vp://...` (or `https://...`) authorization request URI from a verifier.
* **Holder has no DIDs** (`POST /holders/:holderId/process`) — the wallet tenant has no DID with which to bind a claimed credential. The message is `Holder has no DIDs created. Cannot accept credential offer.` Create the holder with a DID method (`POST /agent/holder` with `{ method: "key" }` or `{ method: "web" }`) before claiming offers.
* **Unknown property in the body** — the global validation pipe whitelists request properties, so a JSON body containing a property that is not part of the endpoint's schema is rejected with a message like `property X should not exist`.

## 401 Unauthorized

Returned by authentication, before any role check, when the Bearer token cannot be validated. Tokens must be `RS256`-signed and presented as `Authorization: Bearer <JWT>`.

* **Missing token** — no `Authorization: Bearer <JWT>` header is sent; the request is rejected with a generic `401 Unauthorized`.
* **Malformed token** — a token is present but is not a well-formed JWT (it cannot be decoded). Message: `Invalid access token`.
* **Missing `kid`** — the token header has no `kid`, so the signing key cannot be selected. Message: `Access token header is missing kid`. Ensure your OIDC provider includes a `kid` in the JWT header (Keycloak does this by default).
* **Signing key not found in JWKS** — the token's `kid` does not match any key published at `OIDC_JWKS_URL`. This happens after a provider key rotation (wait for the 10-minute key cache to refresh) or when the token was issued by a different provider/realm than the one configured.
* **Expired token** — expiration is always enforced. Request a fresh token from your OIDC provider.
* **`OIDC_JWKS_URL` misconfigured** — the JWKS endpoint is unreachable or returns no usable keys, so no token can be verified. Confirm the URL points at your provider's JWKS document (for example `https://keycloak.example.com/realms/<realm>/protocol/openid-connect/certs`).

For local development you can bypass authentication entirely with `AUTH_DISABLED=true`, which treats every request as an authenticated `admin` principal. Never enable this outside local development.

## 403 Forbidden

Returned when the token is valid but the caller is not allowed to perform the operation.

* **Insufficient role** — the route requires the `admin` role and the token's `realm_access.roles` (or client roles in `resource_access`) does not include it. The `admin` role is enforced on the `/agent/*` (DID, issuer, verifier, holder management) and `/holders/:holderId/*` (wallet) routes. Message: `Insufficient role`.
* **Holder not owned by the caller** (`/holders/:holderId/*`) — the ownership guard denies access to any `holderId` the caller did not create. Message: `Access to holder denied`. A `holderId` that does not exist at all is indistinguishable from one owned by another user — both return this `403`. A user can only read, decode, or process URIs through holders they created; list your own with `GET /agent/holders`.

## 404 Not Found

Returned when the addressed resource does not exist.

* **Unknown schema id** — message: `Schema not found.` Confirm the id returned when the schema was created.
* **Unknown hosted `did:web` document** (`GET /:uuid/did.json` or `GET /:uuid/.well-known/did.json`) — message: `DID Document not found`.

Two related lookups behave differently: `GET /agent/did/:did` returns `200` with a `null` body for a DID that was not created by this service, and unknown issuer, verifier, or session ids currently surface as `500` errors rather than `404`.

## 429 Too Many Requests

A global limit of **60 requests per 60 seconds** applies to the API. Exceeding it returns `429`. Throttle or batch your calls and retry after a short delay. Serving of hosted `did:web` documents is exempt, so external DID resolution is never rate-limited.

## 500 Internal Server Error

Errors raised outside the HTTP layer — including protocol failures inside a wallet flow — are converted to `500` by the global exception filter. When `NODE_ENV` is `production`, the `message` is replaced with a generic `Internal server error`; otherwise the underlying error message is returned. In production, check the server logs for the real cause.

* **No matching credential for a presentation** (`POST /holders/:holderId/process`) — the holder wallet has no stored credential that satisfies the verifier's DCQL or Presentation Exchange query. Outside production the message is the underlying error, for example `Could not find the required credentials for the presentation submission`. Claim the required credential (for example `EmployeeBadge@1.0:sd-jwt`) into the wallet first, then retry the authorization request.
* **Upstream issuer/verifier rejection** — while processing an offer or authorization request, the remote issuer or verifier returned an error (expired or already-consumed offer, wrong PIN, rejected presentation). The upstream reason appears in `message` outside production; in production it is only in the logs.
* **Unknown issuer, verifier, or session id** — looking up an id the service does not know currently surfaces as `500` rather than `404`.

## Startup & configuration errors

The service validates its configuration at boot and refuses to start when required variables are missing.

* **Missing required env vars** — `ISSUER_BASE_URL`, `VERIFIER_BASE_URL`, and `DATABASE_URL` are mandatory. If any is absent the process exits with `Invalid environment variables:` followed by the offending variable, for example `- ISSUER_BASE_URL must be defined`.
* **Missing JWKS configuration** — unless `AUTH_DISABLED=true`, `OIDC_JWKS_URL` must be set, otherwise startup fails with `OIDC configuration missing. Set OIDC_JWKS_URL.`
* **Database connectivity** — connection failures come from `DATABASE_URL`. Verify the host, port, credentials, and database name, and that the database is reachable from the service and migrations have run.
* **Insecure HTTP blocked** — OpenID4VCI and OpenID4VP flows require HTTPS, so insecure HTTP base URLs are rejected by default. For local development only, set `ALLOW_INSECURE_HTTP=true`. Never set it in production.

## Troubleshooting tips

* **Is the service up?** Call `GET /` for a hello string or `GET /version` for `{ "name": "ssi-core", "version": "..." }`. Neither requires authentication.
* **Authentication problems?** Obtain a valid `admin` token from your OIDC provider and send it as `Authorization: Bearer <token>`. Locally, set `AUTH_DISABLED=true` to skip token handling while you debug other behavior.
* **Inspect the contract.** The interactive API explorer at `GET /api-docs` and the machine-readable spec at `GET /api-docs-json` show every endpoint, request body, and response schema.
* **Read the envelope.** The `path` and `message` fields identify exactly which request failed and why; for validation errors the `message` array names each invalid field.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.empe.io/develop/wallet-server/error-handling-and-troubleshooting.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
