PHPackages                             schaefersoft/laravel-swiss-eid - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. schaefersoft/laravel-swiss-eid

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

schaefersoft/laravel-swiss-eid
==============================

Laravel package for integrating the Swiss eID (swiyu) verification flow

v0.4.0(4w ago)04MITPHPPHP ^8.1CI passing

Since Apr 17Pushed 4w ago1 watchersCompare

[ Source](https://github.com/schaefersoft/laravel-swiss-eid)[ Packagist](https://packagist.org/packages/schaefersoft/laravel-swiss-eid)[ RSS](/packages/schaefersoft-laravel-swiss-eid/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (14)Versions (11)Used By (0)

Laravel Swiss eID
=================

[](#laravel-swiss-eid)

[![Tests](https://github.com/schaefersoft/laravel-swiss-eid/actions/workflows/tests.yml/badge.svg)](https://github.com/schaefersoft/laravel-swiss-eid/actions)[![PHPStan](https://camo.githubusercontent.com/ff3c7f8c8667ce643f47e74532748f673482a5f95d7d4269f925f2eebbe5117e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230382d627269676874677265656e)](https://phpstan.org)[![Total downloads](https://camo.githubusercontent.com/bc6ceb9a7f320347fb47bc1505671c25300583cf75a9dac12e343938ce38e88d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7363686165666572736f66742f6c61726176656c2d73776973732d656964)](https://packagist.org/packages/schaefersoft/laravel-swiss-eid)[![Latest Version on Packagist](https://camo.githubusercontent.com/76d02b8491df8be20a78496fa516254315218f71b7c3b8a62d4fa003f6681850/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7363686165666572736f66742f6c61726176656c2d73776973732d6569642e737667)](https://packagist.org/packages/schaefersoft/laravel-swiss-eid)[![License](https://camo.githubusercontent.com/28f5ce1b35c511cf0128ca07ce6d0e769497f6e084786c6c09371708c8cf6b19/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7363686165666572736f66742f6c61726176656c2d73776973732d6569642e737667)](LICENSE)

Laravel package for integrating the Swiss eID (**swiyu**) verification flow: builds the OpenID4VP / DIF Presentation Exchange request, talks to a swiyu Verifier, persists the verification, handles the webhook callback and emits events. UI is intentionally left to you — the package only exposes data primitives (QR code SVG, deeplink, JSON status endpoint) so you can render in Blade, Livewire, Vue, React, or anything else.

---

5-Minute Quickstart
-------------------

[](#5-minute-quickstart)

> This package is a **client for the swiyu Verifier** — the Spring Boot service that speaks OpenID4VP to the wallet app. You need that verifier running before anything else works.

### Step 1 — Start the verifier locally

[](#step-1--start-the-verifier-locally)

```
git clone https://github.com/swiyu-admin-ch/swiyu-verifier
cd swiyu-verifier
docker compose up -d
# Listening on http://localhost:8083
```

The wallet app on the user's phone must be able to reach the verifier, so expose it through a public tunnel during local development:

```
ngrok http 8083
# → https://abc123.ngrok-free.app  (use this URL in .env below)
```

### Step 2 — Install the package

[](#step-2--install-the-package)

```
composer require schaefersoft/laravel-swiss-eid
php artisan swiss-eid:install
```

The installer publishes the config file and migration, prints all required `.env`variables, and optionally runs `php artisan migrate`.

### Step 3 — Fill in .env

[](#step-3--fill-in-env)

```
SWISS_EID_VERIFIER_URL=https://abc123.ngrok-free.app
SWISS_EID_WEBHOOK_API_KEY=a-secret-key-at-least-32-characters-long

# From your swiyu verifier configuration:
SWISS_EID_CREDENTIAL_TYPE=https://eid.admin.ch/credentials/swiss-eid-beta/1.0
SWISS_EID_ACCEPTED_ISSUERS=did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527
```

### Step 4 — Start a verification and show the QR code

[](#step-4--start-a-verification-and-show-the-qr-code)

```
use SwissEid\LaravelSwissEid\Facades\SwissEid;

$pending = SwissEid::verify()
    ->ageOver18()
    ->forUser(auth()->id())
    ->create();

// $pending->qrCode()    → SVG string, embed with {!! !!}
// $pending->deeplink    → universal link to open the wallet app directly
// $pending->statusUrl() → JSON polling endpoint for your frontend
// $pending->id          → UUID to look up the result later
```

```
{{-- resources/views/verify.blade.php --}}
{!! $pending->qrCode(300) !!}
Open in Swiss Wallet App
```

### Step 5 — React to the result

[](#step-5--react-to-the-result)

Once the wallet has scanned the QR code the verifier fires the webhook. Listen to the event:

```
use SwissEid\LaravelSwissEid\Events\VerificationCompleted;

Event::listen(VerificationCompleted::class, function ($event) {
    $result = $event->verification->toResult();

    if ($result->isSuccessful() && $result->isAdult()) {
        $user->update(['verified_at' => now()]);
    }
});
```

Or poll the status endpoint from the frontend:

```
const poll = async (statusUrl) => {
    const { state, label, is_terminal } = await fetch(statusUrl).then(r => r.json());
    document.querySelector('#status').textContent = label; // "Pending" / "Successful" …
    if (!is_terminal) setTimeout(() => poll(statusUrl), 2500);
};
poll('{{ $pending->statusUrl() }}');
```

### Verify your setup

[](#verify-your-setup)

```
php artisan swiss-eid:doctor
```

Validates all ENV variables, parses the private key, checks DID formats, and probes webhook reachability — in one pass.

---

Table of Contents
-----------------

[](#table-of-contents)

1. [Prerequisites](#prerequisites)
    - [PHP &amp; Laravel versions](#1-php--laravel-versions)
    - [A running swiyu Verifier](#2-a-running-swiyu-verifier)
    - [Public reachability &amp; webhook routing](#3-public-reachability--webhook-routing)
    - [Verifier signing key (PEM)](#4-verifier-signing-key-pem)
    - [Accepted issuer DIDs](#5-accepted-issuer-dids)
    - [Swiss wallet app (for testing)](#6-swiss-wallet-app-for-testing)
2. [Installation](#installation)
3. [Configuration](#configuration)
4. [Usage](#usage)
    - [Localising status labels](#localising-status-labels)
5. [Database](#database)
6. [Artisan Commands](#artisan-commands)
    - [swiss-eid:doctor](#swiss-eiddoctor)
7. [Testing](#testing)
8. [Troubleshooting](#troubleshooting)
9. [Contributing](#contributing)
10. [License](#license)

---

Prerequisites
-------------

[](#prerequisites)

Before installing the package you need a few pieces of Swiss eID infrastructure in place. Each sub-section below covers exactly one requirement.

### 1. PHP &amp; Laravel versions

[](#1-php--laravel-versions)

DependencyVersionPHP`8.1+` (runtime); CI tests `8.2`–`8.5`Laravel`10`, `11`, `12`, `13`Composer2.xThe package itself runs on PHP 8.1+ (uses native enums, readonly DTOs and `HasUuids`). CI only tests against PHP 8.2+ because the Pest/PHPUnit dev toolchain no longer resolves on PHP 8.1. Consumers on PHP 8.1 can still install and use the package without issue.

### 2. A running swiyu Verifier

[](#2-a-running-swiyu-verifier)

The Swiss eID ecosystem separates the **relying party** (your Laravel app) from the **verifier service**, a small Spring Boot process that speaks OpenID4VP to the wallet. This package is a **client of that verifier** — it does not implement the OpenID4VP protocol itself.

You need to run the official verifier locally (or host it somewhere) before this package can do anything:

- Repository:
- Default port: `8083`
- Expected API base path: `/management/api`

A minimal `docker-compose.yml` typically looks like this (see the upstream repo for the full example):

```
services:
  swiyu-verifier:
    image: swiyu-verifier:local
    ports:
      - "8083:8080"
    environment:
      SWIYU_VERIFIER_DID: "did:tdw:...your-verifier-did..."
      SWIYU_SIGNING_KEY: |
        -----BEGIN EC PRIVATE KEY-----
        ...
        -----END EC PRIVATE KEY-----
      LARAVEL_WEBHOOK_URL: "http://your-laravel-host/swiss-eid/webhook"
      LARAVEL_WEBHOOK_API_KEY: "your-secret-key-here"
```

Confirm the verifier is reachable:

```
curl http://localhost:8083/management/api/verifications/anything
# 404 is fine — the connection works and the API responds.
```

### 3. Public reachability &amp; webhook routing

[](#3-public-reachability--webhook-routing)

The verification flow requires **two** network paths that both have to work:

1. **Wallet → Verifier**: the swiyu wallet on the user's phone fetches the presentation request from the verifier's public URL. During local development, expose the verifier via a tunnel (e.g. ngrok, Cloudflare Tunnel):

    ```
    ngrok http 8083
    # → https://something.ngrok-free.dev
    ```

    Put that tunnel URL into your Laravel `.env` as `SWISS_EID_VERIFIER_URL`. The verifier itself also needs to know its public URL in its own configuration so it can embed it in the QR-code deeplink.
2. **Verifier → Laravel webhook**: when the wallet has responded, the verifier POSTs to your Laravel app. If both run in Docker, the verifier must be able to resolve your Laravel host. Two common options:

    - Run the verifier on the **same Docker network** as your Laravel app (e.g. the DDEV project network) and use the internal hostname in `LARAVEL_WEBHOOK_URL`, such as `http://ddev-myproject-web/swiss-eid/webhook`.
    - Or expose Laravel publicly too (another tunnel) and use that URL.

    The webhook is authenticated via a shared secret — the verifier sends `X-Verifier-Api-Key: ` and the package's middleware rejects everything else with `401`.

### 4. Verifier signing key (PEM)

[](#4-verifier-signing-key-pem)

The verifier signs the presentation request it hands to the wallet. It expects an **EC P-256 private key in PEM format**. Generate one with OpenSSL:

```
openssl ecparam -name prime256v1 -genkey -noout -out verifier-key.pem
# Public key (publish as part of your DID document):
openssl ec -in verifier-key.pem -pubout -out verifier-pub.pem
```

When passing the private key through a `.env` file, keep the real newlines intact. Use a double-quoted multi-line value — **not** `\n` escape sequences, which the verifier will refuse to parse:

```
SWIYU_SIGNING_KEY="-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIE+...
-----END EC PRIVATE KEY-----"
```

### 5. Accepted issuer DIDs

[](#5-accepted-issuer-dids)

The Swiss eID beta trust infrastructure uses `did:tdw:` identifiers. For a verification to succeed, the credential presented by the wallet must be issued by a DID that your verifier **trusts**. In practice you will list at least:

- Your **own** verifier DID (useful for self-issued test credentials).
- The **official Beta-ID issuer DID** if you want to accept real beta credentials.

Example for `.env` (comma-separated):

```
SWISS_EID_ACCEPTED_ISSUERS=did:tdw:Qm...your-verifier:...,did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527
```

If you do not know the issuer DID of a credential, you can extract it from a decoded SD-JWT (the `iss` field) or from the verifier logs when it rejects a presentation with `issuer_not_accepted`.

### 6. Swiss wallet app (for testing)

[](#6-swiss-wallet-app-for-testing)

To actually scan a QR code and complete a verification end-to-end you need the **swiyu wallet** app installed on a phone:

- iOS: App Store — search "swiyu"
- Android: Google Play — search "swiyu"

For beta/integration testing against `trust-infra.swiyu-int.admin.ch`, the app must be in the corresponding environment. Check the official documentation at  for environment-specific instructions.

---

Installation
------------

[](#installation)

```
composer require schaefersoft/laravel-swiss-eid
```

Run the installer:

```
php artisan swiss-eid:install
```

This publishes the config file, the migration, prints the required `.env`variables, and (optionally) runs `php artisan migrate`.

Manual alternative:

```
php artisan vendor:publish --tag=swiss-eid-config
php artisan vendor:publish --tag=swiss-eid-migrations
php artisan migrate
```

---

Configuration
-------------

[](#configuration)

All settings live in `config/swiss-eid.php` and can be overridden with environment variables:

VariableDefaultDescription`SWISS_EID_VERIFIER_URL``http://localhost:8083`Base URL of the swiyu verifier (must be reachable from Laravel).`SWISS_EID_TIMEOUT``10`HTTP timeout (seconds) for calls to the verifier.`SWISS_EID_WEBHOOK_PATH``/swiss-eid/webhook`Route the verifier POSTs to when a wallet has responded.`SWISS_EID_WEBHOOK_KEY_HEADER``X-Verifier-Api-Key`HTTP header carrying the shared webhook secret.`SWISS_EID_WEBHOOK_API_KEY`–Shared secret; **required** — the middleware returns 401 without it.`SWISS_EID_RESPONSE_MODE``direct_post`Response mode for wallet responses. Use `direct_post.jwt` for encrypted wallet-to-verifier transport (requires verifier v2.3.1+).`SWISS_EID_CREDENTIAL_TYPE`–Credential type (`vct`) to request from the wallet. **Required** — set to the vct matching your swiyu environment.`SWISS_EID_ACCEPTED_ISSUERS`–Comma-separated list of accepted issuer DIDs. At least one is required.`SWISS_EID_VERIFICATION_TTL``300`Seconds a pending verification stays valid before being marked `expired`.`SWISS_EID_POLLING_ENABLED``true`Enable the built-in `/swiss-eid/status/{id}` JSON endpoint.`SWISS_EID_POLLING_PATH``/swiss-eid/status`Route prefix of the polling endpoint.`SWISS_EID_AUTH_ENABLED``false`Enable OAuth2 client-credentials auth against the verifier's management API.`SWISS_EID_TOKEN_URL`–OAuth2 token endpoint (only if auth is enabled).`SWISS_EID_CLIENT_ID`–OAuth2 client ID (only if auth is enabled).`SWISS_EID_CLIENT_SECRET`–OAuth2 client secret (only if auth is enabled).`SWISS_EID_TABLE_NAME``eid_verifications`Override the DB table name if it clashes with your schema.`SWISS_EID_USER_ID_TYPE``int`Column type for `user_id`: `int` (unsignedBigInteger), `uuid`, or `string`. Set **before** running the migration.---

Usage
-----

[](#usage)

### Starting a verification

[](#starting-a-verification)

Use the `SwissEid` facade's fluent builder. Each call returns the same manager instance, so you can chain freely. `create()` persists the record and returns a `PendingVerification` DTO.

```
use SwissEid\LaravelSwissEid\Facades\SwissEid;

$pending = SwissEid::verify()
    ->ageOver18()
    ->forUser($user->id)
    ->metadata(['order_id' => $order->id])
    ->create();
```

The returned `$pending` has:

Property / methodDescription`$pending->id`Internal UUID of the DB record. Use this to look it up later.`$pending->verifierId`ID assigned by the swiyu verifier.`$pending->deeplink`Universal link — open on the user's phone to launch the wallet.`$pending->verificationUrl`Full URL of the presentation request (for debugging).`$pending->qrCode(int $size = 300)`SVG string. Embed with `{!! !!}`.`$pending->qrCodeDataUri(int $size = 300)``data:image/svg+xml;base64,...` for ``.`$pending->statusUrl()`URL of the JSON polling endpoint for this verification.`$pending->expiresAt`Carbon instance — TTL cutoff.`$pending->isExpired()`Quick boolean check.### Requesting specific fields

[](#requesting-specific-fields)

Use the `CredentialField` enum (preferred) or plain field-name strings:

```
use SwissEid\LaravelSwissEid\Enums\CredentialField;

$pending = SwissEid::verify()
    ->fields([
        CredentialField::GivenName,
        CredentialField::FamilyName,
        CredentialField::DateOfBirth,
        CredentialField::Nationality,
    ])
    ->create();
```

Available cases: `AgeOver18`, `AgeOver16`, `GivenName`, `FamilyName`, `DateOfBirth` (resolves to the JSON key `birth_date`), `Nationality`, `PlaceOfBirth`, `Gender`.

You can also pass a single field by name or full JSON path:

```
SwissEid::verify()->field('given_name');     // resolves to $.given_name
SwissEid::verify()->field('$.custom_path');  // passed through verbatim
```

### Overriding credential type / accepted issuers per request

[](#overriding-credential-type--accepted-issuers-per-request)

```
SwissEid::verify()
    ->credentialType('your-credential-type')
    ->acceptedIssuers([
        'did:tdw:QmPEZ...your-trusted-issuer',
    ])
    ->ageOver18()
    ->create();
```

### Presenting to the user

[](#presenting-to-the-user)

The package is UI-agnostic. Render the primitives in any frontend:

```
{{-- Plain Blade, no JS framework required --}}

    {!! $pending->qrCode(300) !!}
    Open in Swiss Wallet App
    Polling: {{ $pending->statusUrl() }}

```

```
// React / Vue / vanilla JS: poll the JSON endpoint
const response = await fetch(statusUrl);
const { state, label, is_terminal } = await response.json();
if (is_terminal && state === 'success') { /* redirect */ }
```

The polling endpoint returns JSON:

```
{
    "state": "pending | success | failed | expired",
    "label": "Ausstehend | Erfolgreich | Fehlgeschlagen | Abgelaufen",
    "is_terminal": false
}
```

`label` is resolved via Laravel's translation system and automatically respects `App::getLocale()`. See [Localising status labels](#localising-status-labels)below for details.

Poll every 2–3 seconds until `is_terminal === true`. If the TTL has passed, the endpoint will also flip `pending` → `expired` on the first call after expiry and dispatch the `VerificationExpired` event.

### Localising status labels

[](#localising-status-labels)

The polling endpoint's `label` field and `VerificationState::label()` are driven by Laravel's translation system under the `swiss-eid::states` namespace. The package ships translations for **German (de), French (fr), Italian (it), and English (en)**.

The active locale (`App::getLocale()`) is used automatically — no configuration needed. If the locale falls through to a string that does not exist in the package's files (e.g. `es`), Laravel's fallback locale applies next, and then the translation key itself is returned as-is.

**Customising or adding languages**

Publish the translation files once:

```
php artisan vendor:publish --tag=swiss-eid-lang
```

This copies the four files to `lang/vendor/swiss-eid/{de,en,fr,it}/states.php`. Published files take priority over the package originals. Edit them freely:

```
// lang/vendor/swiss-eid/de/states.php
return [
    'pending' => 'Wartet auf Bestätigung',   // custom wording
    'success' => 'Erfolgreich',
    'failed'  => 'Fehlgeschlagen',
    'expired' => 'Abgelaufen',
];
```

To support an additional language, add a new locale directory alongside the existing ones — e.g. `lang/vendor/swiss-eid/es/states.php` with Spanish labels.

**Using `label()` directly**

`VerificationState::label()` resolves the same translation keys, so it works anywhere — not just in the polling JSON:

```
use SwissEid\LaravelSwissEid\Enums\VerificationState;

$state = VerificationState::Pending;
echo $state->label();  // "Ausstehend" (de), "Pending" (en), …
```

---

### Handling the webhook

[](#handling-the-webhook)

The webhook route (`POST /swiss-eid/webhook` by default) is registered automatically by the package's service provider. It:

1. Validates the `X-Verifier-Api-Key` header via middleware.
2. Reads the `verification_id` from the payload.
3. Calls the verifier's GET endpoint to fetch the full result.
4. Writes `state`, `credential_data` (encrypted at rest) and `webhook_received_at` to the DB.
5. Dispatches `VerificationCompleted` or `VerificationFailed`.

You do **not** register this route yourself. Just make sure:

- `SWISS_EID_WEBHOOK_API_KEY` matches what the verifier sends.
- Your firewall / reverse proxy allows the verifier to reach that path.
- CSRF is not a concern — the route lives in the `api` middleware group.

### Retrieving results

[](#retrieving-results)

```
$result = SwissEid::getVerification($pending->id); // or the verifier ID

if ($result->isSuccessful()) {
    $firstName = $result->get('given_name');
    $isAdult   = $result->isAdult();          // age_over_18 convenience
    $raw       = $result->credentialData;     // decrypted array, or null
}
```

`VerificationResult` methods:

MethodReturns`isSuccessful()` / `isFailed()` / `isPending()`bool`get(string $field, mixed $default = null)`Field from credential data.`has(string $field)`bool`isAdult()`Shortcut for `age_over_18 === true`.`toArray()`Plain array (for JSON responses).If the ID is not found a `VerificationNotFoundException` is thrown.

### Events

[](#events)

Listen in your `EventServiceProvider` or with `#[AsEventListener]`:

```
use SwissEid\LaravelSwissEid\Events\VerificationCompleted;
use SwissEid\LaravelSwissEid\Events\VerificationFailed;
use SwissEid\LaravelSwissEid\Events\VerificationExpired;

Event::listen(VerificationCompleted::class, function ($event) {
    $user = User::find($event->verification->user_id);
    $user->update(['verified_at' => now()]);
});
```

Each event carries a single `$verification` property of type `EidVerification` (the Eloquent model). Use its `toResult()` method if you want the DTO view.

---

Database
--------

[](#database)

The package ships a single table (default name `eid_verifications`) with:

ColumnTypeNotes`id`UUID (PK)Your internal ID — the one you hand to the frontend.`verifier_id`stringThe ID returned by the swiyu verifier.`user_id`nullableYour user reference. Column type depends on `SWISS_EID_USER_ID_TYPE` (`int` default, `uuid`, or `string`).`state`enum`pending`, `success`, `failed`, `expired`.`credential_type`stringMirrors `SWISS_EID_CREDENTIAL_TYPE`.`requested_fields`jsonThe presentation-definition fields you requested.`credential_data`encrypted jsonDecrypted automatically by the cast.`metadata`json, nullableAnything you passed via `->metadata([...])`.`deeplink`, `verification_url`stringCached from the verifier response.`webhook_received_at`datetime, nullableSet when the webhook fires.`expires_at`datetimeTTL cutoff.`created_at`, `updated_at`datetimeStandard.Useful Eloquent scopes on `EidVerification`:

```
EidVerification::pending()->get();
EidVerification::expired()->get();
EidVerification::forUser($user->id)->latest()->first();
```

Override the table name with `SWISS_EID_TABLE_NAME` in `.env`. The migration and the model both read from `config('swiss-eid.table_name')`, so renaming is a single-line change.

---

Artisan Commands
----------------

[](#artisan-commands)

CommandDescription`swiss-eid:install`Publish config + migration, print required `.env` variables, optionally run migrations.`swiss-eid:test-connection`Probe the verifier to confirm it is reachable and responding.`swiss-eid:cleanup --days=7`Delete expired records older than N days. Accepts `--dry-run`.`swiss-eid:doctor`Validate the full configuration, parse the private key, check DID formats, and probe the webhook URL.Schedule the cleanup in `App\Console\Kernel` (or `routes/console.php` on Laravel 11+):

```
$schedule->command('swiss-eid:cleanup --days=30')->daily();
```

### swiss-eid:doctor

[](#swiss-eiddoctor)

```
php artisan swiss-eid:doctor
```

A self-contained diagnostic that works through every aspect of the configuration in one pass and exits with a non-zero code if any check fails — useful in CI pipelines or as a post-deploy smoke test.

**Checks performed**

SectionWhat is validated**Verifier**`SWISS_EID_VERIFIER_URL` is a valid URL · timeout is a positive integer · `SWISS_EID_RESPONSE_MODE` is `direct_post` or `direct_post.jwt`**Webhook**Path starts with `/` · header name is set · `SWISS_EID_WEBHOOK_API_KEY` is set and ≥ 32 characters (warns if shorter)**Credentials**`SWISS_EID_CREDENTIAL_TYPE` (vct) is set · at least one accepted issuer is configured**OAuth2 Auth**Skipped when `SWISS_EID_AUTH_ENABLED=false`; otherwise validates token URL, client ID and secret**General**`SWISS_EID_VERIFICATION_TTL` is a positive integer (warns if &lt; 60 s) · `SWISS_EID_USER_ID_TYPE` is one of `int`, `uuid`, `string`**Private Key**Reads `SWISS_EID_PRIVATE_KEY`, parses it with OpenSSL, confirms the key type is EC and the curve is P-256 (`prime256v1`); reports key type and bit count. Required when `response_mode=direct_post.jwt`.**DID Formats**Each entry in `SWISS_EID_ACCEPTED_ISSUERS` matches `did:[method]:[id]`**Webhook Reachability**POSTs to `APP_URL + webhook.path` without credentials: `401`/`403` → correctly protected; `404` → route not found; connection error → not publicly reachable**Private key check**

The doctor command reads the private key directly from the environment (not from the config file) so that the raw value can be passed to OpenSSL. Store it in `.env` using double-quoted multi-line syntax to preserve real newlines:

```
SWISS_EID_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBFv...
-----END EC PRIVATE KEY-----"
```

Single-line values with escaped `\n` sequences also work — the command normalises them before parsing.

**Exit codes**

CodeMeaning`0`All checks passed (warnings are informational only).`1`One or more errors found.**Example output**

```
Swiss eID Doctor — configuration diagnostics

  Verifier
    ✓ Verifier URL: https://verifier.example.com
    ✓ Timeout: 10s
    ✓ Response mode: direct_post.jwt

  Webhook
    ✓ Webhook path: /swiss-eid/webhook
    ✓ API key header: X-Verifier-Api-Key
    ! SWISS_EID_WEBHOOK_API_KEY is shorter than 32 characters — consider a stronger secret

  ...

  Private Key (JWT response mode)
    ✓ EC private key valid — curve: P-256 (prime256v1), bits: 256

  DID Formats (accepted_issuers)
    ✓ Valid DID (method: did:tdw:…) — did:tdw:QmPEZ…

  Webhook Reachability
      Probing: https://your-app.example.com/swiss-eid/webhook
    ✓ Webhook reachable — HTTP 401 (auth middleware is rejecting unauthenticated requests correctly)

  1 warning(s) — review before going to production.

```

---

Testing
-------

[](#testing)

Use the built-in `SwissEidFake` to avoid real HTTP calls in your tests:

```
use SwissEid\LaravelSwissEid\Facades\SwissEid;
use SwissEid\LaravelSwissEid\SwissEidFake;

it('starts a verification', function () {
    $fake = SwissEid::fake();

    // ... code that calls SwissEid::verify()->ageOver18()->create()

    $fake->assertVerificationStarted();
});

it('reacts to a completed verification', function () {
    $result = SwissEidFake::fakeVerification(state: 'success', data: [
        'given_name'  => 'Anna',
        'age_over_18' => true,
    ]);

    $fake = SwissEid::fake([$result->id => $result]);

    SwissEid::getVerification($result->id);

    $fake->assertVerificationCompleted(fn ($r) => $r->get('given_name') === 'Anna');
});
```

Run the package's own test suite:

```
composer test
composer test:coverage   # Pest + min. 80% coverage
composer analyse         # PHPStan level 8
```

---

Troubleshooting
---------------

[](#troubleshooting)

SymptomLikely causeFix`createVerificationManagementDto: Either acceptedIssuerDids or trustAnchors must be set``SWISS_EID_ACCEPTED_ISSUERS` is empty.Set at least one DID.Empty `deeplink` / no QR codeThe verifier's response used different key casing (e.g. `verification_deeplink`).The package already falls back through several aliases; check `storage/logs/laravel.log` for the raw response if a new one appears.Verifier returns 500 on `/oid4vp/api/request-object/...`PEM signing key is malformed (literal `\n` instead of real newlines).Use double-quoted multi-line `.env` value — see the [signing key section](#4-verifier-signing-key-pem).Webhook returns 500, logs say `getaddrinfo for db failed`Verifier container cannot resolve your Laravel/DB host.Join the verifier to the same Docker network as your Laravel app; use the internal hostname in `LARAVEL_WEBHOOK_URL`.Webhook never fires (404)Stale verification IDs in retry queue from a previous failed run.Create a fresh verification — the verifier drops stale webhook retries after a while.State is always `failed` with `issuer_not_accepted`The credential's issuer DID is not in `SWISS_EID_ACCEPTED_ISSUERS`.Add the real issuer DID (extract from wallet logs / decoded SD-JWT).Wallet shows "Kein passender Nachweis verfügbar"Requested field name does not match the credential schema (e.g. `date_of_birth` vs `birth_date`).Use the `CredentialField` enum — the package maps the correct keys.Quick sanity checks:

```
php artisan swiss-eid:doctor          # full config + reachability diagnostics
php artisan swiss-eid:test-connection # targeted verifier connectivity probe
```

---

Contributing
------------

[](#contributing)

1. Fork the repository.
2. Create a feature branch: `git checkout -b feature/my-feature`.
3. Run `composer test` and `composer analyse` — both must pass.
4. Open a Pull Request. Conventional-commit messages are appreciated; they drive the automated release workflow.

---

License
-------

[](#license)

MIT. See [LICENSE](LICENSE) for details.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance94

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 87.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

7

Last Release

29d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d992666fb167b076bc8a865f8a542d2133deb5fc07e48be3443db65f812fa5b2?d=identicon)[schaefersoft](/maintainers/schaefersoft)

---

Top Contributors

[![TheGodlyLuzer](https://avatars.githubusercontent.com/u/71211303?v=4)](https://github.com/TheGodlyLuzer "TheGodlyLuzer (50 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")[![DGINXREAL](https://avatars.githubusercontent.com/u/47042042?v=4)](https://github.com/DGINXREAL "DGINXREAL (1 commits)")

---

Tags

laravelswiss-eidswiyularavelverifiersd-jwtswiss-eidswiyudigital-id

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/schaefersoft-laravel-swiss-eid/health.svg)

```
[![Health](https://phpackages.com/badges/schaefersoft-laravel-swiss-eid/health.svg)](https://phpackages.com/packages/schaefersoft-laravel-swiss-eid)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9732.3M121](/packages/roots-acorn)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45344.0k1](/packages/pressbooks-pressbooks)[simplestats-io/laravel-client

Analytics for Laravel. Track visitors, registrations, and payments. Discover which channels actually drive revenue, not just traffic. Server-side, GDPR compliant, ad-blocker proof.

5019.3k](/packages/simplestats-io-laravel-client)[hasinhayder/tyro

Tyro - The ultimate Authentication, Authorization, and Role &amp; Privilege Management solution for Laravel 12 &amp; 13

6753.6k5](/packages/hasinhayder-tyro)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
