Repository: zitadel/zitadel
Pull request #9445: feat(webkey): migrate to v2beta API
Author: login: livio-a
Created: 2025-03-03T12:59:18Z | Merged: 2025-03-11T10:23:23Z

# Which Problems Are Solved

The current API esp. the versioning led to a lot of confusion since there is/was a V1 marked as `General available` with multiple services, V2 marked the same and additionally a V3 `in preview`.
Since user v3 and the corresponding user schema services have been postponed, the remaining `WebKey` service and `Action` service are the only remaining services in the v3 API.
As these features matured, we want to bring them into a more stable beta phase before moving them into GA later on.
This PR therefore migrates the service to V2 Beta to simplify the versioning.

All V3 Alpha endpoints are removed. Please check out the changes listed below if you already implemented the current API.

# How the Problems Are Solved

- Migrated from v3alpha to v2beta
  - `zitadel.resources.webkey.v3alpha.ZITADELWebKeys` -> `zitadel.webkey.v2beta.WebKeyService`
  - `/resources/v3alpha/webkeys` -> `/v2beta/webkeys`
  - added v2 endpoints to the API reference documentation and removed v3 endpoints
  - updated webkey guide in the docs accordingly
- Additional changes to the endpoints
  - the instance cannot be provided by the request anymore
  - Creation
    - moved configuration into request (form separate message)
    - response provides id and `creation_date` (instead of details message)
  - Activation
    - `/_activate` -> `/activate`
    - response provides `change_date` only (instead of details message)
  - Deletion
    - successful deletion of an (existing) webkey will return a `deletion_date` (instead of details message)
    - deleting a not existing key will no longer return an error (but rather an empty `deletion_date`)
  - List
    - Type changed from `GetWebKey` to `WebKey`
    - moved details (id, creation_date, change_date) into the `WebKey` object from a dedicated message
- Additional type changes
  - `WebKeyState` -> `State`
  - `WebKeyRSAConfig` -> `RSA`
  - `WebKeyECDSAConfig` -> `ECDSA`
  - `WebKeyED25519Config` -> `ED25519`
  - `RSABits`, `RSAHasher` and `ECDSACurve` moved into root enums (form nested config enum)
- improved documentation to reflect the new API design guidelines
- Internal changes
  - DeleteWebKey
    - will now only return deletion time
    - no longer returns an error on not existing
    - returns the old deletion timestamp in case of a previously deleted key

# Additional Changes

Removed `Lifecycle` from the API references sidenav for all v2 and v3 services.

# Additional Context

closes #9409 


Linked issue #9409: WebKeys Alpha v3 API to v2 beta API
The new WebKeys API implemented in V3 should be moved to V2 as a beta version. 

## Acceptance Criteria
- [x] WebKeys API v3 alpha is completely removed
- [x] WebKeys API v2 beta is added
- [x] WebKeys API v2 is documented in the APIS
- [x] The REST API should document the one of for one example properly
- [x] Clean Up API Sidenav
   - [x] Remove (General Availability) from V1 and V2 in main nav
   - [x] Add (Beta) to the sub sidenav item, e.g WebKeys
   - [x] Remove Lifecycle in sidenav (Example User Lifecycle --> Users)

Diff:
diff --git a/cmd/start/start.go b/cmd/start/start.go
index 12062951a98..7f43bad5243 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -48,7 +48,6 @@ import (
 	"github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events"
 	user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
 	userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
-	"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
 	saml_v2 "github.com/zitadel/zitadel/internal/api/grpc/saml/v2"
 	session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
 	session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
@@ -57,6 +56,7 @@ import (
 	"github.com/zitadel/zitadel/internal/api/grpc/system"
 	user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
 	user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
+	"github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta"
 	http_util "github.com/zitadel/zitadel/internal/api/http"
 	"github.com/zitadel/zitadel/internal/api/http/middleware"
 	"github.com/zitadel/zitadel/internal/api/idp"
diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md
index 2b414ae7e90..c39275df8ed 100644
--- a/docs/docs/guides/integrate/login/oidc/webkeys.md
+++ b/docs/docs/guides/integrate/login/oidc/webkeys.md
@@ -112,7 +112,7 @@ When the request does not contain any specific configuration,
 [RSA](#rsa) is used as default with the default options as described below:
 
 ```bash
-curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \
+curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>' \
@@ -136,7 +136,7 @@ The RSA generator config takes two enum values.
 For example, to create a RSA web key with the size of 3072 bits and the SHA512 algorithm (RS512):
 
 ```bash
-curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \
+curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>' \
@@ -160,7 +160,7 @@ The ECDSA generator config takes a single `curve` enum value which determines bo
 For example, to create a ECDSA web key with a P-256 curve and the SHA256 algorithm:
 
 ```bash
-curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \
+curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>' \
@@ -184,7 +184,7 @@ Clients which support both curves must inspect `crv` header value to assert the
 For example, to create a ed25519 web key:
 
 ```bash
-curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \
+curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>' \
@@ -246,11 +246,11 @@ For the sake of this example we will use simplified IDs and restrict timestamps
 After one month, on 2025-02-01, we wish to activate the next available key and create a new key to be available for activation next month. This fulfills requirements 1 and 2.
 
 ```bash
-curl -L -X POST 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/2/_activate' \
+curl -L -X POST 'https://$CUSTOM-DOMAIN/v2beta/web_keys/2/_activate' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>'
 
-curl -L 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys' \
+curl -L 'https://$CUSTOM-DOMAIN/v2beta/web_keys' \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>' \
@@ -282,7 +282,7 @@ In addition to the activate and create calls we made on this iteration,
 we can now safely delete the oldest key, as both requirement 3 and 4 are now fulfilled:
 
 ```bash
-curl -L -X DELETE 'https://$CUSTOM-DOMAIN/resources/v3alpha/web_keys/1' \
+curl -L -X DELETE 'https://$CUSTOM-DOMAIN/v2beta/web_keys/1' \
 -H 'Accept: application/json' \
 -H 'Authorization: Bearer <TOKEN>'
 ```
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index c10c89e1275..65f8d2ac2a5 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -332,9 +332,9 @@ module.exports = {
                 categoryLinkSource: "auto",
             },
           },
-          webkey_v3: {
-            specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json",
-            outputDir: "docs/apis/resources/webkey_service_v3",
+          webkey_v2: {
+            specPath: ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json",
+            outputDir: "docs/apis/resources/webkey_service_v2",
             sidebarOptions: {
                 groupPathsBy: "tag",
                 categoryLinkSource: "auto",
diff --git a/docs/sidebars.js b/docs/sidebars.js
index e6c9609b8e1..c986ff4050f 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -576,7 +576,7 @@ module.exports = {
       items: [
         {
           type: "category",
-          label: "V1 (Generally Available)",
+          label: "V1",
           collapsed: false,
           link: {
             type: "generated-index",
@@ -640,7 +640,7 @@ module.exports = {
         },
         {
           type: "category",
-          label: "V2 (Generally Available)",
+          label: "V2",
           collapsed: false,
           link: {
             type: "doc",
@@ -649,7 +649,7 @@ module.exports = {
           items: [
             {
               type: "category",
-              label: "User Lifecycle",
+              label: "User",
               link: {
                 type: "generated-index",
                 title: "User Service API",
@@ -661,7 +661,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "Session Lifecycle",
+              label: "Session",
               link: {
                 type: "generated-index",
                 title: "Session Service API",
@@ -673,7 +673,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "OIDC Lifecycle",
+              label: "OIDC",
               link: {
                 type: "generated-index",
                 title: "OIDC Service API",
@@ -685,7 +685,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "Settings Lifecycle",
+              label: "Settings",
               link: {
                 type: "generated-index",
                 title: "Settings Service API",
@@ -697,7 +697,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "Feature Lifecycle",
+              label: "Feature",
               link: {
                 type: "generated-index",
                 title: "Feature Service API",
@@ -709,7 +709,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "Organization Lifecycle",
+              label: "Organization",
               link: {
                 type: "generated-index",
                 title: "Organization Service API",
@@ -721,7 +721,7 @@ module.exports = {
             },
             {
               type: "category",
-              label: "Identity Provider Lifecycle",
+              label: "Identity Provider",
               link: {
                 type: "generated-index",
                 title: "Identity Provider Service API",
@@ -731,6 +731,24 @@ module.exports = {
               },
               items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"),
             },
+            {
+              type: "category",
+              label: "Web key (Beta)",
+              link: {
+                type: "generated-index",
+                title: "Web Key Service API (Beta)",
+                slug: "/apis/resources/webkey_service_v2",
+                description:
+                    "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" +
+                    "\n" +
+                    "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+
+                    "\n"+
+                    "The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n"+
+                    "\n"+
+                    "Please make sure to enable the `web_key` feature flag on your instance to use this service.",
+              },
+              items: require("./docs/apis/resources/webkey_service_v2/sidebar.ts"),
+            },
           ],
         },
         {
@@ -740,7 +758,7 @@ module.exports = {
           items: [
             {
               type: "category",
-              label: "Action Lifecycle (Preview)",
+              label: "Action (Preview)",
               link: {
                 type: "generated-index",
                 title: "Action Service API (Preview)",
@@ -752,7 +770,7 @@ module.exports = {
                   "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v3 action executions anymore.\n" +
                   "Instead, it calls external endpoints which are implemented and maintained by action v3 users.\n" +
                   "\n" +
-                  "This project is in Preview state. It can AND will continue breaking until the services provide the same functionality as the current actions.",
+                  "This service is in Preview state. It can AND will continue breaking until the services provide the same functionality as the current actions.",
               },
               items: [{
                 type: "doc",
@@ -762,20 +780,6 @@ module.exports = {
                 id: "apis/actions/v3/testing-locally",
               }].concat(require("./docs/apis/resources/action_service_v3/sidebar.ts")),
             },
-            {
-              type: "category",
-              label: "Web key Lifecycle (Preview)",
-              link: {
-                type: "generated-index",
-                title: "Web Key Service API (Preview)",
-                slug: "/apis/resources/webkey_service_v3",
-                description:
-                  "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" +
-                  "\n" +
-                  "This project is in preview state. It can AND will continue breaking until a stable version is released.",
-              },
-              items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"),
-            },
           ],
         },
         {
diff --git a/go.mod b/go.mod
index f316c3e8665..d689e2cdd34 100644
--- a/go.mod
+++ b/go.mod
@@ -57,6 +57,9 @@ require (
 	github.com/pquerna/otp v1.4.0
 	github.com/rakyll/statik v0.1.7
 	github.com/redis/go-redis/v9 v9.7.0
+	github.com/riverqueue/river v0.16.0
+	github.com/riverqueue/river/riverdriver v0.16.0
+	github.com/riverqueue/river/rivertype v0.16.0
 	github.com/rs/cors v1.11.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
 	github.com/sony/gobreaker/v2 v2.0.0
@@ -124,10 +127,7 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/riverqueue/river v0.16.0 // indirect
-	github.com/riverqueue/river/riverdriver v0.16.0 // indirect
 	github.com/riverqueue/river/rivershared v0.16.0 // indirect
-	github.com/riverqueue/river/rivertype v0.16.0 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index 709d6b7a144..0a2058732ff 100644
--- a/go.sum
+++ b/go.sum
@@ -416,16 +416,14 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.7.0 h1:FG6VLIdzvAPhnYqP14sQ2xhFLkiUQHCs6ySqO91kF4g=
-github.com/jackc/pgx/v5 v5.7.0/go.mod h1:awP1KNnjylvpxHuHP63gzjhnGkI1iw+PMoIwvoleN/8=
 github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
 github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
-github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
-github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 h1:jny9eqYPwkG8IVy7foUoRjQmFLcArCSz+uPsL6KS0HQ=
@@ -648,12 +646,16 @@ github.com/riverqueue/river v0.16.0 h1:YyQrs0kGgjuABwgat02DPUYS0TMyG2ZFlzvf6+fSF
 github.com/riverqueue/river v0.16.0/go.mod h1:pEZ8Gc15XyFjVY89nJeL256ub5z18XF7ukYn8ktqQrs=
 github.com/riverqueue/river/riverdriver v0.16.0 h1:y4Df4e1Xk3Id0nnu1VxHJn9118OzmRHcmvOxM/i1Q30=
 github.com/riverqueue/river/riverdriver v0.16.0/go.mod h1:7Kdf5HQDrLyLUUqPqXobaK+7zbcMctWeAl7yhg4nHes=
+github.com/riverqueue/river/riverdriver/riverdatabasesql v0.16.0 h1:T/DcMmZXiJAyLN3CSyAoNcf3U4oAD9Ht/8Vd5SXv5YU=
+github.com/riverqueue/river/riverdriver/riverdatabasesql v0.16.0/go.mod h1:a9EUhD2yGsAeM9eWo+QrGGbL8LVWoGj2m8KEzm0xUxE=
 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.16.0 h1:6HP296OPN+3ORL9qG1f561pldB5eovkLzfkNIQmaTXI=
 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.16.0/go.mod h1:MAeBNoTQ+CD3nRvV9mF6iCBfsGJTxYHZeZSP4MYoeUE=
 github.com/riverqueue/river/rivershared v0.16.0 h1:L1lQ3gMwdIsxA6yF0/PwAdsFP0T82yBD1V03q2GuJDU=
 github.com/riverqueue/river/rivershared v0.16.0/go.mod h1:y5Xu8Shcp44DUNnEQV4c6oWH4m2OTkSMCe6nRrgzT34=
 github.com/riverqueue/river/rivertype v0.16.0 h1:iDjNtCiUbXwLraqNEyQdH/OD80f1wTo8Ai6WHYCwRxs=
 github.com/riverqueue/river/rivertype v0.16.0/go.mod h1:DETcejveWlq6bAb8tHkbgJqmXWVLiFhTiEm8j7co1bE=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -727,7 +729,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -924,8 +925,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
 golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter.go b/internal/api/grpc/resources/webkey/v3/webkey_converter.go
deleted file mode 100644
index b460775dd5f..00000000000
--- a/internal/api/grpc/resources/webkey/v3/webkey_converter.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package webkey
-
-import (
-	resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
-	"github.com/zitadel/zitadel/internal/crypto"
-	"github.com/zitadel/zitadel/internal/domain"
-	"github.com/zitadel/zitadel/internal/query"
-	object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
-	webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
-)
-
-func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
-	switch config := req.GetKey().GetConfig().(type) {
-	case *webkey.WebKey_Rsa:
-		return webKeyRSAConfigToCrypto(config.Rsa)
-	case *webkey.WebKey_Ecdsa:
-		return webKeyECDSAConfigToCrypto(config.Ecdsa)
-	case *webkey.WebKey_Ed25519:
-		return new(crypto.WebKeyED25519Config)
-	default:
-		return webKeyRSAConfigToCrypto(nil)
-	}
-}
-
-func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig {
-	out := new(crypto.WebKeyRSAConfig)
-
-	switch config.GetBits() {
-	case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED:
-		out.Bits = crypto.RSABits2048
-	case webkey.WebKeyRSAConfig_RSA_BITS_2048:
-		out.Bits = crypto.RSABits2048
-	case webkey.WebKeyRSAConfig_RSA_BITS_3072:
-		out.Bits = crypto.RSABits3072
-	case webkey.WebKeyRSAConfig_RSA_BITS_4096:
-		out.Bits = crypto.RSABits4096
-	default:
-		out.Bits = crypto.RSABits2048
-	}
-
-	switch config.GetHasher() {
-	case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED:
-		out.Hasher = crypto.RSAHasherSHA256
-	case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256:
-		out.Hasher = crypto.RSAHasherSHA256
-	case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384:
-		out.Hasher = crypto.RSAHasherSHA384
-	case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512:
-		out.Hasher = crypto.RSAHasherSHA512
-	default:
-		out.Hasher = crypto.RSAHasherSHA256
-	}
-
-	return out
-}
-
-func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig {
-	out := new(crypto.WebKeyECDSAConfig)
-
-	switch config.GetCurve() {
-	case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED:
-		out.Curve = crypto.EllipticCurveP256
-	case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256:
-		out.Curve = crypto.EllipticCurveP256
-	case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384:
-		out.Curve = crypto.EllipticCurveP384
-	case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512:
-		out.Curve = crypto.EllipticCurveP512
-	default:
-		out.Curve = crypto.EllipticCurveP256
-	}
-
-	return out
-}
-
-func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey {
-	out := make([]*webkey.GetWebKey, len(list))
-	for i := range list {
-		out[i] = webKeyDetailsToPb(&list[i], instanceID)
-	}
-	return out
-}
-
-func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey {
-	out := &webkey.GetWebKey{
-		Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{
-			ID:           details.KeyID,
-			CreationDate: details.CreationDate,
-			EventDate:    details.ChangeDate,
-		}, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
-		State:  webKeyStateToPb(details.State),
-		Config: &webkey.WebKey{},
-	}
-
-	switch config := details.Config.(type) {
-	case *crypto.WebKeyRSAConfig:
-		out.Config.Config = &webkey.WebKey_Rsa{
-			Rsa: webKeyRSAConfigToPb(config),
-		}
-	case *crypto.WebKeyECDSAConfig:
-		out.Config.Config = &webkey.WebKey_Ecdsa{
-			Ecdsa: webKeyECDSAConfigToPb(config),
-		}
-	case *crypto.WebKeyED25519Config:
-		out.Config.Config = &webkey.WebKey_Ed25519{
-			Ed25519: new(webkey.WebKeyED25519Config),
-		}
-	}
-
-	return out
-}
-
-func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState {
-	switch state {
-	case domain.WebKeyStateUnspecified:
-		return webkey.WebKeyState_STATE_UNSPECIFIED
-	case domain.WebKeyStateInitial:
-		return webkey.WebKeyState_STATE_INITIAL
-	case domain.WebKeyStateActive:
-		return webkey.WebKeyState_STATE_ACTIVE
-	case domain.WebKeyStateInactive:
-		return webkey.WebKeyState_STATE_INACTIVE
-	case domain.WebKeyStateRemoved:
-		return webkey.WebKeyState_STATE_REMOVED
-	default:
-		return webkey.WebKeyState_STATE_UNSPECIFIED
-	}
-}
-
-func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig {
-	out := new(webkey.WebKeyRSAConfig)
-
-	switch config.Bits {
-	case crypto.RSABitsUnspecified:
-		out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED
-	case crypto.RSABits2048:
-		out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048
-	case crypto.RSABits3072:
-		out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072
-	case crypto.RSABits4096:
-		out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096
-	}
-
-	switch config.Hasher {
-	case crypto.RSAHasherUnspecified:
-		out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED
-	case crypto.RSAHasherSHA256:
-		out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256
-	case crypto.RSAHasherSHA384:
-		out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384
-	case crypto.RSAHasherSHA512:
-		out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512
-	}
-
-	return out
-}
-
-func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig {
-	out := new(webkey.WebKeyECDSAConfig)
-
-	switch config.Curve {
-	case crypto.EllipticCurveUnspecified:
-		out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED
-	case crypto.EllipticCurveP256:
-		out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256
-	case crypto.EllipticCurveP384:
-		out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384
-	case crypto.EllipticCurveP512:
-		out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512
-	}
-
-	return out
-}
diff --git a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go
similarity index 65%
rename from internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go
rename to internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go
index eafa733fd10..002669c2333 100644
--- a/internal/api/grpc/resources/webkey/v3/integration_test/webkey_integration_test.go
+++ b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go
@@ -17,9 +17,7 @@ import (
 
 	"github.com/zitadel/zitadel/internal/integration"
 	"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
-	object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
-	resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
-	webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
 )
 
 var (
@@ -37,7 +35,7 @@ func TestMain(m *testing.M) {
 
 func TestServer_Feature_Disabled(t *testing.T) {
 	instance, iamCtx, _ := createInstance(t, false)
-	client := instance.Client.WebKeyV3Alpha
+	client := instance.Client.WebKeyV2Beta
 
 	t.Run("CreateWebKey", func(t *testing.T) {
 		_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{})
@@ -65,84 +63,78 @@ func TestServer_ListWebKeys(t *testing.T) {
 	instance, iamCtx, creationDate := createInstance(t, true)
 	// After the feature is first enabled, we can expect 2 generated keys with the default config.
 	checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{
-		Rsa: &webkey.WebKeyRSAConfig{
-			Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-			Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+		Rsa: &webkey.RSA{
+			Bits:   webkey.RSABits_RSA_BITS_2048,
+			Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 		},
 	}, creationDate)
 }
 
 func TestServer_CreateWebKey(t *testing.T) {
 	instance, iamCtx, creationDate := createInstance(t, true)
-	client := instance.Client.WebKeyV3Alpha
+	client := instance.Client.WebKeyV2Beta
 
 	_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
-		Key: &webkey.WebKey{
-			Config: &webkey.WebKey_Rsa{
-				Rsa: &webkey.WebKeyRSAConfig{
-					Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-					Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
-				},
+		Key: &webkey.CreateWebKeyRequest_Rsa{
+			Rsa: &webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_2048,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 			},
 		},
 	})
 	require.NoError(t, err)
 
 	checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{
-		Rsa: &webkey.WebKeyRSAConfig{
-			Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-			Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+		Rsa: &webkey.RSA{
+			Bits:   webkey.RSABits_RSA_BITS_2048,
+			Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 		},
 	}, creationDate)
 }
 
 func TestServer_ActivateWebKey(t *testing.T) {
 	instance, iamCtx, creationDate := createInstance(t, true)
-	client := instance.Client.WebKeyV3Alpha
+	client := instance.Client.WebKeyV2Beta
 
 	resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
-		Key: &webkey.WebKey{
-			Config: &webkey.WebKey_Rsa{
-				Rsa: &webkey.WebKeyRSAConfig{
-					Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-					Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
-				},
+		Key: &webkey.CreateWebKeyRequest_Rsa{
+			Rsa: &webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_2048,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 			},
 		},
 	})
 	require.NoError(t, err)
 
 	_, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
-		Id: resp.GetDetails().GetId(),
+		Id: resp.GetId(),
 	})
 	require.NoError(t, err)
 
-	checkWebKeyListState(iamCtx, t, instance, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{
-		Rsa: &webkey.WebKeyRSAConfig{
-			Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-			Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+	checkWebKeyListState(iamCtx, t, instance, 3, resp.GetId(), &webkey.WebKey_Rsa{
+		Rsa: &webkey.RSA{
+			Bits:   webkey.RSABits_RSA_BITS_2048,
+			Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 		},
 	}, creationDate)
 }
 
 func TestServer_DeleteWebKey(t *testing.T) {
 	instance, iamCtx, creationDate := createInstance(t, true)
-	client := instance.Client.WebKeyV3Alpha
+	client := instance.Client.WebKeyV2Beta
 
 	keyIDs := make([]string, 2)
 	for i := 0; i < 2; i++ {
 		resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
-			Key: &webkey.WebKey{
-				Config: &webkey.WebKey_Rsa{
-					Rsa: &webkey.WebKeyRSAConfig{
-						Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-						Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
-					},
+			Key: &webkey.CreateWebKeyRequest_Rsa{
+				Rsa: &webkey.RSA{
+					Bits:   webkey.RSABits_RSA_BITS_2048,
+					Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 				},
 			},
 		})
 		require.NoError(t, err)
-		keyIDs[i] = resp.GetDetails().GetId()
+		keyIDs[i] = resp.GetId()
 	}
 	_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
 		Id: keyIDs[0],
@@ -162,11 +154,35 @@ func TestServer_DeleteWebKey(t *testing.T) {
 		return
 	}
 
+	start := time.Now()
 	ok = t.Run("delete inactive key", func(t *testing.T) {
-		_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
+		resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
+			Id: keyIDs[1],
+		})
+		require.NoError(t, err)
+		require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now())
+	})
+	if !ok {
+		return
+	}
+
+	ok = t.Run("delete inactive key again", func(t *testing.T) {
+		resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
 			Id: keyIDs[1],
 		})
 		require.NoError(t, err)
+		require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now())
+	})
+	if !ok {
+		return
+	}
+
+	ok = t.Run("delete not existing key", func(t *testing.T) {
+		resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
+			Id: "not-existing",
+		})
+		require.NoError(t, err)
+		require.Nil(t, resp.DeletionDate)
 	})
 	if !ok {
 		return
@@ -174,9 +190,9 @@ func TestServer_DeleteWebKey(t *testing.T) {
 
 	// There are 2 keys from feature setup, +2 created, -1 deleted = 3
 	checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{
-		Rsa: &webkey.WebKeyRSAConfig{
-			Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-			Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+		Rsa: &webkey.RSA{
+			Bits:   webkey.RSABits_RSA_BITS_2048,
+			Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 		},
 	}, creationDate)
 }
@@ -195,7 +211,7 @@ func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, co
 
 	retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute)
 	assert.EventuallyWithT(t, func(collect *assert.CollectT) {
-		resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
+		resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
 		if enableFeature {
 			assert.NoError(collect, err)
 			assert.Len(collect, resp.GetWebKeys(), 2)
@@ -220,7 +236,7 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati
 
 	retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
 	assert.EventuallyWithT(t, func(collect *assert.CollectT) {
-		resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
+		resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
 		require.NoError(collect, err)
 		list := resp.GetWebKeys()
 		assert.Len(collect, list, nKeys)
@@ -228,21 +244,14 @@ func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integrati
 		now := time.Now()
 		var gotActiveKeyID string
 		for _, key := range list {
-			integration.AssertResourceDetails(t, &resource_object.Details{
-				Created: creationDate,
-				Changed: creationDate,
-				Owner: &object.Owner{
-					Type: object.OwnerType_OWNER_TYPE_INSTANCE,
-					Id:   instance.ID(),
-				},
-			}, key.GetDetails())
-			assert.WithinRange(collect, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
-			assert.NotEqual(collect, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState())
-			assert.NotEqual(collect, webkey.WebKeyState_STATE_REMOVED, key.GetState())
-			assert.Equal(collect, config, key.GetConfig().GetConfig())
-
-			if key.GetState() == webkey.WebKeyState_STATE_ACTIVE {
-				gotActiveKeyID = key.GetDetails().GetId()
+			assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
+			assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
+			assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState())
+			assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState())
+			assert.Equal(collect, config, key.GetKey())
+
+			if key.GetState() == webkey.State_STATE_ACTIVE {
+				gotActiveKeyID = key.GetId()
 			}
 		}
 		assert.NotEmpty(collect, gotActiveKeyID)
diff --git a/internal/api/grpc/resources/webkey/v3/server.go b/internal/api/grpc/webkey/v2beta/server.go
similarity index 67%
rename from internal/api/grpc/resources/webkey/v3/server.go
rename to internal/api/grpc/webkey/v2beta/server.go
index 4e979659321..0d4ddb19c82 100644
--- a/internal/api/grpc/resources/webkey/v3/server.go
+++ b/internal/api/grpc/webkey/v2beta/server.go
@@ -7,11 +7,11 @@ import (
 	"github.com/zitadel/zitadel/internal/api/grpc/server"
 	"github.com/zitadel/zitadel/internal/command"
 	"github.com/zitadel/zitadel/internal/query"
-	webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
 )
 
 type Server struct {
-	webkey.UnimplementedZITADELWebKeysServer
+	webkey.UnimplementedWebKeyServiceServer
 	command *command.Commands
 	query   *query.Queries
 }
@@ -27,21 +27,21 @@ func CreateServer(
 }
 
 func (s *Server) RegisterServer(grpcServer *grpc.Server) {
-	webkey.RegisterZITADELWebKeysServer(grpcServer, s)
+	webkey.RegisterWebKeyServiceServer(grpcServer, s)
 }
 
 func (s *Server) AppName() string {
-	return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
+	return webkey.WebKeyService_ServiceDesc.ServiceName
 }
 
 func (s *Server) MethodPrefix() string {
-	return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
+	return webkey.WebKeyService_ServiceDesc.ServiceName
 }
 
 func (s *Server) AuthMethods() authz.MethodMapping {
-	return webkey.ZITADELWebKeys_AuthMethods
+	return webkey.WebKeyService_AuthMethods
 }
 
 func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
-	return webkey.RegisterZITADELWebKeysHandler
+	return webkey.RegisterWebKeyServiceHandler
 }
diff --git a/internal/api/grpc/resources/webkey/v3/webkey.go b/internal/api/grpc/webkey/v2beta/webkey.go
similarity index 72%
rename from internal/api/grpc/resources/webkey/v3/webkey.go
rename to internal/api/grpc/webkey/v2beta/webkey.go
index 8a6e72f9504..eb7cf62f27b 100644
--- a/internal/api/grpc/resources/webkey/v3/webkey.go
+++ b/internal/api/grpc/webkey/v2beta/webkey.go
@@ -3,12 +3,12 @@ package webkey
 import (
 	"context"
 
+	"google.golang.org/protobuf/types/known/timestamppb"
+
 	"github.com/zitadel/zitadel/internal/api/authz"
-	resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
 	"github.com/zitadel/zitadel/internal/telemetry/tracing"
 	"github.com/zitadel/zitadel/internal/zerrors"
-	object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
-	webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
 )
 
 func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) {
@@ -24,7 +24,8 @@ func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyReque
 	}
 
 	return &webkey.CreateWebKeyResponse{
-		Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
+		Id:           webKey.KeyID,
+		CreationDate: timestamppb.New(webKey.ObjectDetails.CreationDate),
 	}, nil
 }
 
@@ -41,7 +42,7 @@ func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyR
 	}
 
 	return &webkey.ActivateWebKeyResponse{
-		Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
+		ChangeDate: timestamppb.New(details.EventDate),
 	}, nil
 }
 
@@ -52,13 +53,17 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque
 	if err = checkWebKeyFeature(ctx); err != nil {
 		return nil, err
 	}
-	details, err := s.command.DeleteWebKey(ctx, req.GetId())
+	deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId())
 	if err != nil {
 		return nil, err
 	}
 
+	var deletionDate *timestamppb.Timestamp
+	if !deletedAt.IsZero() {
+		deletionDate = timestamppb.New(deletedAt)
+	}
 	return &webkey.DeleteWebKeyResponse{
-		Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
+		DeletionDate: deletionDate,
 	}, nil
 }
 
@@ -75,7 +80,7 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest)
 	}
 
 	return &webkey.ListWebKeysResponse{
-		WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()),
+		WebKeys: webKeyDetailsListToPb(list),
 	}, nil
 }
 
diff --git a/internal/api/grpc/webkey/v2beta/webkey_converter.go b/internal/api/grpc/webkey/v2beta/webkey_converter.go
new file mode 100644
index 00000000000..ac8939470b9
--- /dev/null
+++ b/internal/api/grpc/webkey/v2beta/webkey_converter.go
@@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA256:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA384:
+		out.Hasher = crypto.RSAHasherSHA384
+	case webkey.RSAHasher_RSA_HASHER_SHA512:
+		out.Hasher = crypto.RSAHasherSHA512
+	default:
+		out.Hasher = crypto.RSAHasherSHA256
+	}
+
+	return out
+}
+
+func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig {
+	out := new(crypto.WebKeyECDSAConfig)
+
+	switch config.GetCurve() {
+	case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED:
+		out.Curve = crypto.EllipticCurveP256
+	case webkey.ECDSACurve_ECDSA_CURVE_P256:
+		out.Curve = crypto.EllipticCurveP256
+	case webkey.ECDSACurve_ECDSA_CURVE_P384:
+		out.Curve = crypto.EllipticCurveP384
+	case webkey.ECDSACurve_ECDSA_CURVE_P512:
+		out.Curve = crypto.EllipticCurveP512
+	default:
+		out.Curve = crypto.EllipticCurveP256
+	}
+
+	return out
+}
+
+func webKeyDetailsListToPb(list []query.WebKeyDetails) []*webkey.WebKey {
+	out := make([]*webkey.WebKey, len(list))
+	for i := range list {
+		out[i] = webKeyDetailsToPb(&list[i])
+	}
+	return out
+}
+
+func webKeyDetailsToPb(details *query.WebKeyDetails) *webkey.WebKey {
+	out := &webkey.WebKey{
+		Id:           details.KeyID,
+		CreationDate: timestamppb.New(details.CreationDate),
+		ChangeDate:   timestamppb.New(details.ChangeDate),
+		State:        webKeyStateToPb(details.State),
+	}
+
+	switch config := details.Config.(type) {
+	case *crypto.WebKeyRSAConfig:
+		out.Key = &webkey.WebKey_Rsa{
+			Rsa: webKeyRSAConfigToPb(config),
+		}
+	case *crypto.WebKeyECDSAConfig:
+		out.Key = &webkey.WebKey_Ecdsa{
+			Ecdsa: webKeyECDSAConfigToPb(config),
+		}
+	case *crypto.WebKeyED25519Config:
+		out.Key = &webkey.WebKey_Ed25519{
+			Ed25519: new(webkey.ED25519),
+		}
+	}
+
+	return out
+}
+
+func webKeyStateToPb(state domain.WebKeyState) webkey.State {
+	switch state {
+	case domain.WebKeyStateUnspecified:
+		return webkey.State_STATE_UNSPECIFIED
+	case domain.WebKeyStateInitial:
+		return webkey.State_STATE_INITIAL
+	case domain.WebKeyStateActive:
+		return webkey.State_STATE_ACTIVE
+	case domain.WebKeyStateInactive:
+		return webkey.State_STATE_INACTIVE
+	case domain.WebKeyStateRemoved:
+		return webkey.State_STATE_REMOVED
+	default:
+		return webkey.State_STATE_UNSPECIFIED
+	}
+}
+
+func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.RSA {
+	out := new(webkey.RSA)
+
+	switch config.Bits {
+	case crypto.RSABitsUnspecified:
+		out.Bits = webkey.RSABits_RSA_BITS_UNSPECIFIED
+	case crypto.RSABits2048:
+		out.Bits = webkey.RSABits_RSA_BITS_2048
+	case crypto.RSABits3072:
+		out.Bits = webkey.RSABits_RSA_BITS_3072
+	case crypto.RSABits4096:
+		out.Bits = webkey.RSABits_RSA_BITS_4096
+	}
+
+	switch config.Hasher {
+	case crypto.RSAHasherUnspecified:
+		out.Hasher = webkey.RSAHasher_RSA_HASHER_UNSPECIFIED
+	case crypto.RSAHasherSHA256:
+		out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA256
+	case crypto.RSAHasherSHA384:
+		out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA384
+	case crypto.RSAHasherSHA512:
+		out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA512
+	}
+
+	return out
+}
+
+func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.ECDSA {
+	out := new(webkey.ECDSA)
+
+	switch config.Curve {
+	case crypto.EllipticCurveUnspecified:
+		out.Curve = webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED
+	case crypto.EllipticCurveP256:
+		out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P256
+	case crypto.EllipticCurveP384:
+		out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P384
+	case crypto.EllipticCurveP512:
+		out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P512
+	}
+
+	return out
+}
diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go
similarity index 56%
rename from internal/api/grpc/resources/webkey/v3/webkey_converter_test.go
rename to internal/api/grpc/webkey/v2beta/webkey_converter_test.go
index e755d2be087..d78e9968dc1 100644
--- a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go
+++ b/internal/api/grpc/webkey/v2beta/webkey_converter_test.go
@@ -10,9 +10,7 @@ import (
 	"github.com/zitadel/zitadel/internal/crypto"
 	"github.com/zitadel/zitadel/internal/domain"
 	"github.com/zitadel/zitadel/internal/query"
-	object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
-	resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
-	webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
 )
 
 func Test_createWebKeyRequestToConfig(t *testing.T) {
@@ -27,12 +25,10 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
 		{
 			name: "RSA",
 			args: args{&webkey.CreateWebKeyRequest{
-				Key: &webkey.WebKey{
-					Config: &webkey.WebKey_Rsa{
-						Rsa: &webkey.WebKeyRSAConfig{
-							Bits:   webkey.WebKeyRSAConfig_RSA_BITS_3072,
-							Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
-						},
+				Key: &webkey.CreateWebKeyRequest_Rsa{
+					Rsa: &webkey.RSA{
+						Bits:   webkey.RSABits_RSA_BITS_3072,
+						Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
 					},
 				},
 			}},
@@ -44,11 +40,9 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
 		{
 			name: "ECDSA",
 			args: args{&webkey.CreateWebKeyRequest{
-				Key: &webkey.WebKey{
-					Config: &webkey.WebKey_Ecdsa{
-						Ecdsa: &webkey.WebKeyECDSAConfig{
-							Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
-						},
+				Key: &webkey.CreateWebKeyRequest_Ecdsa{
+					Ecdsa: &webkey.ECDSA{
+						Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
 					},
 				},
 			}},
@@ -59,10 +53,8 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
 		{
 			name: "ED25519",
 			args: args{&webkey.CreateWebKeyRequest{
-				Key: &webkey.WebKey{
-					Config: &webkey.WebKey_Ed25519{
-						Ed25519: &webkey.WebKeyED25519Config{},
-					},
+				Key: &webkey.CreateWebKeyRequest_Ed25519{
+					Ed25519: &webkey.ED25519{},
 				},
 			}},
 			want: &crypto.WebKeyED25519Config{},
@@ -86,7 +78,7 @@ func Test_createWebKeyRequestToConfig(t *testing.T) {
 
 func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 	type args struct {
-		config *webkey.WebKeyRSAConfig
+		config *webkey.RSA
 	}
 	tests := []struct {
 		name string
@@ -95,9 +87,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 	}{
 		{
 			name: "unspecified",
-			args: args{&webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED,
+			args: args{&webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_UNSPECIFIED,
+				Hasher: webkey.RSAHasher_RSA_HASHER_UNSPECIFIED,
 			}},
 			want: &crypto.WebKeyRSAConfig{
 				Bits:   crypto.RSABits2048,
@@ -106,9 +98,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "2048, RSA256",
-			args: args{&webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+			args: args{&webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_2048,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 			}},
 			want: &crypto.WebKeyRSAConfig{
 				Bits:   crypto.RSABits2048,
@@ -117,9 +109,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "3072, RSA384",
-			args: args{&webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_3072,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
+			args: args{&webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_3072,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
 			}},
 			want: &crypto.WebKeyRSAConfig{
 				Bits:   crypto.RSABits3072,
@@ -128,9 +120,9 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "4096, RSA512",
-			args: args{&webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_4096,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
+			args: args{&webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_4096,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA512,
 			}},
 			want: &crypto.WebKeyRSAConfig{
 				Bits:   crypto.RSABits4096,
@@ -139,7 +131,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "invalid",
-			args: args{&webkey.WebKeyRSAConfig{
+			args: args{&webkey.RSA{
 				Bits:   99,
 				Hasher: 99,
 			}},
@@ -151,7 +143,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := webKeyRSAConfigToCrypto(tt.args.config)
+			got := rsaToCrypto(tt.args.config)
 			assert.Equal(t, tt.want, got)
 		})
 	}
@@ -159,7 +151,7 @@ func Test_webKeyRSAConfigToCrypto(t *testing.T) {
 
 func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 	type args struct {
-		config *webkey.WebKeyECDSAConfig
+		config *webkey.ECDSA
 	}
 	tests := []struct {
 		name string
@@ -168,8 +160,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 	}{
 		{
 			name: "unspecified",
-			args: args{&webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED,
+			args: args{&webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED,
 			}},
 			want: &crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP256,
@@ -177,8 +169,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "P256",
-			args: args{&webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
+			args: args{&webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P256,
 			}},
 			want: &crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP256,
@@ -186,8 +178,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "P384",
-			args: args{&webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
+			args: args{&webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
 			}},
 			want: &crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP384,
@@ -195,8 +187,8 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "P512",
-			args: args{&webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
+			args: args{&webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P512,
 			}},
 			want: &crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP512,
@@ -204,7 +196,7 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 		},
 		{
 			name: "invalid",
-			args: args{&webkey.WebKeyECDSAConfig{
+			args: args{&webkey.ECDSA{
 				Curve: 99,
 			}},
 			want: &crypto.WebKeyECDSAConfig{
@@ -214,14 +206,13 @@ func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := webKeyECDSAConfigToCrypto(tt.args.config)
+			got := ecdsaToCrypto(tt.args.config)
 			assert.Equal(t, tt.want, got)
 		})
 	}
 }
 
 func Test_webKeyDetailsListToPb(t *testing.T) {
-	instanceID := "ownerid"
 	list := []query.WebKeyDetails{
 		{
 			KeyID:        "key1",
@@ -243,52 +234,41 @@ func Test_webKeyDetailsListToPb(t *testing.T) {
 			Config:       &crypto.WebKeyED25519Config{},
 		},
 	}
-	want := []*webkey.GetWebKey{
-		{
-			Details: &resource_object.Details{
-				Id:      "key1",
-				Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
-				Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
-				Owner:   &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
-			},
-			State: webkey.WebKeyState_STATE_ACTIVE,
-			Config: &webkey.WebKey{
-				Config: &webkey.WebKey_Rsa{
-					Rsa: &webkey.WebKeyRSAConfig{
-						Bits:   webkey.WebKeyRSAConfig_RSA_BITS_3072,
-						Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
-					},
+	want := []*webkey.WebKey{
+		{
+			Id:           "key1",
+			CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
+			ChangeDate:   &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
+			State:        webkey.State_STATE_ACTIVE,
+			Key: &webkey.WebKey_Rsa{
+				Rsa: &webkey.RSA{
+					Bits:   webkey.RSABits_RSA_BITS_3072,
+					Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
 				},
 			},
 		},
 		{
-			Details: &resource_object.Details{
-				Id:      "key2",
-				Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
-				Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
-				Owner:   &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
-			},
-			State: webkey.WebKeyState_STATE_ACTIVE,
-			Config: &webkey.WebKey{
-				Config: &webkey.WebKey_Ed25519{
-					Ed25519: &webkey.WebKeyED25519Config{},
-				},
+			Id:           "key2",
+			CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
+			ChangeDate:   &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
+			State:        webkey.State_STATE_ACTIVE,
+			Key: &webkey.WebKey_Ed25519{
+				Ed25519: &webkey.ED25519{},
 			},
 		},
 	}
-	got := webKeyDetailsListToPb(list, instanceID)
+	got := webKeyDetailsListToPb(list)
 	assert.Equal(t, want, got)
 }
 
 func Test_webKeyDetailsToPb(t *testing.T) {
-	instanceID := "ownerid"
 	type args struct {
 		details *query.WebKeyDetails
 	}
 	tests := []struct {
 		name string
 		args args
-		want *webkey.GetWebKey
+		want *webkey.WebKey
 	}{
 		{
 			name: "RSA",
@@ -303,20 +283,15 @@ func Test_webKeyDetailsToPb(t *testing.T) {
 					Hasher: crypto.RSAHasherSHA384,
 				},
 			}},
-			want: &webkey.GetWebKey{
-				Details: &resource_object.Details{
-					Id:      "keyID",
-					Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
-					Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
-					Owner:   &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
-				},
-				State: webkey.WebKeyState_STATE_ACTIVE,
-				Config: &webkey.WebKey{
-					Config: &webkey.WebKey_Rsa{
-						Rsa: &webkey.WebKeyRSAConfig{
-							Bits:   webkey.WebKeyRSAConfig_RSA_BITS_3072,
-							Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
-						},
+			want: &webkey.WebKey{
+				Id:           "keyID",
+				CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
+				ChangeDate:   &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
+				State:        webkey.State_STATE_ACTIVE,
+				Key: &webkey.WebKey_Rsa{
+					Rsa: &webkey.RSA{
+						Bits:   webkey.RSABits_RSA_BITS_3072,
+						Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
 					},
 				},
 			},
@@ -333,19 +308,14 @@ func Test_webKeyDetailsToPb(t *testing.T) {
 					Curve: crypto.EllipticCurveP384,
 				},
 			}},
-			want: &webkey.GetWebKey{
-				Details: &resource_object.Details{
-					Id:      "keyID",
-					Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
-					Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
-					Owner:   &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
-				},
-				State: webkey.WebKeyState_STATE_ACTIVE,
-				Config: &webkey.WebKey{
-					Config: &webkey.WebKey_Ecdsa{
-						Ecdsa: &webkey.WebKeyECDSAConfig{
-							Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
-						},
+			want: &webkey.WebKey{
+				Id:           "keyID",
+				CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
+				ChangeDate:   &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
+				State:        webkey.State_STATE_ACTIVE,
+				Key: &webkey.WebKey_Ecdsa{
+					Ecdsa: &webkey.ECDSA{
+						Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
 					},
 				},
 			},
@@ -360,25 +330,20 @@ func Test_webKeyDetailsToPb(t *testing.T) {
 				State:        domain.WebKeyStateActive,
 				Config:       &crypto.WebKeyED25519Config{},
 			}},
-			want: &webkey.GetWebKey{
-				Details: &resource_object.Details{
-					Id:      "keyID",
-					Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
-					Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
-					Owner:   &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
-				},
-				State: webkey.WebKeyState_STATE_ACTIVE,
-				Config: &webkey.WebKey{
-					Config: &webkey.WebKey_Ed25519{
-						Ed25519: &webkey.WebKeyED25519Config{},
-					},
+			want: &webkey.WebKey{
+				Id:           "keyID",
+				CreationDate: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
+				ChangeDate:   &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
+				State:        webkey.State_STATE_ACTIVE,
+				Key: &webkey.WebKey_Ed25519{
+					Ed25519: &webkey.ED25519{},
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := webKeyDetailsToPb(tt.args.details, instanceID)
+			got := webKeyDetailsToPb(tt.args.details)
 			assert.Equal(t, tt.want, got)
 		})
 	}
@@ -391,37 +356,37 @@ func Test_webKeyStateToPb(t *testing.T) {
 	tests := []struct {
 		name string
 		args args
-		want webkey.WebKeyState
+		want webkey.State
 	}{
 		{
 			name: "unspecified",
 			args: args{domain.WebKeyStateUnspecified},
-			want: webkey.WebKeyState_STATE_UNSPECIFIED,
+			want: webkey.State_STATE_UNSPECIFIED,
 		},
 		{
 			name: "initial",
 			args: args{domain.WebKeyStateInitial},
-			want: webkey.WebKeyState_STATE_INITIAL,
+			want: webkey.State_STATE_INITIAL,
 		},
 		{
 			name: "active",
 			args: args{domain.WebKeyStateActive},
-			want: webkey.WebKeyState_STATE_ACTIVE,
+			want: webkey.State_STATE_ACTIVE,
 		},
 		{
 			name: "inactive",
 			args: args{domain.WebKeyStateInactive},
-			want: webkey.WebKeyState_STATE_INACTIVE,
+			want: webkey.State_STATE_INACTIVE,
 		},
 		{
 			name: "removed",
 			args: args{domain.WebKeyStateRemoved},
-			want: webkey.WebKeyState_STATE_REMOVED,
+			want: webkey.State_STATE_REMOVED,
 		},
 		{
 			name: "invalid",
 			args: args{99},
-			want: webkey.WebKeyState_STATE_UNSPECIFIED,
+			want: webkey.State_STATE_UNSPECIFIED,
 		},
 	}
 	for _, tt := range tests {
@@ -439,7 +404,7 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
 	tests := []struct {
 		name string
 		args args
-		want *webkey.WebKeyRSAConfig
+		want *webkey.RSA
 	}{
 		{
 			name: "2048, RSA256",
@@ -447,9 +412,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
 				Bits:   crypto.RSABits2048,
 				Hasher: crypto.RSAHasherSHA256,
 			}},
-			want: &webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_2048,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
+			want: &webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_2048,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA256,
 			},
 		},
 		{
@@ -458,9 +423,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
 				Bits:   crypto.RSABits3072,
 				Hasher: crypto.RSAHasherSHA384,
 			}},
-			want: &webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_3072,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
+			want: &webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_3072,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA384,
 			},
 		},
 		{
@@ -469,9 +434,9 @@ func Test_webKeyRSAConfigToPb(t *testing.T) {
 				Bits:   crypto.RSABits4096,
 				Hasher: crypto.RSAHasherSHA512,
 			}},
-			want: &webkey.WebKeyRSAConfig{
-				Bits:   webkey.WebKeyRSAConfig_RSA_BITS_4096,
-				Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
+			want: &webkey.RSA{
+				Bits:   webkey.RSABits_RSA_BITS_4096,
+				Hasher: webkey.RSAHasher_RSA_HASHER_SHA512,
 			},
 		},
 	}
@@ -490,15 +455,15 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
 	tests := []struct {
 		name string
 		args args
-		want *webkey.WebKeyECDSAConfig
+		want *webkey.ECDSA
 	}{
 		{
 			name: "P256",
 			args: args{&crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP256,
 			}},
-			want: &webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
+			want: &webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P256,
 			},
 		},
 		{
@@ -506,8 +471,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
 			args: args{&crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP384,
 			}},
-			want: &webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
+			want: &webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P384,
 			},
 		},
 		{
@@ -515,8 +480,8 @@ func Test_webKeyECDSAConfigToPb(t *testing.T) {
 			args: args{&crypto.WebKeyECDSAConfig{
 				Curve: crypto.EllipticCurveP512,
 			}},
-			want: &webkey.WebKeyECDSAConfig{
-				Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
+			want: &webkey.ECDSA{
+				Curve: webkey.ECDSACurve_ECDSA_CURVE_P512,
 			},
 		},
 	}
diff --git a/internal/command/web_key.go b/internal/command/web_key.go
index e8481541c34..b46d5fc1fcf 100644
--- a/internal/command/web_key.go
+++ b/internal/command/web_key.go
@@ -2,6 +2,7 @@ package command
 
 import (
 	"context"
+	"time"
 
 	"github.com/zitadel/zitadel/internal/api/authz"
 	"github.com/zitadel/zitadel/internal/command/preparation"
@@ -156,27 +157,28 @@ func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWrite
 	return models.keys, models.activeID, nil
 }
 
-func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) {
+func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ time.Time, err error) {
 	ctx, span := tracing.NewSpan(ctx)
 	defer func() { span.EndWithError(err) }()
 
 	model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
 	if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil {
-		return nil, err
+		return time.Time{}, err
 	}
-	if model.State == domain.WebKeyStateUnspecified {
-		return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound")
+	if model.State == domain.WebKeyStateUnspecified ||
+		model.State == domain.WebKeyStateRemoved {
+		return model.WriteModel.ChangeDate, nil
 	}
 	if model.State == domain.WebKeyStateActive {
-		return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete")
+		return time.Time{}, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete")
 	}
 	err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx,
 		webkey.AggregateFromWriteModel(ctx, &model.WriteModel),
 	))
 	if err != nil {
-		return nil, err
+		return time.Time{}, err
 	}
-	return writeModelToObjectDetails(&model.WriteModel), nil
+	return model.WriteModel.ChangeDate, nil
 }
 
 func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation {
diff --git a/internal/command/web_key_test.go b/internal/command/web_key_test.go
index 63463de1df6..13587d0f688 100644
--- a/internal/command/web_key_test.go
+++ b/internal/command/web_key_test.go
@@ -7,6 +7,7 @@ import (
 	"crypto/rand"
 	"io"
 	"testing"
+	"time"
 
 	"github.com/go-jose/go-jose/v4"
 	"github.com/stretchr/testify/assert"
@@ -610,7 +611,7 @@ func TestCommands_DeleteWebKey(t *testing.T) {
 		name    string
 		fields  fields
 		args    args
-		want    *domain.ObjectDetails
+		want    time.Time
 		wantErr error
 	}{
 		{
@@ -624,14 +625,73 @@ func TestCommands_DeleteWebKey(t *testing.T) {
 			wantErr: io.ErrClosedPipe,
 		},
 		{
-			name: "not found error",
+			name: "not found",
 			fields: fields{
 				eventstore: expectEventstore(
 					expectFilter(),
 				),
 			},
-			args:    args{"key1"},
-			wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"),
+			args: args{"key1"},
+			want: time.Time{},
+		},
+		{
+			name: "previously deleted",
+			fields: fields{
+				eventstore: expectEventstore(
+					expectFilter(
+						eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
+							webkey.NewAggregate("key1", "instance1"),
+							&crypto.CryptoValue{
+								CryptoType: crypto.TypeEncryption,
+								Algorithm:  "alg",
+								KeyID:      "encKey",
+								Crypted:    []byte("crypted"),
+							},
+							&jose.JSONWebKey{
+								Key:       &key.PublicKey,
+								KeyID:     "key1",
+								Algorithm: string(jose.ES384),
+								Use:       crypto.KeyUsageSigning.String(),
+							},
+							&crypto.WebKeyECDSAConfig{
+								Curve: crypto.EllipticCurveP384,
+							},
+						)),
+						eventFromEventPusher(webkey.NewActivatedEvent(ctx,
+							webkey.NewAggregate("key1", "instance1"),
+						)),
+						eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
+							webkey.NewAggregate("key2", "instance1"),
+							&crypto.CryptoValue{
+								CryptoType: crypto.TypeEncryption,
+								Algorithm:  "alg",
+								KeyID:      "encKey",
+								Crypted:    []byte("crypted"),
+							},
+							&jose.JSONWebKey{
+								Key:       &key.PublicKey,
+								KeyID:     "key2",
+								Algorithm: string(jose.ES384),
+								Use:       crypto.KeyUsageSigning.String(),
+							},
+							&crypto.WebKeyECDSAConfig{
+								Curve: crypto.EllipticCurveP384,
+							},
+						)),
+						eventFromEventPusher(webkey.NewActivatedEvent(ctx,
+							webkey.NewAggregate("key2", "instance1"),
+						)),
+						eventFromEventPusher(webkey.NewDeactivatedEvent(ctx,
+							webkey.NewAggregate("key1", "instance1"),
+						)),
+						eventFromEventPusher(webkey.NewRemovedEvent(ctx,
+							webkey.NewAggregate("key1", "instance1"),
+						)),
+					),
+				),
+			},
+			args: args{"key1"},
+			want: time.Time{},
 		},
 		{
 			name: "key active error",
@@ -722,10 +782,7 @@ func TestCommands_DeleteWebKey(t *testing.T) {
 				),
 			},
 			args: args{"key1"},
-			want: &domain.ObjectDetails{
-				ResourceOwner: "instance1",
-				ID:            "key1",
-			},
+			want: time.Time{},
 		},
 	}
 	for _, tt := range tests {
diff --git a/internal/integration/client.go b/internal/integration/client.go
index a480a86ce0a..4dca5fa1fa4 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -34,7 +34,6 @@ import (
 	action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
 	user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
 	userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
-	webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
 	saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
 	"github.com/zitadel/zitadel/pkg/grpc/session/v2"
 	session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
@@ -43,6 +42,7 @@ import (
 	user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
 	user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
 	user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
+	webkey_v2beta "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
 )
 
 type Client struct {
@@ -64,7 +64,7 @@ type Client struct {
 	FeatureV2beta  feature_v2beta.FeatureServiceClient
 	FeatureV2      feature.FeatureServiceClient
 	UserSchemaV3   userschema_v3alpha.ZITADELUserSchemasClient
-	WebKeyV3Alpha  webkey_v3alpha.ZITADELWebKeysClient
+	WebKeyV2Beta   webkey_v2beta.WebKeyServiceClient
 	IDPv2          idp_pb.IdentityProviderServiceClient
 	UserV3Alpha    user_v3alpha.ZITADELUsersClient
 	SAMLv2         saml_pb.SAMLServiceClient
@@ -97,7 +97,7 @@ func newClient(ctx context.Context, target string) (*Client, error) {
 		FeatureV2beta:  feature_v2beta.NewFeatureServiceClient(cc),
 		FeatureV2:      feature.NewFeatureServiceClient(cc),
 		UserSchemaV3:   userschema_v3alpha.NewZITADELUserSchemasClient(cc),
-		WebKeyV3Alpha:  webkey_v3alpha.NewZITADELWebKeysClient(cc),
+		WebKeyV2Beta:   webkey_v2beta.NewWebKeyServiceClient(cc),
 		IDPv2:          idp_pb.NewIdentityProviderServiceClient(cc),
 		UserV3Alpha:    user_v3alpha.NewZITADELUsersClient(cc),
 		SAMLv2:         saml_pb.NewSAMLServiceClient(cc),
diff --git a/proto/zitadel/resources/webkey/v3alpha/config.proto b/proto/zitadel/resources/webkey/v3alpha/config.proto
deleted file mode 100644
index 170334afa57..00000000000
--- a/proto/zitadel/resources/webkey/v3alpha/config.proto
+++ /dev/null
@@ -1,41 +0,0 @@
-syntax = "proto3";
-
-package zitadel.resources.webkey.v3alpha;
-
-import "validate/validate.proto";
-
-option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
-
-message WebKeyRSAConfig {
-  enum RSABits {
-    RSA_BITS_UNSPECIFIED = 0;
-    RSA_BITS_2048 = 1;
-    RSA_BITS_3072 = 2;
-    RSA_BITS_4096 = 3;
-  }
-
-  enum RSAHasher {
-    RSA_HASHER_UNSPECIFIED = 0;
-    RSA_HASHER_SHA256 = 1;
-    RSA_HASHER_SHA384 = 2;
-    RSA_HASHER_SHA512 = 3;
-  }
-
-  // bit size of the RSA key
-  RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
-  // signing algrithm used
-  RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
-}
-
-message WebKeyECDSAConfig {
-  enum ECDSACurve {
-    ECDSA_CURVE_UNSPECIFIED = 0;
-    ECDSA_CURVE_P256 = 1;
-    ECDSA_CURVE_P384 = 2;
-    ECDSA_CURVE_P512 = 3;
-  }
-
-  ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
-}
-
-message WebKeyED25519Config {}
diff --git a/proto/zitadel/resources/webkey/v3alpha/key.proto b/proto/zitadel/resources/webkey/v3alpha/key.proto
deleted file mode 100644
index 47486f7aee9..00000000000
--- a/proto/zitadel/resources/webkey/v3alpha/key.proto
+++ /dev/null
@@ -1,31 +0,0 @@
-syntax = "proto3";
-
-package zitadel.resources.webkey.v3alpha;
-
-import "google/protobuf/timestamp.proto";
-import "zitadel/resources/webkey/v3alpha/config.proto";
-import "zitadel/resources/object/v3alpha/object.proto";
-
-option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
-
-enum WebKeyState {
-  STATE_UNSPECIFIED = 0;
-  STATE_INITIAL = 1;
-  STATE_ACTIVE = 2;
-  STATE_INACTIVE = 3;
-  STATE_REMOVED = 4;
-}
-
-message GetWebKey {
-  zitadel.resources.object.v3alpha.Details details = 1;
-  WebKey config = 2;
-  WebKeyState state = 3;
-}
-
-message WebKey {
-  oneof config {
-    WebKeyRSAConfig rsa = 6;
-    WebKeyECDSAConfig ecdsa = 7;
-    WebKeyED25519Config ed25519 = 8;
-  }
-}
diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
deleted file mode 100644
index 43d2edab2ba..00000000000
--- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
+++ /dev/null
@@ -1,278 +0,0 @@
-syntax = "proto3";
-
-package zitadel.resources.webkey.v3alpha;
-
-import "google/api/annotations.proto";
-import "google/api/field_behavior.proto";
-import "protoc-gen-openapiv2/options/annotations.proto";
-import "validate/validate.proto";
-
-import "zitadel/protoc_gen_zitadel/v2/options.proto";
-
-import "zitadel/resources/webkey/v3alpha/key.proto";
-import "zitadel/resources/object/v3alpha/object.proto";
-import "zitadel/object/v3alpha/object.proto";
-
-option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
-
-option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
-  info: {
-    title: "Web key Service";
-    version: "3.0-preview";
-    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released.";
-    contact:{
-      name: "ZITADEL"
-      url: "https://zitadel.com"
-      email: "hi@zitadel.com"
-    }
-    license: {
-      name: "Apache 2.0",
-      url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
-    };
-  };
-  schemes: HTTPS;
-  schemes: HTTP;
-
-  consumes: "application/json";
-  produces: "application/json";
-
-  consumes: "application/grpc";
-  produces: "application/grpc";
-
-  consumes: "application/grpc-web+proto";
-  produces: "application/grpc-web+proto";
-
-  host: "$CUSTOM-DOMAIN";
-  base_path: "/";
-
-  external_docs: {
-    description: "Detailed information about ZITADEL",
-    url: "https://zitadel.com/docs"
-  }
-  security_definitions: {
-    security: {
-      key: "OAuth2";
-      value: {
-        type: TYPE_OAUTH2;
-        flow: FLOW_ACCESS_CODE;
-        authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
-        token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
-        scopes: {
-          scope: {
-            key: "openid";
-            value: "openid";
-          }
-          scope: {
-            key: "urn:zitadel:iam:org:project:id:zitadel:aud";
-            value: "urn:zitadel:iam:org:project:id:zitadel:aud";
-          }
-        }
-      }
-    }
-  }
-  security: {
-    security_requirement: {
-      key: "OAuth2";
-      value: {
-        scope: "openid";
-        scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
-      }
-    }
-  }
-  responses: {
-    key: "403";
-    value: {
-      description: "Returned when the user does not have permission to access the resource.";
-      schema: {
-        json_schema: {
-          ref: "#/definitions/rpcStatus";
-        }
-      }
-    }
-  }
-  responses: {
-    key: "404";
-    value: {
-      description: "Returned when the resource does not exist.";
-      schema: {
-        json_schema: {
-          ref: "#/definitions/rpcStatus";
-        }
-      }
-    }
-  }
-};
-
-service ZITADELWebKeys {
-  rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
-    option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
-    };
-
-    option (zitadel.protoc_gen_zitadel.v2.options) = {
-      auth_option: {
-        permission: "iam.web_key.write"
-      }
-      http_response: {
-        success_code: 201
-      }
-    };
-
-    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
-      responses: {
-        key: "200"
-        value: {
-          description: "OK";
-        }
-      };
-    };
-  }
-  
-  rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
-    option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
-    };
-
-    option (zitadel.protoc_gen_zitadel.v2.options) = {
-      auth_option: {
-        permission: "iam.web_key.write"
-      }
-      http_response: {
-        success_code: 200
-      }
-    };
-
-    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Activate a signing key for the instance";
-      description: "Switch the active signing web key. The previously active key will be deactivated. Note that the JWKs OIDC endpoint returns a cacheable response. Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), as the public key may not have been propagated to caches and clients yet."
-      responses: {
-        key: "200"
-        value: {
-          description: "OK";
-        }
-      };
-    };
-  }
-  
-  rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) {
-    option (google.api.http) = {
-      delete: "/resources/v3alpha/web_keys/{id}"
-    };
-
-    option (zitadel.protoc_gen_zitadel.v2.options) = {
-      auth_option: {
-        permission: "iam.web_key.delete"
-      }
-      http_response: {
-        success_code: 200
-      }
-    };
-
-    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Delete a web key pair for the instance";
-      description: "Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid."
-      responses: {
-        key: "200"
-        value: {
-          description: "OK";
-        }
-      };
-    };
-  }
-  
-  rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) {
-    option (google.api.http) = {
-      get: "/resources/v3alpha/web_keys"
-    };
-
-    option (zitadel.protoc_gen_zitadel.v2.options) = {
-      auth_option: {
-        permission: "iam.web_key.read"
-      }
-      http_response: {
-        success_code: 200
-      }
-    };
-
-    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "List web key details for the instance";
-      description: "List web key details for the instance"
-      responses: {
-        key: "200"
-        value: {
-          description: "OK";
-        }
-      };
-    };
-  }
-}
-
-message CreateWebKeyRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
-    }
-  ];
-  WebKey key = 2;
-}
-
-message CreateWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ActivateWebKeyRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
-    }
-  ];
-  string id = 2 [
-    (validate.rules).string = {min_len: 1, max_len: 200},
-    (google.api.field_behavior) = REQUIRED,
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      min_length: 1,
-      max_length: 200,
-      example: "\"69629026806489455\"";
-    }
-  ];
-}
-
-message ActivateWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message DeleteWebKeyRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
-    }
-  ];
-  string id = 2 [
-    (validate.rules).string = {min_len: 1, max_len: 200},
-    (google.api.field_behavior) = REQUIRED,
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      min_length: 1,
-      max_length: 200,
-      example: "\"69629026806489455\"";
-    }
-  ];
-}
-
-message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
-    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
-    }
-  ];
-}
-
-message ListWebKeysResponse {
-  repeated GetWebKey web_keys = 1;
-}
\ No newline at end of file
diff --git a/proto/zitadel/webkey/v2beta/key.proto b/proto/zitadel/webkey/v2beta/key.proto
new file mode 100644
index 00000000000..b2a8a380b24
--- /dev/null
+++ b/proto/zitadel/webkey/v2beta/key.proto
@@ -0,0 +1,109 @@
+syntax = "proto3";
+
+package zitadel.webkey.v2beta;
+
+import "google/protobuf/timestamp.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey";
+
+enum State {
+  STATE_UNSPECIFIED = 0;
+  // A newly created key is in the initial state and published to the public key endpoint.
+  STATE_INITIAL = 1;
+  // The active key is used to sign tokens. Only one key can be active at a time.
+  STATE_ACTIVE = 2;
+  // The inactive key is not used to sign tokens anymore, but still published to the public key endpoint.
+  STATE_INACTIVE = 3;
+  // The removed key is not used to sign tokens anymore and not published to the public key endpoint.
+  STATE_REMOVED = 4;
+}
+
+message WebKey {
+  // The unique identifier of the key.
+  string id = 1 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"69629012906488334\"";
+    }
+  ];
+  // The timestamp of the key creation.
+  google.protobuf.Timestamp creation_date = 2 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"2024-12-18T07:50:47.492Z\"";
+    }
+  ];
+  // The timestamp of the last change to the key (e.g. creation, activation, deactivation).
+  google.protobuf.Timestamp change_date = 3 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"2025-01-23T10:34:18.051Z\"";
+    }
+  ];
+  // State of the key
+  State state = 4;
+  // Configured type of the key (either RSA, ECDSA or ED25519)
+  oneof key {
+    RSA rsa = 5;
+    ECDSA ecdsa = 6;
+    ED25519 ed25519 = 7;
+  }
+}
+
+message RSA {
+  // Bit size of the RSA key. Default is 2048 bits.
+  RSABits bits = 1 [
+    (validate.rules).enum = {defined_only: true, not_in: [0]},
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      default: "RSA_BITS_2048";
+    }
+  ];
+  // Signing algrithm used. Default is SHA256.
+  RSAHasher hasher = 2 [
+    (validate.rules).enum = {defined_only: true, not_in: [0]},
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      default: "RSA_HASHER_SHA256";
+    }
+  ];
+}
+
+enum RSABits {
+  RSA_BITS_UNSPECIFIED = 0;
+  // 2048 bit RSA key
+  RSA_BITS_2048 = 1;
+  // 3072 bit RSA key
+  RSA_BITS_3072 = 2;
+  // 4096 bit RSA key
+  RSA_BITS_4096 = 3;
+}
+
+enum RSAHasher {
+  RSA_HASHER_UNSPECIFIED = 0;
+  // SHA256 hashing algorithm resulting in the RS256 algorithm header
+  RSA_HASHER_SHA256 = 1;
+  // SHA384 hashing algorithm resulting in the RS384 algorithm header
+  RSA_HASHER_SHA384 = 2;
+  // SHA512 hashing algorithm resulting in the RS512 algorithm header
+  RSA_HASHER_SHA512 = 3;
+}
+
+message ECDSA {
+  // Curve of the ECDSA key. Default is P-256.
+  ECDSACurve curve = 1 [
+    (validate.rules).enum = {defined_only: true, not_in: [0]},
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      default: "ECDSA_CURVE_P256";
+    }
+  ];
+}
+
+enum ECDSACurve {
+  ECDSA_CURVE_UNSPECIFIED = 0;
+  // NIST P-256 curve resulting in the ES256 algorithm header
+  ECDSA_CURVE_P256 = 1;
+  // NIST P-384 curve resulting in the ES384 algorithm header
+  ECDSA_CURVE_P384 = 2;
+  // NIST P-512 curve resulting in the ES512 algorithm header
+  ECDSA_CURVE_P512 = 3;
+}
+
+message ED25519 {}
diff --git a/proto/zitadel/webkey/v2beta/webkey_service.proto b/proto/zitadel/webkey/v2beta/webkey_service.proto
new file mode 100644
index 00000000000..f395ea8a8b6
--- /dev/null
+++ b/proto/zitadel/webkey/v2beta/webkey_service.proto
@@ -0,0 +1,359 @@
+syntax = "proto3";
+
+package zitadel.webkey.v2beta;
+
+import "google/api/annotations.proto";
+import "google/api/field_behavior.proto";
+import "google/protobuf/timestamp.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/webkey/v2beta/key.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey";
+
+option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
+  info: {
+    title: "Web key Service";
+    version: "2.0-beta";
+    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This service is in beta state. It can AND will continue breaking until a stable version is released.\n\nThe public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n\nPlease make sure to enable the `web_key` feature flag on your instance to use this service.";
+    contact:{
+      name: "ZITADEL"
+      url: "https://zitadel.com"
+      email: "hi@zitadel.com"
+    }
+    license: {
+      name: "Apache 2.0",
+      url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
+    };
+  };
+  schemes: HTTPS;
+  schemes: HTTP;
+
+  consumes: "application/json";
+  produces: "application/json";
+
+  consumes: "application/grpc";
+  produces: "application/grpc";
+
+  consumes: "application/grpc-web+proto";
+  produces: "application/grpc-web+proto";
+
+  host: "$CUSTOM-DOMAIN";
+  base_path: "/";
+
+  external_docs: {
+    description: "Detailed information about ZITADEL",
+    url: "https://zitadel.com/docs"
+  }
+  security_definitions: {
+    security: {
+      key: "OAuth2";
+      value: {
+        type: TYPE_OAUTH2;
+        flow: FLOW_ACCESS_CODE;
+        authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
+        token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
+        scopes: {
+          scope: {
+            key: "openid";
+            value: "openid";
+          }
+          scope: {
+            key: "urn:zitadel:iam:org:project:id:zitadel:aud";
+            value: "urn:zitadel:iam:org:project:id:zitadel:aud";
+          }
+        }
+      }
+    }
+  }
+  security: {
+    security_requirement: {
+      key: "OAuth2";
+      value: {
+        scope: "openid";
+        scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
+      }
+    }
+  }
+  responses: {
+    key: "403";
+    value: {
+      description: "Returned when the user does not have permission to access the resource.";
+      schema: {
+        json_schema: {
+          ref: "#/definitions/rpcStatus";
+        }
+      }
+    }
+  }
+  responses: {
+    key: "404";
+    value: {
+      description: "Returned when the resource does not exist.";
+      schema: {
+        json_schema: {
+          ref: "#/definitions/rpcStatus";
+        }
+      }
+    }
+  }
+};
+
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+//
+// Please make sure to enable the `web_key` feature flag on your instance to use this service.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
+  //
+  // Required feature flag:
+  //   - `web_key`
+  rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
+    option (google.api.http) = {
+      post: "/v2beta/web_keys"
+      body: "*"
+    };
+
+    option (zitadel.protoc_gen_zitadel.v2.options) = {
+      auth_option: {
+        permission: "iam.web_key.write"
+      }
+    };
+
+    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+      responses: {
+        key: "200"
+        value: {
+          description: "Web key created successfully.";
+        }
+      };
+      responses: {
+        key: "409"
+        value: {
+          description: "The feature flag `web_key` is not enabled.";
+        }
+      };
+    };
+  }
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
+  //
+  // Required feature flag:
+  //   - `web_key`
+  rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
+    option (google.api.http) = {
+      post: "/v2beta/web_keys/{id}/activate"
+    };
+
+    option (zitadel.protoc_gen_zitadel.v2.options) = {
+      auth_option: {
+        permission: "iam.web_key.write"
+      }
+    };
+
+    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+      responses: {
+        key: "200"
+        value: {
+          description: "Web key activated successfully.";
+        }
+      };
+      responses: {
+        key: "404"
+        value: {
+          description: "The web key to active does not exist.";
+        }
+      };
+      responses: {
+        key: "409"
+        value: {
+          description: "The feature flag `web_key` is not enabled.";
+        }
+      };
+    };
+  }
+
+  // Delete Web Key
+  //
+  // Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted,
+  // any tokens signed by this key will be invalid.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // In case the web key is not found, the request will return a successful response as
+  // the desired state is already achieved.
+  // You can check the change date in the response to verify if the web key was deleted during the request.
+  //
+  // Required permission:
+  //   - `iam.web_key.delete`
+  //
+  // Required feature flag:
+  //   - `web_key`
+  rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) {
+    option (google.api.http) = {
+      delete: "/v2beta/web_keys/{id}"
+    };
+
+    option (zitadel.protoc_gen_zitadel.v2.options) = {
+      auth_option: {
+        permission: "iam.web_key.delete"
+      }
+    };
+
+    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+      responses: {
+        key: "200"
+        value: {
+          description: "Web key deleted successfully.";
+        }
+      };
+      responses: {
+        key: "409"
+        value: {
+          description: "The feature flag `web_key` is not enabled or the web key is currently active.";
+        }
+      };
+    };
+  }
+
+  // List Web Keys
+  //
+  // List all web keys and their states.
+  //
+  // Required permission:
+  //   - `iam.web_key.read`
+  //
+  // Required feature flag:
+  //   - `web_key`
+  rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) {
+    option (google.api.http) = {
+      get: "/v2beta/web_keys"
+    };
+
+    option (zitadel.protoc_gen_zitadel.v2.options) = {
+      auth_option: {
+        permission: "iam.web_key.read"
+      }
+    };
+
+    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+      responses: {
+        key: "200"
+        value: {
+          description: "List of all web keys.";
+        }
+      };
+      responses: {
+        key: "409"
+        value: {
+          description: "The feature flag `web_key` is not enabled.";
+        }
+      };
+    };
+  }
+}
+
+message CreateWebKeyRequest {
+  // The key type to create (RSA, ECDSA, ED25519).
+  // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
+  oneof key {
+    // Create a RSA key pair and specify the bit size and hashing algorithm.
+    // If no bits and hasher are provided, a RSA key pair with 2048 bits and SHA256 hashing will be created.
+    RSA rsa = 1;
+    // Create a ECDSA key pair and specify the curve.
+    // If no curve is provided, a ECDSA key pair with P-256 curve will be created.
+    ECDSA ecdsa = 2;
+    // Create a ED25519 key pair.
+    ED25519 ed25519 = 3;
+  }
+  option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
+    example: "{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA5256\"}";
+  };
+}
+
+message CreateWebKeyResponse {
+  // The unique identifier of the newly created key.
+  string id = 1 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"69629012906488334\"";
+    }
+  ];
+  // The timestamp of the key creation.
+  google.protobuf.Timestamp creation_date = 2 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"2024-12-18T07:50:47.492Z\"";
+    }
+  ];
+}
+
+message ActivateWebKeyRequest {
+  string id = 1 [
+    (validate.rules).string = {min_len: 1, max_len: 200},
+    (google.api.field_behavior) = REQUIRED,
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      min_length: 1,
+      max_length: 200,
+      example: "\"69629026806489455\"";
+    }
+  ];
+}
+
+message ActivateWebKeyResponse {
+  // The timestamp of the activation of the key.
+  google.protobuf.Timestamp change_date = 3 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"2025-01-23T10:34:18.051Z\"";
+    }
+  ];
+}
+
+message DeleteWebKeyRequest {
+  string id = 1 [
+    (validate.rules).string = {min_len: 1, max_len: 200},
+    (google.api.field_behavior) = REQUIRED,
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      min_length: 1,
+      max_length: 200,
+      example: "\"69629026806489455\"";
+    }
+  ];
+}
+
+message DeleteWebKeyResponse {
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "\"2025-01-23T10:34:18.051Z\"";
+    }
+  ];
+}
+
+message ListWebKeysRequest {}
+
+message ListWebKeysResponse {
+  repeated WebKey web_keys = 1 [
+    (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+      example: "[{\"id\":\"69629012906488334\",\"creationDate\":\"2024-12-18T07:50:47.492Z\",\"changeDate\":\"2024-12-18T08:04:47.492Z\",\"state\":\"STATE_ACTIVE\",\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}},{\"id\":\"69629012909346200\",\"creationDate\":\"2025-01-18T12:05:47.492Z\",\"state\":\"STATE_INITIAL\",\"ecdsa\":{\"curve\":\"ECDSA_CURVE_P256\"}}]";
+    }
+  ];
+}
\ No newline at end of file


Reviews:
user: login: adlerhurst
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T10:01:45Z
user: login: adlerhurst
body: Looks good, only a few unclarities.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T10:30:57Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:21:10Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:40:50Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:41:27Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T17:08:05Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:35:23Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:35:38Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:36:45Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:40:13Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:40:39Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:43:21Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:43:28Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:43:32Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:45:49Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:51:22Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:51:48Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:57:08Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:17:07Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:22:22Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:26:32Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T10:22:07Z
user: login: stebenz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:22:26Z
user: login: livio-a
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T15:21:47Z
user: login: adlerhurst
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T15:30:27Z

Review comments:
diff_hunk: @@ -52,13 +53,17 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque
 	if err = checkWebKeyFeature(ctx); err != nil {
 		return nil, err
 	}
-	details, err := s.command.DeleteWebKey(ctx, req.GetId())
+	date, err := s.command.DeleteWebKey(ctx, req.GetId())
 	if err != nil {
 		return nil, err
 	}
 
+	var deletionDate *timestamppb.Timestamp
+	if !date.IsZero() {
+		deletionDate = timestamppb.New(date)
path: internal/api/grpc/webkey/v2beta/webkey.go
user: login: adlerhurst
body: 
```suggestion
	deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId())
	if err != nil {
		return nil, err
	}

	var deletionDate *timestamppb.Timestamp
	if !date.IsZero() {
		deletionDate = timestamppb.New(deletedAt)
```
created_at: 2025-03-04T10:01:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: adlerhurst
body: 
```suggestion
		out.Hasher = crypto.RSAHasherUnspecified
```
created_at: 2025-03-04T10:04:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: adlerhurst
body: I think we should either mention that we fallback or use unspecified
created_at: 2025-03-04T10:16:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA256:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA384:
+		out.Hasher = crypto.RSAHasherSHA384
+	case webkey.RSAHasher_RSA_HASHER_SHA512:
+		out.Hasher = crypto.RSAHasherSHA512
+	default:
+		out.Hasher = crypto.RSAHasherSHA256
+	}
+
+	return out
+}
+
+func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig {
+	out := new(crypto.WebKeyECDSAConfig)
+
+	switch config.GetCurve() {
+	case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED:
+		out.Curve = crypto.EllipticCurveP256
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: adlerhurst
body: I think we should either mention that we fallback or use unspecified
created_at: 2025-03-04T10:16:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: adlerhurst
body: I think we should either mention that we fallback or use unspecified
created_at: 2025-03-04T10:16:46Z
author_association: MEMBER
diff_hunk: @@ -1,25 +1,23 @@
 syntax = "proto3";
 
-package zitadel.resources.webkey.v3alpha;
+package zitadel.webkey.v2beta;
 
 import "google/api/annotations.proto";
 import "google/api/field_behavior.proto";
+import "google/protobuf/timestamp.proto";
 import "protoc-gen-openapiv2/options/annotations.proto";
 import "validate/validate.proto";
-
 import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/webkey/v2beta/key.proto";
+import "zitadel/object/v2beta/object.proto";
 
-import "zitadel/resources/webkey/v3alpha/key.proto";
-import "zitadel/resources/object/v3alpha/object.proto";
-import "zitadel/object/v3alpha/object.proto";
-
-option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey";
 
 option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   info: {
     title: "Web key Service";
-    version: "3.0-preview";
-    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released.";
+    version: "2.0-beta";
+    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in beta state. It can AND will continue breaking until a stable version is released.";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: adlerhurst
body: Not sure if project is correct here
```suggestion
    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This service is in beta state. It can AND will continue breaking until a stable version is released.";
```
created_at: 2025-03-04T10:25:47Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Isn't there also a documentation required for the possible errors?
created_at: 2025-03-05T09:45:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Would this not be an `AddWebKey` as you generate a new resource and can't update it?
created_at: 2025-03-05T09:51:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
+      post: "/v2beta/web_keys/{id}/activate"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we still check the permission in the API where possible? Or do we move it into the command and query side as described in https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md#authentication-and-authorization ?
created_at: 2025-03-05T09:58:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
+      post: "/v2beta/web_keys/{id}/activate"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 200
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we still need this openapiv2_operation only for the status code 200?
created_at: 2025-03-05T09:58:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we still need the examples?
created_at: 2025-03-05T10:00:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
     }
   ];
 }
 
+message ListWebKeysRequest {}
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we need options to sort and paginate?
created_at: 2025-03-05T16:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
     }
   ];
 }
 
+message ListWebKeysRequest {}
+
 message ListWebKeysResponse {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we need options to paginate?
created_at: 2025-03-05T16:41:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,25 +1,23 @@
 syntax = "proto3";
 
-package zitadel.resources.webkey.v3alpha;
+package zitadel.webkey.v2beta;
 
 import "google/api/annotations.proto";
 import "google/api/field_behavior.proto";
+import "google/protobuf/timestamp.proto";
 import "protoc-gen-openapiv2/options/annotations.proto";
 import "validate/validate.proto";
-
 import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/webkey/v2beta/key.proto";
+import "zitadel/object/v2beta/object.proto";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: Do we still need the object.proto import?
created_at: 2025-03-05T17:08:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,25 +1,23 @@
 syntax = "proto3";
 
-package zitadel.resources.webkey.v3alpha;
+package zitadel.webkey.v2beta;
 
 import "google/api/annotations.proto";
 import "google/api/field_behavior.proto";
+import "google/protobuf/timestamp.proto";
 import "protoc-gen-openapiv2/options/annotations.proto";
 import "validate/validate.proto";
-
 import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/webkey/v2beta/key.proto";
+import "zitadel/object/v2beta/object.proto";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: good catch. now cleaned up.
created_at: 2025-03-06T07:35:23Z
author_association: MEMBER
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
     }
   ];
 }
 
+message ListWebKeysRequest {}
+
 message ListWebKeysResponse {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: dupplicate
created_at: 2025-03-06T07:35:38Z
author_association: MEMBER
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
     }
   ];
 }
 
+message ListWebKeysRequest {}
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: as of now there will be only a very few keys. typically 2 to 3.
if needed we'll add this in the future
created_at: 2025-03-06T07:36:45Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: IMO add would be adding them to a list. In this case we create a new resource and even if we cannot change it's properties like we can on other resources, we still can manipulate the state.
Also to me it feels way more natural to create a key rather than to add one, but that's maybe just me.
created_at: 2025-03-06T07:40:12Z
author_association: MEMBER
diff_hunk: @@ -1,25 +1,23 @@
 syntax = "proto3";
 
-package zitadel.resources.webkey.v3alpha;
+package zitadel.webkey.v2beta;
 
 import "google/api/annotations.proto";
 import "google/api/field_behavior.proto";
+import "google/protobuf/timestamp.proto";
 import "protoc-gen-openapiv2/options/annotations.proto";
 import "validate/validate.proto";
-
 import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/webkey/v2beta/key.proto";
+import "zitadel/object/v2beta/object.proto";
 
-import "zitadel/resources/webkey/v3alpha/key.proto";
-import "zitadel/resources/object/v3alpha/object.proto";
-import "zitadel/object/v3alpha/object.proto";
-
-option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta;webkey";
 
 option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   info: {
     title: "Web key Service";
-    version: "3.0-preview";
-    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released.";
+    version: "2.0-beta";
+    description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in beta state. It can AND will continue breaking until a stable version is released.";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: also added some more documentation
created_at: 2025-03-06T07:40:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: livio-a
body: default was only documented for openapi... did now add it to the call itself and types

From what I see the idea behind that was to prevent unnecessary errors and as OIDC requires support RS256 as a default, it makes sense to default to in this API to that as well.
created_at: 2025-03-06T07:43:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_2048:
+		out.Bits = crypto.RSABits2048
+	case webkey.RSABits_RSA_BITS_3072:
+		out.Bits = crypto.RSABits3072
+	case webkey.RSABits_RSA_BITS_4096:
+		out.Bits = crypto.RSABits4096
+	default:
+		out.Bits = crypto.RSABits2048
+	}
+
+	switch config.GetHasher() {
+	case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA256:
+		out.Hasher = crypto.RSAHasherSHA256
+	case webkey.RSAHasher_RSA_HASHER_SHA384:
+		out.Hasher = crypto.RSAHasherSHA384
+	case webkey.RSAHasher_RSA_HASHER_SHA512:
+		out.Hasher = crypto.RSAHasherSHA512
+	default:
+		out.Hasher = crypto.RSAHasherSHA256
+	}
+
+	return out
+}
+
+func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig {
+	out := new(crypto.WebKeyECDSAConfig)
+
+	switch config.GetCurve() {
+	case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED:
+		out.Curve = crypto.EllipticCurveP256
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: livio-a
body: see above
created_at: 2025-03-06T07:43:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,170 @@
+package webkey
+
+import (
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	"github.com/zitadel/zitadel/internal/crypto"
+	"github.com/zitadel/zitadel/internal/domain"
+	"github.com/zitadel/zitadel/internal/query"
+	webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta"
+)
+
+func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
+	switch config := req.GetKey().(type) {
+	case *webkey.CreateWebKeyRequest_Rsa:
+		return rsaToCrypto(config.Rsa)
+	case *webkey.CreateWebKeyRequest_Ecdsa:
+		return ecdsaToCrypto(config.Ecdsa)
+	case *webkey.CreateWebKeyRequest_Ed25519:
+		return new(crypto.WebKeyED25519Config)
+	default:
+		return rsaToCrypto(nil)
+	}
+}
+
+func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig {
+	out := new(crypto.WebKeyRSAConfig)
+
+	switch config.GetBits() {
+	case webkey.RSABits_RSA_BITS_UNSPECIFIED:
+		out.Bits = crypto.RSABits2048
path: internal/api/grpc/webkey/v2beta/webkey_converter.go
user: login: livio-a
body: see above
created_at: 2025-03-06T07:43:31Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: as we do not yet have the error codes we'll have to document them once we have the proper concept. please also have a look at the note in the guidelines: https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md#error-codes
created_at: 2025-03-06T07:45:49Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
+      post: "/v2beta/web_keys/{id}/activate"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 200
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: Ok maybe it makes sense to at least document the HTTP error codes for now until we transition to connectRPC, which would solve the most urgent problem with not having proper error codes.
Will do that after my meetings
created_at: 2025-03-06T07:51:22Z
author_association: MEMBER
diff_hunk: @@ -262,17 +290,22 @@ message DeleteWebKeyRequest {
 }
 
 message DeleteWebKeyResponse {
-  zitadel.resources.object.v3alpha.Details details = 1;
-}
-
-message ListWebKeysRequest {
-  optional zitadel.object.v3alpha.Instance instance = 1 [
+  // The timestamp of the deletion of the key.
+  // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+  // In case the deletion occurred in a previous request, the deletion date might be empty.
+  google.protobuf.Timestamp deletion_date = 3 [
     (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
-      default: "\"domain from HOST or :authority header\""
+      example: "\"2025-01-23T10:34:18.051Z\"";
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: as our documentation currently still relies on openapi, yes.
created_at: 2025-03-06T07:51:48Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
+      post: "/v2beta/web_keys/{id}/activate"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: started a PR to make it more clear: https://github.com/zitadel/zitadel/pull/9478
will finished that after my meetings
created_at: 2025-03-06T07:57:08Z
author_association: MEMBER
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: I saw the note, what I meant is more that we could maybe remove these openapi responses:
https://github.com/zitadel/zitadel/pull/9445/files#diff-189e52dffe06176d8d66604e3c1fece077b3f1361149793744f8a20fe0811865R143
created_at: 2025-03-06T09:17:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: That's alright with me, will resolve this.
IMO you have a list of keys and add one to it, for that list you have the ListWebKeys and keys endpoint. The key is automatically part of the list, as you don't have anything else grouping it, as you would have an organization for a user.
created_at: 2025-03-06T09:22:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: stebenz
body: But will resolve this for now as you answered my question in another comment.
created_at: 2025-03-06T09:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,133 +101,163 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
   }
 };
 
-service ZITADELWebKeys {
+// Service to manage web keys for OIDC token signing and validation.
+// The service provides methods to create, activate, delete and list web keys.
+// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.
+service WebKeyService {
+  // Create Web Key
+  //
+  // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation.
+  // The public key can be used to validate OIDC tokens.
+  // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys"
-      body: "key"
+      post: "/v2beta/web_keys"
+      body: "*"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 201
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
-      summary: "Generate a web key pair for the instance";
-      description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
       responses: {
         key: "200"
         value: {
-          description: "OK";
+          description: "Web key created successfully.";
         }
       };
     };
   }
-  
+
+  // Activate Web Key
+  //
+  // Switch the active signing web key. The previously active key will be deactivated.
+  // Note that the JWKs OIDC endpoint returns a cacheable response.
+  // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min),
+  // as the public key may not have been propagated to caches and clients yet.
+  //
+  // Required permission:
+  //   - `iam.web_key.write`
   rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
     option (google.api.http) = {
-      post: "/resources/v3alpha/web_keys/{id}/_activate"
+      post: "/v2beta/web_keys/{id}/activate"
     };
 
     option (zitadel.protoc_gen_zitadel.v2.options) = {
       auth_option: {
         permission: "iam.web_key.write"
       }
-      http_response: {
-        success_code: 200
-      }
     };
 
     option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
path: proto/zitadel/webkey/v2beta/webkey_service.proto
user: login: livio-a
body: done
created_at: 2025-03-06T10:22:07Z
author_association: MEMBER
diff_hunk: @@ -332,9 +332,9 @@ module.exports = {
                 categoryLinkSource: "auto",
             },
           },
-          webkey_v3: {
-            specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json",
-            outputDir: "docs/apis/resources/webkey_service_v3",
+          webkey_v2: {
path: docs/docusaurus.config.js
user: login: stebenz
body: Do we leave this under API v3?
created_at: 2025-03-07T14:22:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -332,9 +332,9 @@ module.exports = {
                 categoryLinkSource: "auto",
             },
           },
-          webkey_v3: {
-            specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json",
-            outputDir: "docs/apis/resources/webkey_service_v3",
+          webkey_v2: {
path: docs/docusaurus.config.js
user: login: livio-a
body: this is just the docs generation part... the side nav is [here](https://github.com/zitadel/zitadel/pull/9445/files/74b2c368d3ece2960a8b126e9866387d71f72f37#diff-e8aa23ec31a264f87ba92ce3eef2133d44d0190ec08ac6d6eba4913d031294e9R734) and changed to v2
created_at: 2025-03-07T15:21:47Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-03T12:59:23Z
body: [vc]: #DlPG+/ZfgOAYaWAyBFo7c5vBtDFItSZR0QwcbXXVJ/o=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkb2NzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3ppdGFkZWwvZG9jcy84bUs2cWExeUY4U0dnWjlFS052TXl4Vk5Kc0RxIiwicHJldmlld1VybCI6ImRvY3MtZ2l0LXdlYmtleXMtdjIteml0YWRlbC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImRvY3MtZ2l0LXdlYmtleXMtdjIteml0YWRlbC52ZXJjZWwuYXBwIn0sInJvb3REaXJlY3RvcnkiOiJkb2NzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs** | ✅ Ready ([Inspect](https://vercel.com/zitadel/docs/8mK6qa1yF8SGgZ9EKNvMyxVNJsDq)) | [Visit Preview](https://docs-git-webkeys-v2-zitadel.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/docs-git-webkeys-v2-zitadel.vercel.app?via=pr-comment-feedback-link) | Mar 6, 2025 9:20am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T12:59:33Z
body: ### Thanks for your contribution @livio-a! 🎉

Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review:

  - [x] I am happy with the code
  - [x] Documentations and examples are up-to-date
  - [x] Logical behavior changes are tested automatically
  - [x] No debug or dead code
  - [x] My code has no repetitions
  - [x] The PR title adheres to the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/)
  - [x] The example texts in the PR description are replaced.
  - [x] If there are any open TODOs or follow-ups, they are described in issues and link to this PR
  - [x] If there are deviations from a user stories acceptance criteria or design, they are agreed upon with the PO and documented.
  
author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-03T13:28:31Z
body: ## [Codecov](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) Report
Attention: Patch coverage is `94.73684%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 62.95%. Comparing base [(`4e1868e`)](https://app.codecov.io/gh/zitadel/zitadel/commit/4e1868e9bbedccf192e42ac2635c7d9478b7cad7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) to head [(`74b2c36`)](https://app.codecov.io/gh/zitadel/zitadel/commit/74b2c368d3ece2960a8b126e9866387d71f72f37?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel).
> Report is 1 commits behind head on v3.x.

| [Files with missing lines](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) | Patch % | Lines |
|---|---|---|
| [...nternal/api/grpc/webkey/v2beta/webkey\_converter.go](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&filepath=internal%2Fapi%2Fgrpc%2Fwebkey%2Fv2beta%2Fwebkey_converter.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel#diff-aW50ZXJuYWwvYXBpL2dycGMvd2Via2V5L3YyYmV0YS93ZWJrZXlfY29udmVydGVyLmdv) | 95.31% | [6 Missing :warning: ](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) |
| [internal/command/web\_key.go](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&filepath=internal%2Fcommand%2Fweb_key.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel#diff-aW50ZXJuYWwvY29tbWFuZC93ZWJfa2V5Lmdv) | 87.50% | [1 Missing :warning: ](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) |
| [internal/integration/client.go](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&filepath=internal%2Fintegration%2Fclient.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel#diff-aW50ZXJuYWwvaW50ZWdyYXRpb24vY2xpZW50Lmdv) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             v3.x    #9445   +/-   ##
=======================================
  Coverage   62.94%   62.95%           
=======================================
  Files        1625     1625           
  Lines      152407   152410    +3     
=======================================
+ Hits        95936    95951   +15     
+ Misses      51728    51720    -8     
+ Partials     4743     4739    -4     
```

| [Flag](https://app.codecov.io/gh/zitadel/zitadel/pull/9445/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) | Coverage Δ | |
|---|---|---|
| [core-integration-tests-postgres](https://app.codecov.io/gh/zitadel/zitadel/pull/9445/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) | `37.88% <48.34%> (-0.01%)` | :arrow_down: |
| [core-unit-tests](https://app.codecov.io/gh/zitadel/zitadel/pull/9445/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel) | `46.32% <84.86%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zitadel/zitadel/pull/9445?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zitadel).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE