Repository: ory/kratos
Pull request #589 (2020-07-18 10:24:01)

fix: Network errors during pwnedpasswords.com check causing recursive loop
## Related issue

On network errors, the password validator ends up in a recursion loop until either the fault disappears - or more likely the stack overflows. Related to #316, @aeneasr .

## Proposed changes

Change the validator code so that when `ignoreNetworkErrors = true`, a failure during the _pwnedpasswords.com_ check returns no error, but the validation of the same password later would retry the online check.

Added test cases that previously crashed or timed out the test suite (because of the loop), but they pass now. I also took the liberty to add a few more tests for the online password leak check.

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [x] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md).
- [x] I have read the [security policy](../security/policy).
- [x] I confirm that this pull request does not address a security
      vulnerability. If this pull request addresses a security. vulnerability, I
      confirm that I got green light (please contact
      [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push
      the changes.
- [x] I have added tests that prove my fix is effective or that my feature
      works.
- [ ] I have added or changed [the documentation](docs/docs).

## Further comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->

I implemented a `FakeHTTPClient` to use for these tests and feel like it should really live somewhere else to potentially be reused, but I couldn't find a good home for it. The _internal/testhelpers_ package doesn't work as it causes an import cycle, any suggestions?

This is also the first Go test I've written, so happy with any feedback if something is not perfect 🙂 



Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e7b776826..315261ffb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -133,30 +133,34 @@ workflows:
       - test-e2e:
           name: test-e2e-sqlite
           flavor: sqlite
-          filters:
-            tags:
-              only: /.*/
-      - test-e2e:
-          name: test-e2e-memory
-          flavor: memory
+          requires:
+            - test
+            - nancy/test
+            - golangci/lint
           filters:
             tags:
               only: /.*/
       - test-e2e:
           name: test-e2e-postgres
           flavor: postgres
+          requires:
+            - test-e2e-sqlite
           filters:
             tags:
               only: /.*/
       - test-e2e:
           name: test-e2e-mysql
           flavor: mysql
+          requires:
+            - test-e2e-sqlite
           filters:
             tags:
               only: /.*/
       - test-e2e:
           name: test-e2e-cockroach
           flavor: cockroach
+          requires:
+            - test-e2e-sqlite
           filters:
             tags:
               only: /.*/
@@ -170,7 +174,6 @@ workflows:
           requires:
             - test
             - test-e2e-sqlite
-            - test-e2e-memory
             - test-e2e-postgres
             - test-e2e-mysql
             - test-e2e-cockroach
@@ -184,7 +187,6 @@ workflows:
           requires:
             - test
             - test-e2e-sqlite
-            - test-e2e-memory
             - test-e2e-postgres
             - test-e2e-mysql
             - test-e2e-cockroach
@@ -198,7 +200,6 @@ workflows:
           requires:
             - test
             - test-e2e-sqlite
-            - test-e2e-memory
             - test-e2e-postgres
             - test-e2e-mysql
             - test-e2e-cockroach
@@ -214,7 +215,6 @@ workflows:
           requires:
             - test
             - test-e2e-sqlite
-            - test-e2e-memory
             - test-e2e-postgres
             - test-e2e-mysql
             - test-e2e-cockroach
@@ -235,7 +235,6 @@ workflows:
             - goreleaser/test
             - test
             - test-e2e-sqlite
-            - test-e2e-memory
             - test-e2e-postgres
             - test-e2e-mysql
             - test-e2e-cockroach
diff --git a/.schema/api.swagger.json b/.schema/api.swagger.json
index db3ddfa85..7004d007c 100755
--- a/.schema/api.swagger.json
+++ b/.schema/api.swagger.json
@@ -74,7 +74,7 @@
     },
     "/identities": {
       "get": {
-        "description": "This endpoint returns a login request's context with, for example, error details and\nother information.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).",
+        "description": "Lists all identities. Does not support search at the moment.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).",
         "produces": [
           "application/json"
         ],
@@ -85,8 +85,29 @@
         "tags": [
           "admin"
         ],
-        "summary": "List all identities in the system",
+        "summary": "List Identities",
         "operationId": "listIdentities",
+        "parameters": [
+          {
+            "maximum": 500,
+            "minimum": 1,
+            "type": "integer",
+            "format": "int64",
+            "default": 100,
+            "description": "Items per Page\n\nThis is the number of items per page.",
+            "name": "per_page",
+            "in": "query"
+          },
+          {
+            "minimum": 0,
+            "type": "integer",
+            "format": "int64",
+            "default": 0,
+            "description": "Pagination Page",
+            "name": "page",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "description": "A list of identities.",
@@ -120,7 +141,7 @@
         "tags": [
           "admin"
         ],
-        "summary": "Create an identity",
+        "summary": "Create an Identity",
         "operationId": "createIdentity",
         "parameters": [
           {
@@ -170,7 +191,7 @@
         "tags": [
           "admin"
         ],
-        "summary": "Get an identity",
+        "summary": "Get an Identity",
         "operationId": "getIdentity",
         "parameters": [
           {
@@ -217,7 +238,7 @@
         "tags": [
           "admin"
         ],
-        "summary": "Update an identity",
+        "summary": "Update an Identity",
         "operationId": "updateIdentity",
         "parameters": [
           {
@@ -264,7 +285,7 @@
         }
       },
       "delete": {
-        "description": "This endpoint deletes an identity. This can not be undone.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).",
+        "description": "Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.\nThis endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is\nassumed that is has been deleted already.\n\nLearn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).",
         "produces": [
           "application/json"
         ],
@@ -275,7 +296,7 @@
         "tags": [
           "admin"
         ],
-        "summary": "Delete an identity",
+        "summary": "Delete an Identity",
         "operationId": "deleteIdentity",
         "parameters": [
           {
@@ -290,6 +311,68 @@
           "204": {
             "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201."
           },
+          "500": {
+            "description": "genericError",
+            "schema": {
+              "$ref": "#/definitions/genericError"
+            }
+          }
+        }
+      }
+    },
+    "/recovery/link": {
+      "post": {
+        "description": "This endpoint creates a recovery link which should be given to the user in order for them to recover\n(or activate) their account.",
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "schemes": [
+          "http",
+          "https"
+        ],
+        "tags": [
+          "admin"
+        ],
+        "summary": "Create a Recovery Link",
+        "operationId": "createRecoveryLink",
+        "parameters": [
+          {
+            "name": "Body",
+            "in": "body",
+            "schema": {
+              "type": "object",
+              "required": [
+                "identity_id"
+              ],
+              "properties": {
+                "expires_in": {
+                  "description": "Link Expires In\n\nThe recovery link will expire at that point in time. Defaults to the configuration value of\n`selfservice.flows.recovery.request_lifespan`.",
+                  "type": "string",
+                  "pattern": "^[0-9]+(ns|us|ms|s|m|h)$"
+                },
+                "identity_id": {
+                  "$ref": "#/definitions/UUID"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "createRecoveryLinkResponse",
+            "schema": {
+              "$ref": "#/definitions/createRecoveryLinkResponse"
+            }
+          },
+          "400": {
+            "description": "genericError",
+            "schema": {
+              "$ref": "#/definitions/genericError"
+            }
+          },
           "404": {
             "description": "genericError",
             "schema": {
@@ -439,7 +522,6 @@
       "post": {
         "description": "\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).",
         "consumes": [
-          "application/json",
           "application/x-www-form-urlencoded"
         ],
         "schemes": [
@@ -451,6 +533,28 @@
         ],
         "summary": "Complete the browser-based recovery flow using a recovery link",
         "operationId": "completeSelfServiceBrowserRecoveryLinkStrategyFlow",
+        "parameters": [
+          {
+            "description": "Email to Recover\n\nNeeds to be set when initiating the flow. If the email is a registered\nrecovery email, a recovery link will be sent. If the email is not known,\na email with details on what happened will be sent instead.",
+            "name": "email",
+            "in": "body",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "type": "string",
+            "description": "Recovery Token\n\nThe recovery token which completes the recovery request. If the token\nis invalid (e.g. expired) an error will be shown to the end-user.",
+            "name": "token",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "Recovery Request ID",
+            "name": "request",
+            "in": "query"
+          }
+        ],
         "responses": {
           "302": {
             "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201."
@@ -1338,6 +1442,23 @@
         }
       }
     },
+    "createRecoveryLinkResponse": {
+      "type": "object",
+      "required": [
+        "recovery_link"
+      ],
+      "properties": {
+        "expires_at": {
+          "description": "Recovery Link Expires At\n\nThe timestamp when the recovery link expires.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "recovery_link": {
+          "description": "Recovery Link\n\nThis link can be used to recover the account.",
+          "type": "string"
+        }
+      }
+    },
     "errorContainer": {
       "type": "object",
       "properties": {
diff --git a/.schema/config.schema.json b/.schema/config.schema.json
index 72f70863c..5964dc428 100644
--- a/.schema/config.schema.json
+++ b/.schema/config.schema.json
@@ -855,10 +855,9 @@
       "properties": {
         "lifespan": {
           "title": "Session Lifespan",
-          "description": "Defines how long a session is active. This value is ignored if the \"remember me\" feature is used.",
+          "description": "Defines how long a session is active. This value is ignored if the \"remember me\" feature is used. If unset (default), the cookie's `Max-Age` will not be set.",
           "type": "string",
           "pattern": "^[0-9]+(ns|us|ms|s|m|h)$",
-          "default": "1h",
           "examples": [
             "1h",
             "1m",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56209dd45..6c1669b3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,11 +4,12 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**
 
-- [Unreleased (2020-07-17)](#unreleased-2020-07-17)
+- [Unreleased (2020-07-27)](#unreleased-2020-07-27)
     - [Bug Fixes](#bug-fixes)
     - [Code Refactoring](#code-refactoring)
     - [Documentation](#documentation)
     - [Features](#features)
+    - [Tests](#tests)
     - [BREAKING CHANGES](#breaking-changes)
   - [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13)
     - [Bug Fixes](#bug-fixes-1)
@@ -100,39 +101,65 @@
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
-# [Unreleased](https://github.com/ory/kratos/compare/v0.4.6-alpha.1...feef8a7d4454c1b343c34a96fa4dadd56149b0cd) (2020-07-17)
+# [Unreleased](https://github.com/ory/kratos/compare/v0.4.6-alpha.1...52bd839ea9fe8de1aac4663b9dc0a88ae18a5765) (2020-07-27)
 
 
 ### Bug Fixes
 
 * Don't resend verification emails once verified ([#583](https://github.com/ory/kratos/issues/583)) ([a4d9969](https://github.com/ory/kratos/commit/a4d99694525e65b58d49197c96324b27fb8c31c2)), closes [#578](https://github.com/ory/kratos/issues/578)
+* Fallback to default return url if logout after url is not defined ([#594](https://github.com/ory/kratos/issues/594)) ([7edd367](https://github.com/ory/kratos/commit/7edd367dc64a01dbe252ca0ab8cf4d3926a35014))
+* Improve pagination when listing identities ([c60bf44](https://github.com/ory/kratos/commit/c60bf440b9c85b4f2e871237e3d7725571151efe))
 * Resolve cookie issues ([6e2b6d2](https://github.com/ory/kratos/commit/6e2b6d2f0ce2fb6df7d3e26d6cc8e755e6593a81))
 * Resolve flaky passwort setting tests ([#582](https://github.com/ory/kratos/issues/582)) ([c42d936](https://github.com/ory/kratos/commit/c42d936ef51d2ffb48b491b99988d048442e3b8b)), closes [#581](https://github.com/ory/kratos/issues/581) [#577](https://github.com/ory/kratos/issues/577)
 * Resolve identity admin api issues ([#586](https://github.com/ory/kratos/issues/586)) ([feef8a7](https://github.com/ory/kratos/commit/feef8a7d4454c1b343c34a96fa4dadd56149b0cd)), closes [#435](https://github.com/ory/kratos/issues/435) [#500](https://github.com/ory/kratos/issues/500):
 
     > This patch resolves several issues that occurred when creating or updating identities using the Admin API. Now, all hooks are running properly and updating privileged properties no longer causes errors.
+* Resolve interface type issues ([064b305](https://github.com/ory/kratos/commit/064b305ab31dc003ccb5992eb1ed2804f85085b9))
 * Resolve panic when DSN="memory" ([#574](https://github.com/ory/kratos/issues/574)) ([05e55f3](https://github.com/ory/kratos/commit/05e55f3584e20ae5d39cfda6e542d4da40d718e4)):
 
     > Executing the migration logic in registry.go cause a panic as the registry is not initalized at that point. Therefore we decided to move the handling to driver_default.go, after the registry has been initialized.
+* Use params per_page and page for pagination ([5dfb6e3](https://github.com/ory/kratos/commit/5dfb6e32c44420ed49d652733b9099a41c9347f2))
 * Use relative threshold to judge longest common substring in password policy ([#585](https://github.com/ory/kratos/issues/585)) ([3e9f8cc](https://github.com/ory/kratos/commit/3e9f8cce4b058b05d69c73fff514f3b8e46c2be3)), closes [#581](https://github.com/ory/kratos/issues/581)
 
 
 ### Code Refactoring
 
 * Move samesite config to cookie parent-key ([753eb86](https://github.com/ory/kratos/commit/753eb86c904c4af9e7d91e46ff4c836dcce35807))
+* Rename verify to verificaiton ([#597](https://github.com/ory/kratos/issues/597)) ([0ecd69a](https://github.com/ory/kratos/commit/0ecd69a60f741fc334c9b060b6aeaafc39e048b1))
 
 
 ### Documentation
 
+* Add administrative user management guide ([b97e0c6](https://github.com/ory/kratos/commit/b97e0c69bb1115bdec88b218e8cdda34f137d798))
 * Add documentation for configuring cookies ([e3dbc8a](https://github.com/ory/kratos/commit/e3dbc8acc055f6e2d78bc959be7356f9a66ac90f)), closes [#516](https://github.com/ory/kratos/issues/516)
+* Add guide for dealing with login sessions ([4e2718c](https://github.com/ory/kratos/commit/4e2718c779031c0e3b877e9df1747ccb2371927b))
+* Add identity state ([fb4aedb](https://github.com/ory/kratos/commit/fb4aedb9a95367e25080491b54aab11de491d819))
+* Add login session to navbar ([b212d64](https://github.com/ory/kratos/commit/b212d6484e40c9f2cce10f2ba4aaf4e2a72f03a1))
+* Add pagination docs ([7fe0901](https://github.com/ory/kratos/commit/7fe0901ee5d0e829e110bd0c4fdecb24bfc27768))
 * Add secret key rotation guide ([3d6e21a](https://github.com/ory/kratos/commit/3d6e21af2f726944468299c326600a8ab0e4e885))
+* Add terminology section ([29b81a7](https://github.com/ory/kratos/commit/29b81a78fcf880cd6d9d3b2cbb03f955b701ffbd))
 * Docker image documentation ([#573](https://github.com/ory/kratos/issues/573)) ([bfe032e](https://github.com/ory/kratos/commit/bfe032e2b6bfd8b9415d466011bdd7e36efa4146))
+* Fix logout url ([#593](https://github.com/ory/kratos/issues/593)) ([f0971d4](https://github.com/ory/kratos/commit/f0971d44a911caed8a6071358fa6b7ebc0fcf145))
+* Rename self service and add admin section ([639c424](https://github.com/ory/kratos/commit/639c424d3bde0557f7edd7edc489a476f1aa60b3))
+* Use correct path in 0.4 docs ([9fcaac4](https://github.com/ory/kratos/commit/9fcaac4048e05500d0456eb3cd9cd11cc123e370)), closes [#588](https://github.com/ory/kratos/issues/588)
 
 
 ### Features
 
 * Add ability to configure session cookie domain/path ([faeb332](https://github.com/ory/kratos/commit/faeb3328dab343c6ef3974065ba0c5c590a8817e)), closes [#516](https://github.com/ory/kratos/issues/516)
+* Add nocache helpers ([54dcc4d](https://github.com/ory/kratos/commit/54dcc4da2ff22bdb17e53dd6eac1c0bd54a20390))
+* Add pagination tests ([e3aa81b](https://github.com/ory/kratos/commit/e3aa81b7da55108f43ea6e16c817c97e2f8a1d50))
 * Add tests and helpers to test recovery/verifiable addresses ([#579](https://github.com/ory/kratos/issues/579)) ([29979e6](https://github.com/ory/kratos/commit/29979e6c4934b71c7fb158cfa5b85e97be3ea8fc)), closes [#576](https://github.com/ory/kratos/issues/576)
+* Implement administrative account recovery ([f5f9c43](https://github.com/ory/kratos/commit/f5f9c43e10dd3a9547e87776164d2d4a171f35ce))
+* Implement max-age for session cookie ([2e642ff](https://github.com/ory/kratos/commit/2e642ff13c59a7e23babe9209c1a114ef0163bad)), closes [#326](https://github.com/ory/kratos/issues/326)
+* Reply with cache-control: 0 for browser-facing APIs ([1a45b53](https://github.com/ory/kratos/commit/1a45b5341e0ab4580208bfb6a505859d1e5d2faf)), closes [#360](https://github.com/ory/kratos/issues/360)
+
+
+### Tests
+
+* Add handler update tests ([aea1fb8](https://github.com/ory/kratos/commit/aea1fb807a16acd8406b94a72c3b39be8c3e1280)), closes [#325](https://github.com/ory/kratos/issues/325)
+* Add test for no-cache on public router ([b8aa63b](https://github.com/ory/kratos/commit/b8aa63b7ebd269a87578e8a5c6b2df27e18f9efa))
+* Replace e2e-memory with unit test ([52bd839](https://github.com/ory/kratos/commit/52bd839ea9fe8de1aac4663b9dc0a88ae18a5765)), closes [#580](https://github.com/ory/kratos/issues/580)
 
 
 ### BREAKING CHANGES
diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go
index 5a3997f23..2b493f0ca 100644
--- a/cmd/daemon/serve.go
+++ b/cmd/daemon/serve.go
@@ -24,7 +24,7 @@ import (
 	"github.com/ory/kratos/selfservice/flow/logout"
 	"github.com/ory/kratos/selfservice/flow/registration"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/selfservice/strategy/oidc"
 	"github.com/ory/kratos/selfservice/strategy/password"
 	"github.com/ory/kratos/selfservice/strategy/profile"
@@ -131,10 +131,10 @@ func sqa(cmd *cobra.Command, d driver.Driver) *metricsx.Service {
 				settings.PublicPath,
 				settings.PublicRequestPath,
 				profile.PublicSettingsProfilePath,
-				verify.PublicVerificationCompletePath,
-				strings.ReplaceAll(strings.ReplaceAll(verify.PublicVerificationConfirmPath, ":via", "email"), ":code", ""),
-				strings.ReplaceAll(verify.PublicVerificationInitPath, ":via", "email"),
-				verify.PublicVerificationRequestPath,
+				verification.PublicVerificationCompletePath,
+				strings.ReplaceAll(strings.ReplaceAll(verification.PublicVerificationConfirmPath, ":via", "email"), ":code", ""),
+				strings.ReplaceAll(verification.PublicVerificationInitPath, ":via", "email"),
+				verification.PublicVerificationRequestPath,
 				errorx.ErrorsPath,
 			},
 			BuildVersion: d.Registry().BuildVersion(),
diff --git a/docs/docs/admin/managing-users-identities.mdx b/docs/docs/admin/managing-users-identities.mdx
new file mode 100644
index 000000000..96a71dd8a
--- /dev/null
+++ b/docs/docs/admin/managing-users-identities.mdx
@@ -0,0 +1,142 @@
+---
+id: managing-users-identities
+title: Managing Users and Identities
+---
+
+import Tabs from '@theme/Tabs'
+import TabItem from '@theme/TabItem'
+
+This document walks you through the administrative identity management in ORY Kratos. You should already
+be familiar with the [Identity Data Model](../concepts/identity-data-model) before reading this guide.
+
+## Creating an Identity
+
+There are three principal flows supported for creating identities as an administrator:
+
+1. Inviting users - e.g. inviting a new employee to your organization IT.
+2. Importing existing users - e.g. when migrating from another system to ORY Kratos.
+3. Creating machine users - e.g. creating Service Accounts.
+
+:::note
+
+Similar to other guides, we assume that ORY Kratos runs on 127.0.0.1:4433 (public endpoint) and
+127.0.0.1:4434 (admin endpoint) in this guide, which is the default when running the quickstart.
+
+:::
+
+### Invite a User
+
+The goal of this flow is to create an identity and provide the end-user with a way
+of signing into the identity (account) and setting their password (or any other type of credential)
+for future logins. To achieve this, first create the identity and set its traits and schema:
+
+```shell script
+$ curl --request POST -sL \
+    --header "Content-Type: application/json" \
+    --request POST \
+    --data '{
+  "schema_id": "default",
+  "traits": {
+    "email": "foo@ory.sh"
+  }
+}' \
+    http://127.0.0.1:4434/identities
+
+{
+  "id": "954f7f59-16a5-4152-8ce7-ad7c73bb124a",
+  "schema_id": "default",
+  "traits":{
+    "email": "foo@ory.sh"
+  }
+}
+```
+
+Keep in mind that you can change the `schema_id` to reflect the schema you want to use for this user. Similarly,
+the trait key/values depend on your schema as well. The command shown does not create a password for the identity
+or any other type of credential. Instead, we will use another REST call to create a recovery link (here "invite link" is
+probably more appropriate, but the flow uses an account recovery link).
+
+To create the account recovery link, use:
+
+<Tabs
+  defaultValue="curl"
+  values={[
+    {label: 'curl', value: 'curl'},
+    {label: 'GoLang', value: 'go'},
+  ]}>
+<TabItem value="curl">
+
+```shell script
+$ curl --request POST -sL \
+    --header "Content-Type: application/json" \
+    --request POST \
+    --data '{
+  "expires_in": "12h",
+  "identity_id": "954f7f59-16a5-4152-8ce7-ad7c73bb124a"
+}' \
+    http://127.0.0.1:4434/flows/recovery/link
+
+{
+  "expires_at": "2020-07-27T10:47:45.806Z",
+  "recovery_link": "http://127.0.0.1:4433/self-service/browser/flows/recovery/link?request=8b6fd3e4-1de2-49bf-aa88-1a26634bf062\u0026token=b1tGmHf64cYDeHB9wKiuCF1FfycMJEyf"
+}
+```
+
+</TabItem>
+<TabItem value="go">
+
+```go
+package main
+
+import (
+	"fmt"
+	"github.com/ory/kratos-client-go/client"
+	"github.com/ory/kratos-client-go/client/admin"
+	"github.com/ory/kratos-client-go/models"
+)
+
+func main() {
+	c := client.New(nil, &client.TransportConfig{
+        Host: "127.0.0.1:4434",
+        BasePath: "/",
+        Schemes: []string{"http"},
+    })
+
+	res, err := c.Admin.CreateRecoveryLink(admin.NewCreateRecoveryLinkParams().WithBody(admin.CreateRecoveryLinkBody{
+		IdentityID: models.UUID("the-uuid"),
+	}))
+	if err != nil {
+		// ...
+	}
+
+	fmt.Printf("Use link: %s", *res.Payload.RecoveryLink)
+}
+```
+
+The response contains a `recovery_link` value which is the link the user should use to set up his or her credentials
+(e.g. connect to a Social Sign In Provider, set up a password, ...). The user has only a limited amount of time to do
+so - the amount of time is specified in the ORY Kratos config:
+
+```yaml title="path/to/kratos/config.yml"
+selfservice:
+  flows:
+    settings:
+      privileged_session_max_age: 30m
+```
+
+If the user fails to set up his / her credentials in time, another recovery link needs to be issued and the user
+needs to re-do the flow.
+
+It is currently not possible to send the recovery link directly to a user's email, this feature is tracked as
+[#595](https://github.com/ory/kratos/issues/595).
+
+</TabItem>
+</Tabs>
+
+### Import a User Identity
+
+This feature is not implemented yet.
+
+### Creating a Machine Identity
+
+This feature is not implemented yet.
diff --git a/docs/docs/concepts/identity-data-model.md b/docs/docs/concepts/identity-data-model.md
index 3d9882be3..617020115 100644
--- a/docs/docs/concepts/identity-data-model.md
+++ b/docs/docs/concepts/identity-data-model.md
@@ -8,8 +8,14 @@ software system. It can be a customer, employee, user, contractor, or even a
 programmatic identity such as an IoT device, application, or some other type of
 "robot."
 
+:::info
+
 In ORY Kratos' terminology we call all of them "identities", and it is always
 exposed as `identity` in the API endpoints, requests, and response payloads.
+In the documentation however, we mix these words as "account recovery" or "account activation"
+is a widely accepted and understood terminology and user flow,
+while "identity recovery" or "identity activation" is not.
+:::
 
 The following examples use YAML for improved readability. However, the API
 payload is usually in JSON format. An `identity` has the following properties:
@@ -58,6 +64,28 @@ traits:
   accepted_tos: true
 ```
 
+## Identity State
+
+Identities are
+
+- `created` - via API or self-service registration);
+- `updated` - via API or self-serfice settings, account recovery, ...;
+- `disabled` - not yet implemented, see [#598](https://github.com/ory/kratos/issues/598);
+- `deleted` - via API or with a self-service flow (not yet implemented see [#596](https://github.com/ory/kratos/issues/596)).
+
+The identity state is therefore `active` or `disabled` (not yet implemented see [#598](https://github.com/ory/kratos/issues/598):
+
+<Mermaid
+  chart={`
+stateDiagram-v2
+	[*] --> Active: create
+    Active --> Active: update
+	Active --> Disabled: disable
+	Disabled --> [*]: delete
+    Disabled --> Active: enable
+`}
+/>
+
 ## Identity Traits and JSON Schemas
 
 Traits are data associated with an identity. You have to define its schema
diff --git a/docs/docs/concepts/rest-api.mdx b/docs/docs/concepts/rest-api.mdx
new file mode 100644
index 000000000..2d64dd650
--- /dev/null
+++ b/docs/docs/concepts/rest-api.mdx
@@ -0,0 +1,54 @@
+---
+id: rest-api
+title: REST API Design
+---
+
+This chapter contains generally applicable information on API design. When using
+Ory services, one can expect a consistent experience when interacting with REST
+APIs.
+
+## Pagination
+
+On REST endpoints that are explicitly labeled as such, pagination information is
+available through the `Link` HTTP header.
+
+The `Link` header contains a comma-delimited list of links to the following
+pages (where applicable):
+
+- First
+- Next
+- Previous (prev)
+- Last
+
+Pages are created based on the values of `per_page` and `page` provided in the
+querystring, where `per_page` is the page size, and `page` is the current page.
+
+In most scenarios, the `per_page` should be a multiple of the `page`.
+
+Example:
+
+```
+> GET /resources?per_page=5&page=10 HTTP/1.1
+> Host: localhost:4445
+> User-Agent: curl/7.64.1
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< Content-Type: application/json
+< Link: </resources?per_page=5&page=0>; rel="first",</resources?per_page=5&page=15>; rel="next",</resources?per_page=5&page=5>; rel="prev",</resources?per_page=5&page=20>; rel="last"
+< Date: Mon, 22 Apr 2019 23:34:29 GMT
+< Transfer-Encoding: chunked
+<
+[...]
+```
+
+## Date Format
+
+ORY's APIs use [rfc3339](https://tools.ietf.org/html/rfc3339) as the date
+format:
+
+```
+{
+  "created_at": "2006-01-02T15:04:05Z07:00"
+}
+```
diff --git a/docs/docs/concepts/terminology.mdx b/docs/docs/concepts/terminology.mdx
new file mode 100644
index 000000000..4b2a60155
--- /dev/null
+++ b/docs/docs/concepts/terminology.mdx
@@ -0,0 +1,26 @@
+---
+id: terminology
+title: Terminology
+---
+
+The purpose of this document is to define terminology used through this documentation.
+
+## End-User
+
+The person who actually uses a particular product that uses ORY Kratos.
+
+## Identity
+
+the "who" of a software system. It can be a customer, employee, user, contractor, or even a
+programmatic identity such as an IoT device, application, or some other type of
+"robot."
+
+Common aliases are "user", "user account", "account", "subject".
+
+## Self-Service
+
+Interfaces and flows allowing customers to produce services
+independent of involvement of direct service employee.
+
+You perform self-service registration when signing up for a new Goole account, because no one (e.g. support)
+else is involved in the process.
diff --git a/docs/docs/guides/login-session.mdx b/docs/docs/guides/login-session.mdx
new file mode 100644
index 000000000..dbef2442b
--- /dev/null
+++ b/docs/docs/guides/login-session.mdx
@@ -0,0 +1,23 @@
+---
+id: login-session
+title: Login Sessions
+---
+
+A login session is created when a user signs in. The session is either stored as a cookie or as a token, depending
+on the interaction type.
+
+You can set the cookie's `max-age` value - which effectively sets how long the session is active - by changing
+the ORY Kratos configuration file:
+
+```yaml title="path/to/kratos/config.yml
+session:
+  lifespan: 720h # 30 days
+```
+
+Once the lifespan is reached, the user needs to sign in again. If `lifespan` is not set, then the cookie's `max-age`
+will also not be set. Please be aware of how `max-age` behaves:
+
+- The browser interprets the cookie to be removed when the session ends (e.g. the browser window is closed) if
+`max-age` is not set as part of the `Set-Cookie` header. Please be aware that this behavior is not consistent across
+browsers.
+- The browser keeps the cookie until `max-age` is reached otherwise.
diff --git a/docs/docs/self-service/flows/user-logout.md b/docs/docs/self-service/flows/user-logout.md
index cdcd6dd09..66a040b65 100644
--- a/docs/docs/self-service/flows/user-logout.md
+++ b/docs/docs/self-service/flows/user-logout.md
@@ -18,7 +18,7 @@ ORY Kratos supports two logout flows:
 > as [kratos#142](https://github.com/ory/kratos/issues/142).
 
 To log a user out, all you have to do is to direct the browser to
-`http://ory-kratos-public/auth/browser/logout`. After successful logout, the
+`http://ory-kratos-public/self-service/browser/flows/logout`. After successful logout, the
 browser will be redirected to the `redirect_to` value set in ORY Krato's
 configuration file:
 
diff --git a/docs/sidebar.js b/docs/sidebar.js
index 145631bce..db6e54616 100644
--- a/docs/sidebar.js
+++ b/docs/sidebar.js
@@ -4,6 +4,7 @@ module.exports = {
   ],
   Concepts: [
     "concepts/index",
+    "concepts/terminology",
     "concepts/ui-user-interface",
     "concepts/identity-user-model",
     {
@@ -17,10 +18,11 @@ module.exports = {
     },
     "concepts/browser-redirect-flow-completion",
     "concepts/email-sms",
+    "concepts/rest-api",
     "concepts/federation",
     "concepts/security"
   ],
-  "Self Service": [
+  "Self Service (End-User)": [
     "self-service",
     {
       type: "category",
@@ -58,6 +60,9 @@ module.exports = {
     "self-service/flows/2fa-mfa-multi-factor-authentication",
     "self-service/hooks/index",
   ],
+  "Administration": [
+    "admin/managing-users-identities",
+  ],
   Guides: [
     "guides/sign-in-with-github-google-facebook-linkedin",
     "guides/zero-trust-iap-proxy-identity-access-proxy",
@@ -65,6 +70,7 @@ module.exports = {
     "guides/account-activation-email-verification",
     "guides/multi-tenancy-multitenant",
     "guides/configuring-cookies",
+    "guides/login-session",
     "guides/secret-key-rotation",
     "guides/high-availability-ha",
     "guides/docker"
diff --git a/docs/versioned_docs/version-v0.4/concepts/credentials/openid-connect-oidc-oauth2.mdx b/docs/versioned_docs/version-v0.4/concepts/credentials/openid-connect-oidc-oauth2.mdx
index 58e84525b..b13757daa 100644
--- a/docs/versioned_docs/version-v0.4/concepts/credentials/openid-connect-oidc-oauth2.mdx
+++ b/docs/versioned_docs/version-v0.4/concepts/credentials/openid-connect-oidc-oauth2.mdx
@@ -12,7 +12,7 @@ provider (for example [ORY Hydra](https://www.ory.sh/hydra)). "Social Sign In"
 or "Sign in with ..." are common aliases for this flow.
 
 This strategy expects that you've set up your
-[Default Identity JSON Schema](../identity-user-model).
+[Default Identity JSON Schema](../identity-data-model).
 
 ## Configuration
 
diff --git a/driver/configuration/provider.go b/driver/configuration/provider.go
index 73c142a40..bd54419c1 100644
--- a/driver/configuration/provider.go
+++ b/driver/configuration/provider.go
@@ -64,7 +64,7 @@ type Provider interface {
 
 	SecretsDefault() [][]byte
 	SecretsSession() [][]byte
-	SessionLifespan() time.Duration
+	SessionLifespan() *time.Duration
 	SessionSameSiteMode() http.SameSite
 	SessionDomain() string
 	SessionPath() string
diff --git a/driver/configuration/provider_viper.go b/driver/configuration/provider_viper.go
index ff7044c3b..175a40713 100644
--- a/driver/configuration/provider_viper.go
+++ b/driver/configuration/provider_viper.go
@@ -379,8 +379,14 @@ func (p *ViperProvider) SelfServiceFlowRecoveryUI() *url.URL {
 	return mustParseURLFromViper(p.l, ViperKeySelfServiceRecoveryUI)
 }
 
-func (p *ViperProvider) SessionLifespan() time.Duration {
-	return viperx.GetDuration(p.l, ViperKeySessionLifespan, time.Hour)
+// SessionLifespan returns nil when the value is not set.
+func (p *ViperProvider) SessionLifespan() *time.Duration {
+	if viper.Get(ViperKeySessionLifespan) == nil {
+		return nil
+	}
+
+	d := viper.GetDuration(ViperKeySessionLifespan)
+	return &d
 }
 
 func (p *ViperProvider) SelfServiceBrowserWhitelistedReturnToDomains() (us []url.URL) {
@@ -407,7 +413,12 @@ func (p *ViperProvider) SelfServiceFlowRegistrationRequestLifespan() time.Durati
 }
 
 func (p *ViperProvider) SelfServiceFlowLogoutRedirectURL() *url.URL {
-	return mustParseURLFromViper(p.l, ViperKeySelfServiceLogoutBrowserDefaultReturnTo)
+	redir, err := url.ParseRequestURI(
+		viperx.GetString(p.l, ViperKeySelfServiceLogoutBrowserDefaultReturnTo, ""))
+	if err != nil {
+		return p.SelfServiceBrowserDefaultReturnTo()
+	}
+	return redir
 }
 
 func (p *ViperProvider) CourierSMTPFrom() string {
diff --git a/driver/registry.go b/driver/registry.go
index 785c21528..d132b477f 100644
--- a/driver/registry.go
+++ b/driver/registry.go
@@ -12,7 +12,7 @@ import (
 	"github.com/ory/kratos/schema"
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 
 	"github.com/ory/x/healthx"
 
@@ -108,10 +108,10 @@ type Registry interface {
 	registration.HandlerProvider
 	registration.StrategyProvider
 
-	verify.PersistenceProvider
-	verify.ErrorHandlerProvider
-	verify.SenderProvider
-	verify.HandlerProvider
+	verification.PersistenceProvider
+	verification.ErrorHandlerProvider
+	verification.SenderProvider
+	verification.HandlerProvider
 
 	recovery.RequestPersistenceProvider
 	recovery.ErrorHandlerProvider
diff --git a/driver/registry_default.go b/driver/registry_default.go
index 15c94f01f..3abd13c03 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -13,7 +13,7 @@ import (
 	"github.com/ory/kratos/schema"
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/selfservice/hook"
 	"github.com/ory/kratos/selfservice/strategy/profile"
 	"github.com/ory/kratos/selfservice/strategy/recoverytoken"
@@ -106,10 +106,10 @@ type RegistryDefault struct {
 	selfserviceSettingsErrorHandler *settings.ErrorHandler
 	selfserviceSettingsExecutor     *settings.HookExecutor
 
-	selfserviceVerifyErrorHandler *verify.ErrorHandler
+	selfserviceVerifyErrorHandler *verification.ErrorHandler
 	selfserviceVerifyManager      *identity.Manager
-	selfserviceVerifyHandler      *verify.Handler
-	selfserviceVerifySender       *verify.Sender
+	selfserviceVerifyHandler      *verification.Handler
+	selfserviceVerifySender       *verification.Sender
 
 	selfserviceRecoveryErrorHandler *recovery.ErrorHandler
 	selfserviceRecoveryHandler      *recovery.Handler
@@ -172,6 +172,7 @@ func (m *RegistryDefault) RegisterAdminRoutes(router *x.RouterAdmin) {
 
 	if m.c.SelfServiceFlowRecoveryEnabled() {
 		m.RecoveryHandler().RegisterAdminRoutes(router)
+		m.RecoveryStrategies().RegisterAdminRoutes(router)
 	}
 
 	if m.c.SelfServiceFlowVerificationEnabled() {
diff --git a/driver/registry_default_verify.go b/driver/registry_default_verify.go
index 831b81d10..3a367de94 100644
--- a/driver/registry_default_verify.go
+++ b/driver/registry_default_verify.go
@@ -2,16 +2,16 @@ package driver
 
 import (
 	"github.com/ory/kratos/identity"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 )
 
-func (m *RegistryDefault) VerificationPersister() verify.Persister {
+func (m *RegistryDefault) VerificationPersister() verification.Persister {
 	return m.persister
 }
 
-func (m *RegistryDefault) VerificationRequestErrorHandler() *verify.ErrorHandler {
+func (m *RegistryDefault) VerificationRequestErrorHandler() *verification.ErrorHandler {
 	if m.selfserviceVerifyErrorHandler == nil {
-		m.selfserviceVerifyErrorHandler = verify.NewErrorHandler(m, m.c)
+		m.selfserviceVerifyErrorHandler = verification.NewErrorHandler(m, m.c)
 	}
 
 	return m.selfserviceVerifyErrorHandler
@@ -25,17 +25,17 @@ func (m *RegistryDefault) VerificationManager() *identity.Manager {
 	return m.selfserviceVerifyManager
 }
 
-func (m *RegistryDefault) VerificationHandler() *verify.Handler {
+func (m *RegistryDefault) VerificationHandler() *verification.Handler {
 	if m.selfserviceVerifyHandler == nil {
-		m.selfserviceVerifyHandler = verify.NewHandler(m, m.c)
+		m.selfserviceVerifyHandler = verification.NewHandler(m, m.c)
 	}
 
 	return m.selfserviceVerifyHandler
 }
 
-func (m *RegistryDefault) VerificationSender() *verify.Sender {
+func (m *RegistryDefault) VerificationSender() *verification.Sender {
 	if m.selfserviceVerifySender == nil {
-		m.selfserviceVerifySender = verify.NewSender(m, m.c)
+		m.selfserviceVerifySender = verification.NewSender(m, m.c)
 	}
 
 	return m.selfserviceVerifySender
diff --git a/identity/handler.go b/identity/handler.go
index 9a7787a6f..cbeb83279 100644
--- a/identity/handler.go
+++ b/identity/handler.go
@@ -11,8 +11,6 @@ import (
 	"github.com/ory/x/jsonx"
 	"github.com/ory/x/urlx"
 
-	"github.com/ory/x/pagination"
-
 	"github.com/ory/kratos/x"
 )
 
@@ -71,12 +69,33 @@ type identitiesListResponse struct {
 	Body []Identity
 }
 
+// swagger:parameters listIdentities
+type listIdentityParameters struct {
+	// Items per Page
+	//
+	// This is the number of items per page.
+	//
+	// required: false
+	// in: query
+	// default: 100
+	// min: 1
+	// max: 500
+	PerPage int `json:"per_page"`
+
+	// Pagination Page
+	//
+	// required: false
+	// in: query
+	// default: 0
+	// min: 0
+	Page int `json:"page"`
+}
+
 // swagger:route GET /identities admin listIdentities
 //
-// List all identities in the system
+// List Identities
 //
-// This endpoint returns a login request's context with, for example, error details and
-// other information.
+// Lists all identities. Does not support search at the moment.
 //
 // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).
 //
@@ -88,14 +107,21 @@ type identitiesListResponse struct {
 //     Responses:
 //       200: identityList
 //       500: genericError
-func (h *Handler) list(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	limit, offset := pagination.Parse(r, 100, 0, 500)
-	is, err := h.r.IdentityPool().ListIdentities(r.Context(), limit, offset)
+func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+	page, itemsPerPage := x.ParsePagination(r)
+	is, err := h.r.IdentityPool().ListIdentities(r.Context(), page, itemsPerPage)
 	if err != nil {
 		h.r.Writer().WriteError(w, r, err)
 		return
 	}
 
+	total, err := h.r.IdentityPool().CountIdentities(r.Context())
+	if err != nil {
+		h.r.Writer().WriteError(w, r, err)
+		return
+	}
+
+	x.PaginationHeader(w, urlx.AppendPaths(h.c.SelfAdminURL(), IdentitiesPath), total, page, itemsPerPage)
 	h.r.Writer().Write(w, r, is)
 }
 
@@ -110,7 +136,7 @@ type getIdentityParameters struct {
 
 // swagger:route GET /identities/{id} admin getIdentity
 //
-// Get an identity
+// Get an Identity
 //
 // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).
 //
@@ -160,7 +186,7 @@ type CreateIdentityRequestPayload struct {
 
 // swagger:route POST /identities admin createIdentity
 //
-// Create an identity
+// Create an Identity
 //
 // This endpoint creates an identity. It is NOT possible to set an identity's credentials (password, ...)
 // using this method! A way to achieve that will be introduced in the future.
@@ -223,7 +249,7 @@ type UpdateIdentityRequestPayload struct {
 
 // swagger:route PUT /identities/{id} admin updateIdentity
 //
-// Update an identity
+// Update an Identity
 //
 // This endpoint updates an identity. It is NOT possible to set an identity's credentials (password, ...)
 // using this method! A way to achieve that will be introduced in the future.
@@ -278,9 +304,11 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P
 
 // swagger:route DELETE /identities/{id} admin deleteIdentity
 //
-// Delete an identity
+// Delete an Identity
 //
-// This endpoint deletes an identity. This can not be undone.
+// Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.
+// This endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is
+// assumed that is has been deleted already.
 //
 // Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).
 //
@@ -291,7 +319,6 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P
 //
 //     Responses:
 //       204: emptyResponse
-//       404: genericError
 //       500: genericError
 func (h *Handler) delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 	if err := h.r.IdentityPool().(PrivilegedPool).DeleteIdentity(r.Context(), x.ParseUUID(ps.ByName("id"))); err != nil {
diff --git a/identity/pool.go b/identity/pool.go
index d2845df97..99f66df19 100644
--- a/identity/pool.go
+++ b/identity/pool.go
@@ -30,7 +30,11 @@ import (
 
 type (
 	Pool interface {
-		ListIdentities(ctx context.Context, limit, offset int) ([]Identity, error)
+		// ListIdentities lists all identities in the store given the page and itemsPerPage.
+		ListIdentities(ctx context.Context, page, itemsPerPage int) ([]Identity, error)
+
+		// CountIdentities counts the number of identities in the store.
+		CountIdentities(ctx context.Context) (int64, error)
 
 		// GetIdentity returns an identity by its id. Will return an error if the identity does not exist or backend
 		// connectivity is broken.
@@ -142,6 +146,10 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
 			require.NoError(t, p.CreateIdentity(context.Background(), i))
 			assert.NotEqual(t, uuid.Nil, i.ID)
 			createdIDs = append(createdIDs, i.ID)
+
+			count, err := p.CountIdentities(context.Background())
+			require.NoError(t, err)
+			assert.EqualValues(t, 1, count)
 		})
 
 		t.Run("case=create with default values", func(t *testing.T) {
@@ -156,6 +164,10 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
 			assert.Equal(t, configuration.DefaultIdentityTraitsSchemaID, actual.SchemaID)
 			assert.Equal(t, defaultSchema.SchemaURL(exampleServerURL).String(), actual.SchemaURL)
 			assertEqual(t, expected, actual)
+
+			count, err := p.CountIdentities(context.Background())
+			require.NoError(t, err)
+			assert.EqualValues(t, 2, count)
 		})
 
 		t.Run("case=should error when the identity ID does not exist", func(t *testing.T) {
@@ -351,7 +363,7 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
 		})
 
 		t.Run("case=list", func(t *testing.T) {
-			is, err := p.ListIdentities(context.Background(), 25, 0)
+			is, err := p.ListIdentities(context.Background(), 0, 25)
 			require.NoError(t, err)
 			assert.Len(t, is, len(createdIDs))
 			for _, id := range createdIDs {
diff --git a/internal/httpclient/client/admin/admin_client.go b/internal/httpclient/client/admin/admin_client.go
index 1836cb6fe..558b19308 100644
--- a/internal/httpclient/client/admin/admin_client.go
+++ b/internal/httpclient/client/admin/admin_client.go
@@ -29,6 +29,8 @@ type Client struct {
 type ClientService interface {
 	CreateIdentity(params *CreateIdentityParams) (*CreateIdentityCreated, error)
 
+	CreateRecoveryLink(params *CreateRecoveryLinkParams) (*CreateRecoveryLinkOK, error)
+
 	DeleteIdentity(params *DeleteIdentityParams) (*DeleteIdentityNoContent, error)
 
 	GetIdentity(params *GetIdentityParams) (*GetIdentityOK, error)
@@ -79,10 +81,49 @@ func (a *Client) CreateIdentity(params *CreateIdentityParams) (*CreateIdentityCr
 	panic(msg)
 }
 
+/*
+  CreateRecoveryLink creates a recovery link
+
+  This endpoint creates a recovery link which should be given to the user in order for them to recover
+(or activate) their account.
+*/
+func (a *Client) CreateRecoveryLink(params *CreateRecoveryLinkParams) (*CreateRecoveryLinkOK, error) {
+	// TODO: Validate the params before sending
+	if params == nil {
+		params = NewCreateRecoveryLinkParams()
+	}
+
+	result, err := a.transport.Submit(&runtime.ClientOperation{
+		ID:                 "createRecoveryLink",
+		Method:             "POST",
+		PathPattern:        "/recovery/link",
+		ProducesMediaTypes: []string{"application/json"},
+		ConsumesMediaTypes: []string{"application/json"},
+		Schemes:            []string{"http", "https"},
+		Params:             params,
+		Reader:             &CreateRecoveryLinkReader{formats: a.formats},
+		Context:            params.Context,
+		Client:             params.HTTPClient,
+	})
+	if err != nil {
+		return nil, err
+	}
+	success, ok := result.(*CreateRecoveryLinkOK)
+	if ok {
+		return success, nil
+	}
+	// unexpected success response
+	// safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue
+	msg := fmt.Sprintf("unexpected success response for createRecoveryLink: API contract not enforced by server. Client expected to get an error, but got: %T", result)
+	panic(msg)
+}
+
 /*
   DeleteIdentity deletes an identity
 
-  This endpoint deletes an identity. This can not be undone.
+  Calling this endpoint irrecoverably and permanently deletes the identity given its ID. This action can not be undone.
+This endpoint returns 204 when the identity was deleted or when the identity was not found, in which case it is
+assumed that is has been deleted already.
 
 Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).
 */
@@ -154,10 +195,9 @@ func (a *Client) GetIdentity(params *GetIdentityParams) (*GetIdentityOK, error)
 }
 
 /*
-  ListIdentities lists all identities in the system
+  ListIdentities lists identities
 
-  This endpoint returns a login request's context with, for example, error details and
-other information.
+  Lists all identities. Does not support search at the moment.
 
 Learn how identities work in [ORY Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model).
 */
diff --git a/internal/httpclient/client/admin/create_identity_responses.go b/internal/httpclient/client/admin/create_identity_responses.go
index 5e2173f5c..274bbf9ac 100644
--- a/internal/httpclient/client/admin/create_identity_responses.go
+++ b/internal/httpclient/client/admin/create_identity_responses.go
@@ -43,7 +43,7 @@ func (o *CreateIdentityReader) ReadResponse(response runtime.ClientResponse, con
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/admin/create_recovery_link_parameters.go b/internal/httpclient/client/admin/create_recovery_link_parameters.go
new file mode 100644
index 000000000..14e9786c2
--- /dev/null
+++ b/internal/httpclient/client/admin/create_recovery_link_parameters.go
@@ -0,0 +1,131 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package admin
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	cr "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/strfmt"
+)
+
+// NewCreateRecoveryLinkParams creates a new CreateRecoveryLinkParams object
+// with the default values initialized.
+func NewCreateRecoveryLinkParams() *CreateRecoveryLinkParams {
+	var ()
+	return &CreateRecoveryLinkParams{
+
+		timeout: cr.DefaultTimeout,
+	}
+}
+
+// NewCreateRecoveryLinkParamsWithTimeout creates a new CreateRecoveryLinkParams object
+// with the default values initialized, and the ability to set a timeout on a request
+func NewCreateRecoveryLinkParamsWithTimeout(timeout time.Duration) *CreateRecoveryLinkParams {
+	var ()
+	return &CreateRecoveryLinkParams{
+
+		timeout: timeout,
+	}
+}
+
+// NewCreateRecoveryLinkParamsWithContext creates a new CreateRecoveryLinkParams object
+// with the default values initialized, and the ability to set a context for a request
+func NewCreateRecoveryLinkParamsWithContext(ctx context.Context) *CreateRecoveryLinkParams {
+	var ()
+	return &CreateRecoveryLinkParams{
+
+		Context: ctx,
+	}
+}
+
+// NewCreateRecoveryLinkParamsWithHTTPClient creates a new CreateRecoveryLinkParams object
+// with the default values initialized, and the ability to set a custom HTTPClient for a request
+func NewCreateRecoveryLinkParamsWithHTTPClient(client *http.Client) *CreateRecoveryLinkParams {
+	var ()
+	return &CreateRecoveryLinkParams{
+		HTTPClient: client,
+	}
+}
+
+/*CreateRecoveryLinkParams contains all the parameters to send to the API endpoint
+for the create recovery link operation typically these are written to a http.Request
+*/
+type CreateRecoveryLinkParams struct {
+
+	/*Body*/
+	Body CreateRecoveryLinkBody
+
+	timeout    time.Duration
+	Context    context.Context
+	HTTPClient *http.Client
+}
+
+// WithTimeout adds the timeout to the create recovery link params
+func (o *CreateRecoveryLinkParams) WithTimeout(timeout time.Duration) *CreateRecoveryLinkParams {
+	o.SetTimeout(timeout)
+	return o
+}
+
+// SetTimeout adds the timeout to the create recovery link params
+func (o *CreateRecoveryLinkParams) SetTimeout(timeout time.Duration) {
+	o.timeout = timeout
+}
+
+// WithContext adds the context to the create recovery link params
+func (o *CreateRecoveryLinkParams) WithContext(ctx context.Context) *CreateRecoveryLinkParams {
+	o.SetContext(ctx)
+	return o
+}
+
+// SetContext adds the context to the create recovery link params
+func (o *CreateRecoveryLinkParams) SetContext(ctx context.Context) {
+	o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the create recovery link params
+func (o *CreateRecoveryLinkParams) WithHTTPClient(client *http.Client) *CreateRecoveryLinkParams {
+	o.SetHTTPClient(client)
+	return o
+}
+
+// SetHTTPClient adds the HTTPClient to the create recovery link params
+func (o *CreateRecoveryLinkParams) SetHTTPClient(client *http.Client) {
+	o.HTTPClient = client
+}
+
+// WithBody adds the body to the create recovery link params
+func (o *CreateRecoveryLinkParams) WithBody(body CreateRecoveryLinkBody) *CreateRecoveryLinkParams {
+	o.SetBody(body)
+	return o
+}
+
+// SetBody adds the body to the create recovery link params
+func (o *CreateRecoveryLinkParams) SetBody(body CreateRecoveryLinkBody) {
+	o.Body = body
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *CreateRecoveryLinkParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+	if err := r.SetTimeout(o.timeout); err != nil {
+		return err
+	}
+	var res []error
+
+	if err := r.SetBodyParam(o.Body); err != nil {
+		return err
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/internal/httpclient/client/admin/create_recovery_link_responses.go b/internal/httpclient/client/admin/create_recovery_link_responses.go
new file mode 100644
index 000000000..eb651d02a
--- /dev/null
+++ b/internal/httpclient/client/admin/create_recovery_link_responses.go
@@ -0,0 +1,268 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package admin
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+
+	"github.com/ory/kratos/internal/httpclient/models"
+)
+
+// CreateRecoveryLinkReader is a Reader for the CreateRecoveryLink structure.
+type CreateRecoveryLinkReader struct {
+	formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *CreateRecoveryLinkReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	switch response.Code() {
+	case 200:
+		result := NewCreateRecoveryLinkOK()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return result, nil
+	case 400:
+		result := NewCreateRecoveryLinkBadRequest()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	case 404:
+		result := NewCreateRecoveryLinkNotFound()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+	case 500:
+		result := NewCreateRecoveryLinkInternalServerError()
+		if err := result.readResponse(response, consumer, o.formats); err != nil {
+			return nil, err
+		}
+		return nil, result
+
+	default:
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
+	}
+}
+
+// NewCreateRecoveryLinkOK creates a CreateRecoveryLinkOK with default headers values
+func NewCreateRecoveryLinkOK() *CreateRecoveryLinkOK {
+	return &CreateRecoveryLinkOK{}
+}
+
+/*CreateRecoveryLinkOK handles this case with default header values.
+
+createRecoveryLinkResponse
+*/
+type CreateRecoveryLinkOK struct {
+	Payload *models.CreateRecoveryLinkResponse
+}
+
+func (o *CreateRecoveryLinkOK) Error() string {
+	return fmt.Sprintf("[POST /recovery/link][%d] createRecoveryLinkOK  %+v", 200, o.Payload)
+}
+
+func (o *CreateRecoveryLinkOK) GetPayload() *models.CreateRecoveryLinkResponse {
+	return o.Payload
+}
+
+func (o *CreateRecoveryLinkOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.CreateRecoveryLinkResponse)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewCreateRecoveryLinkBadRequest creates a CreateRecoveryLinkBadRequest with default headers values
+func NewCreateRecoveryLinkBadRequest() *CreateRecoveryLinkBadRequest {
+	return &CreateRecoveryLinkBadRequest{}
+}
+
+/*CreateRecoveryLinkBadRequest handles this case with default header values.
+
+genericError
+*/
+type CreateRecoveryLinkBadRequest struct {
+	Payload *models.GenericError
+}
+
+func (o *CreateRecoveryLinkBadRequest) Error() string {
+	return fmt.Sprintf("[POST /recovery/link][%d] createRecoveryLinkBadRequest  %+v", 400, o.Payload)
+}
+
+func (o *CreateRecoveryLinkBadRequest) GetPayload() *models.GenericError {
+	return o.Payload
+}
+
+func (o *CreateRecoveryLinkBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.GenericError)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewCreateRecoveryLinkNotFound creates a CreateRecoveryLinkNotFound with default headers values
+func NewCreateRecoveryLinkNotFound() *CreateRecoveryLinkNotFound {
+	return &CreateRecoveryLinkNotFound{}
+}
+
+/*CreateRecoveryLinkNotFound handles this case with default header values.
+
+genericError
+*/
+type CreateRecoveryLinkNotFound struct {
+	Payload *models.GenericError
+}
+
+func (o *CreateRecoveryLinkNotFound) Error() string {
+	return fmt.Sprintf("[POST /recovery/link][%d] createRecoveryLinkNotFound  %+v", 404, o.Payload)
+}
+
+func (o *CreateRecoveryLinkNotFound) GetPayload() *models.GenericError {
+	return o.Payload
+}
+
+func (o *CreateRecoveryLinkNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.GenericError)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+// NewCreateRecoveryLinkInternalServerError creates a CreateRecoveryLinkInternalServerError with default headers values
+func NewCreateRecoveryLinkInternalServerError() *CreateRecoveryLinkInternalServerError {
+	return &CreateRecoveryLinkInternalServerError{}
+}
+
+/*CreateRecoveryLinkInternalServerError handles this case with default header values.
+
+genericError
+*/
+type CreateRecoveryLinkInternalServerError struct {
+	Payload *models.GenericError
+}
+
+func (o *CreateRecoveryLinkInternalServerError) Error() string {
+	return fmt.Sprintf("[POST /recovery/link][%d] createRecoveryLinkInternalServerError  %+v", 500, o.Payload)
+}
+
+func (o *CreateRecoveryLinkInternalServerError) GetPayload() *models.GenericError {
+	return o.Payload
+}
+
+func (o *CreateRecoveryLinkInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+	o.Payload = new(models.GenericError)
+
+	// response payload
+	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+/*CreateRecoveryLinkBody create recovery link body
+swagger:model CreateRecoveryLinkBody
+*/
+type CreateRecoveryLinkBody struct {
+
+	// Link Expires In
+	//
+	// The recovery link will expire at that point in time. Defaults to the configuration value of
+	// `selfservice.flows.recovery.request_lifespan`.
+	// Pattern: ^[0-9]+(ns|us|ms|s|m|h)$
+	ExpiresIn string `json:"expires_in,omitempty"`
+
+	// identity id
+	// Required: true
+	// Format: uuid4
+	IdentityID models.UUID `json:"identity_id"`
+}
+
+// Validate validates this create recovery link body
+func (o *CreateRecoveryLinkBody) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := o.validateExpiresIn(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := o.validateIdentityID(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (o *CreateRecoveryLinkBody) validateExpiresIn(formats strfmt.Registry) error {
+
+	if swag.IsZero(o.ExpiresIn) { // not required
+		return nil
+	}
+
+	if err := validate.Pattern("Body"+"."+"expires_in", "body", string(o.ExpiresIn), `^[0-9]+(ns|us|ms|s|m|h)$`); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (o *CreateRecoveryLinkBody) validateIdentityID(formats strfmt.Registry) error {
+
+	if err := o.IdentityID.Validate(formats); err != nil {
+		if ve, ok := err.(*errors.Validation); ok {
+			return ve.ValidateName("Body" + "." + "identity_id")
+		}
+		return err
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (o *CreateRecoveryLinkBody) MarshalBinary() ([]byte, error) {
+	if o == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(o)
+}
+
+// UnmarshalBinary interface implementation
+func (o *CreateRecoveryLinkBody) UnmarshalBinary(b []byte) error {
+	var res CreateRecoveryLinkBody
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*o = res
+	return nil
+}
diff --git a/internal/httpclient/client/admin/delete_identity_responses.go b/internal/httpclient/client/admin/delete_identity_responses.go
index 5b1140d26..e427870e7 100644
--- a/internal/httpclient/client/admin/delete_identity_responses.go
+++ b/internal/httpclient/client/admin/delete_identity_responses.go
@@ -29,12 +29,6 @@ func (o *DeleteIdentityReader) ReadResponse(response runtime.ClientResponse, con
 			return nil, err
 		}
 		return result, nil
-	case 404:
-		result := NewDeleteIdentityNotFound()
-		if err := result.readResponse(response, consumer, o.formats); err != nil {
-			return nil, err
-		}
-		return nil, result
 	case 500:
 		result := NewDeleteIdentityInternalServerError()
 		if err := result.readResponse(response, consumer, o.formats); err != nil {
@@ -43,7 +37,7 @@ func (o *DeleteIdentityReader) ReadResponse(response runtime.ClientResponse, con
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
@@ -69,39 +63,6 @@ func (o *DeleteIdentityNoContent) readResponse(response runtime.ClientResponse,
 	return nil
 }
 
-// NewDeleteIdentityNotFound creates a DeleteIdentityNotFound with default headers values
-func NewDeleteIdentityNotFound() *DeleteIdentityNotFound {
-	return &DeleteIdentityNotFound{}
-}
-
-/*DeleteIdentityNotFound handles this case with default header values.
-
-genericError
-*/
-type DeleteIdentityNotFound struct {
-	Payload *models.GenericError
-}
-
-func (o *DeleteIdentityNotFound) Error() string {
-	return fmt.Sprintf("[DELETE /identities/{id}][%d] deleteIdentityNotFound  %+v", 404, o.Payload)
-}
-
-func (o *DeleteIdentityNotFound) GetPayload() *models.GenericError {
-	return o.Payload
-}
-
-func (o *DeleteIdentityNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
-
-	o.Payload = new(models.GenericError)
-
-	// response payload
-	if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
-		return err
-	}
-
-	return nil
-}
-
 // NewDeleteIdentityInternalServerError creates a DeleteIdentityInternalServerError with default headers values
 func NewDeleteIdentityInternalServerError() *DeleteIdentityInternalServerError {
 	return &DeleteIdentityInternalServerError{}
diff --git a/internal/httpclient/client/admin/get_identity_responses.go b/internal/httpclient/client/admin/get_identity_responses.go
index 9a8941f9f..627ef91e5 100644
--- a/internal/httpclient/client/admin/get_identity_responses.go
+++ b/internal/httpclient/client/admin/get_identity_responses.go
@@ -43,7 +43,7 @@ func (o *GetIdentityReader) ReadResponse(response runtime.ClientResponse, consum
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/admin/list_identities_parameters.go b/internal/httpclient/client/admin/list_identities_parameters.go
index b88e99230..bd9b16269 100644
--- a/internal/httpclient/client/admin/list_identities_parameters.go
+++ b/internal/httpclient/client/admin/list_identities_parameters.go
@@ -14,13 +14,19 @@ import (
 	"github.com/go-openapi/runtime"
 	cr "github.com/go-openapi/runtime/client"
 	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
 )
 
 // NewListIdentitiesParams creates a new ListIdentitiesParams object
 // with the default values initialized.
 func NewListIdentitiesParams() *ListIdentitiesParams {
-
+	var (
+		pageDefault    = int64(0)
+		perPageDefault = int64(100)
+	)
 	return &ListIdentitiesParams{
+		Page:    &pageDefault,
+		PerPage: &perPageDefault,
 
 		timeout: cr.DefaultTimeout,
 	}
@@ -29,8 +35,13 @@ func NewListIdentitiesParams() *ListIdentitiesParams {
 // NewListIdentitiesParamsWithTimeout creates a new ListIdentitiesParams object
 // with the default values initialized, and the ability to set a timeout on a request
 func NewListIdentitiesParamsWithTimeout(timeout time.Duration) *ListIdentitiesParams {
-
+	var (
+		pageDefault    = int64(0)
+		perPageDefault = int64(100)
+	)
 	return &ListIdentitiesParams{
+		Page:    &pageDefault,
+		PerPage: &perPageDefault,
 
 		timeout: timeout,
 	}
@@ -39,8 +50,13 @@ func NewListIdentitiesParamsWithTimeout(timeout time.Duration) *ListIdentitiesPa
 // NewListIdentitiesParamsWithContext creates a new ListIdentitiesParams object
 // with the default values initialized, and the ability to set a context for a request
 func NewListIdentitiesParamsWithContext(ctx context.Context) *ListIdentitiesParams {
-
+	var (
+		pageDefault    = int64(0)
+		perPageDefault = int64(100)
+	)
 	return &ListIdentitiesParams{
+		Page:    &pageDefault,
+		PerPage: &perPageDefault,
 
 		Context: ctx,
 	}
@@ -49,8 +65,13 @@ func NewListIdentitiesParamsWithContext(ctx context.Context) *ListIdentitiesPara
 // NewListIdentitiesParamsWithHTTPClient creates a new ListIdentitiesParams object
 // with the default values initialized, and the ability to set a custom HTTPClient for a request
 func NewListIdentitiesParamsWithHTTPClient(client *http.Client) *ListIdentitiesParams {
-
+	var (
+		pageDefault    = int64(0)
+		perPageDefault = int64(100)
+	)
 	return &ListIdentitiesParams{
+		Page:       &pageDefault,
+		PerPage:    &perPageDefault,
 		HTTPClient: client,
 	}
 }
@@ -59,6 +80,20 @@ func NewListIdentitiesParamsWithHTTPClient(client *http.Client) *ListIdentitiesP
 for the list identities operation typically these are written to a http.Request
 */
 type ListIdentitiesParams struct {
+
+	/*Page
+	  Pagination Page
+
+	*/
+	Page *int64
+	/*PerPage
+	  Items per Page
+
+	This is the number of items per page.
+
+	*/
+	PerPage *int64
+
 	timeout    time.Duration
 	Context    context.Context
 	HTTPClient *http.Client
@@ -97,6 +132,28 @@ func (o *ListIdentitiesParams) SetHTTPClient(client *http.Client) {
 	o.HTTPClient = client
 }
 
+// WithPage adds the page to the list identities params
+func (o *ListIdentitiesParams) WithPage(page *int64) *ListIdentitiesParams {
+	o.SetPage(page)
+	return o
+}
+
+// SetPage adds the page to the list identities params
+func (o *ListIdentitiesParams) SetPage(page *int64) {
+	o.Page = page
+}
+
+// WithPerPage adds the perPage to the list identities params
+func (o *ListIdentitiesParams) WithPerPage(perPage *int64) *ListIdentitiesParams {
+	o.SetPerPage(perPage)
+	return o
+}
+
+// SetPerPage adds the perPage to the list identities params
+func (o *ListIdentitiesParams) SetPerPage(perPage *int64) {
+	o.PerPage = perPage
+}
+
 // WriteToRequest writes these params to a swagger request
 func (o *ListIdentitiesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
 
@@ -105,6 +162,38 @@ func (o *ListIdentitiesParams) WriteToRequest(r runtime.ClientRequest, reg strfm
 	}
 	var res []error
 
+	if o.Page != nil {
+
+		// query param page
+		var qrPage int64
+		if o.Page != nil {
+			qrPage = *o.Page
+		}
+		qPage := swag.FormatInt64(qrPage)
+		if qPage != "" {
+			if err := r.SetQueryParam("page", qPage); err != nil {
+				return err
+			}
+		}
+
+	}
+
+	if o.PerPage != nil {
+
+		// query param per_page
+		var qrPerPage int64
+		if o.PerPage != nil {
+			qrPerPage = *o.PerPage
+		}
+		qPerPage := swag.FormatInt64(qrPerPage)
+		if qPerPage != "" {
+			if err := r.SetQueryParam("per_page", qPerPage); err != nil {
+				return err
+			}
+		}
+
+	}
+
 	if len(res) > 0 {
 		return errors.CompositeValidationError(res...)
 	}
diff --git a/internal/httpclient/client/admin/list_identities_responses.go b/internal/httpclient/client/admin/list_identities_responses.go
index c5f85b4f5..66cb2fda6 100644
--- a/internal/httpclient/client/admin/list_identities_responses.go
+++ b/internal/httpclient/client/admin/list_identities_responses.go
@@ -37,7 +37,7 @@ func (o *ListIdentitiesReader) ReadResponse(response runtime.ClientResponse, con
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/admin/update_identity_responses.go b/internal/httpclient/client/admin/update_identity_responses.go
index c3521abb3..6bf416f4b 100644
--- a/internal/httpclient/client/admin/update_identity_responses.go
+++ b/internal/httpclient/client/admin/update_identity_responses.go
@@ -49,7 +49,7 @@ func (o *UpdateIdentityReader) ReadResponse(response runtime.ClientResponse, con
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_schema_responses.go b/internal/httpclient/client/common/get_schema_responses.go
index 7b35f518d..74d43bbbc 100644
--- a/internal/httpclient/client/common/get_schema_responses.go
+++ b/internal/httpclient/client/common/get_schema_responses.go
@@ -43,7 +43,7 @@ func (o *GetSchemaReader) ReadResponse(response runtime.ClientResponse, consumer
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go
index 32b07cd5f..89903376e 100644
--- a/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go
+++ b/internal/httpclient/client/common/get_self_service_browser_login_request_responses.go
@@ -55,7 +55,7 @@ func (o *GetSelfServiceBrowserLoginRequestReader) ReadResponse(response runtime.
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_browser_recovery_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_recovery_request_responses.go
index 1be5312ea..fa40ce21d 100644
--- a/internal/httpclient/client/common/get_self_service_browser_recovery_request_responses.go
+++ b/internal/httpclient/client/common/get_self_service_browser_recovery_request_responses.go
@@ -55,7 +55,7 @@ func (o *GetSelfServiceBrowserRecoveryRequestReader) ReadResponse(response runti
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go
index 8651879d9..932cb3f20 100644
--- a/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go
+++ b/internal/httpclient/client/common/get_self_service_browser_registration_request_responses.go
@@ -55,7 +55,7 @@ func (o *GetSelfServiceBrowserRegistrationRequestReader) ReadResponse(response r
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go b/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go
index a2b6d4737..2bd06d6b8 100644
--- a/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go
+++ b/internal/httpclient/client/common/get_self_service_browser_settings_request_responses.go
@@ -55,7 +55,7 @@ func (o *GetSelfServiceBrowserSettingsRequestReader) ReadResponse(response runti
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_error_responses.go b/internal/httpclient/client/common/get_self_service_error_responses.go
index 101855f2f..21fccc2df 100644
--- a/internal/httpclient/client/common/get_self_service_error_responses.go
+++ b/internal/httpclient/client/common/get_self_service_error_responses.go
@@ -49,7 +49,7 @@ func (o *GetSelfServiceErrorReader) ReadResponse(response runtime.ClientResponse
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/common/get_self_service_verification_request_responses.go b/internal/httpclient/client/common/get_self_service_verification_request_responses.go
index 629e2177c..61d9f44c2 100644
--- a/internal/httpclient/client/common/get_self_service_verification_request_responses.go
+++ b/internal/httpclient/client/common/get_self_service_verification_request_responses.go
@@ -49,7 +49,7 @@ func (o *GetSelfServiceVerificationRequestReader) ReadResponse(response runtime.
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/health/is_instance_alive_responses.go b/internal/httpclient/client/health/is_instance_alive_responses.go
index e2a749c5f..392091c8f 100644
--- a/internal/httpclient/client/health/is_instance_alive_responses.go
+++ b/internal/httpclient/client/health/is_instance_alive_responses.go
@@ -37,7 +37,7 @@ func (o *IsInstanceAliveReader) ReadResponse(response runtime.ClientResponse, co
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/health/is_instance_ready_responses.go b/internal/httpclient/client/health/is_instance_ready_responses.go
index d64fb185a..e3b8c0c29 100644
--- a/internal/httpclient/client/health/is_instance_ready_responses.go
+++ b/internal/httpclient/client/health/is_instance_ready_responses.go
@@ -37,7 +37,7 @@ func (o *IsInstanceReadyReader) ReadResponse(response runtime.ClientResponse, co
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go
index a25c44325..88d4a51d7 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go
@@ -19,7 +19,7 @@ import (
 // NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
 // with the default values initialized.
 func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams() *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
-
+	var ()
 	return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
 
 		timeout: cr.DefaultTimeout,
@@ -29,7 +29,7 @@ func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams() *CompleteSelf
 // NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithTimeout creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
 // with the default values initialized, and the ability to set a timeout on a request
 func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithTimeout(timeout time.Duration) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
-
+	var ()
 	return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
 
 		timeout: timeout,
@@ -39,7 +39,7 @@ func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithTimeout(time
 // NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithContext creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
 // with the default values initialized, and the ability to set a context for a request
 func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithContext(ctx context.Context) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
-
+	var ()
 	return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
 
 		Context: ctx,
@@ -49,7 +49,7 @@ func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithContext(ctx
 // NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithHTTPClient creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
 // with the default values initialized, and the ability to set a custom HTTPClient for a request
 func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithHTTPClient(client *http.Client) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
-
+	var ()
 	return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
 		HTTPClient: client,
 	}
@@ -59,6 +59,30 @@ func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithHTTPClient(c
 for the complete self service browser recovery link strategy flow operation typically these are written to a http.Request
 */
 type CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams struct {
+
+	/*Email
+	  Email to Recover
+
+	Needs to be set when initiating the flow. If the email is a registered
+	recovery email, a recovery link will be sent. If the email is not known,
+	a email with details on what happened will be sent instead.
+
+	*/
+	Email string
+	/*Request
+	  Recovery Request ID
+
+	*/
+	Request *string
+	/*Token
+	  Recovery Token
+
+	The recovery token which completes the recovery request. If the token
+	is invalid (e.g. expired) an error will be shown to the end-user.
+
+	*/
+	Token *string
+
 	timeout    time.Duration
 	Context    context.Context
 	HTTPClient *http.Client
@@ -97,6 +121,39 @@ func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetHTTPClient
 	o.HTTPClient = client
 }
 
+// WithEmail adds the email to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithEmail(email string) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+	o.SetEmail(email)
+	return o
+}
+
+// SetEmail adds the email to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetEmail(email string) {
+	o.Email = email
+}
+
+// WithRequest adds the request to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithRequest(request *string) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+	o.SetRequest(request)
+	return o
+}
+
+// SetRequest adds the request to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetRequest(request *string) {
+	o.Request = request
+}
+
+// WithToken adds the token to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithToken(token *string) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+	o.SetToken(token)
+	return o
+}
+
+// SetToken adds the token to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetToken(token *string) {
+	o.Token = token
+}
+
 // WriteToRequest writes these params to a swagger request
 func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
 
@@ -105,6 +162,42 @@ func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WriteToReques
 	}
 	var res []error
 
+	if err := r.SetBodyParam(o.Email); err != nil {
+		return err
+	}
+
+	if o.Request != nil {
+
+		// query param request
+		var qrRequest string
+		if o.Request != nil {
+			qrRequest = *o.Request
+		}
+		qRequest := qrRequest
+		if qRequest != "" {
+			if err := r.SetQueryParam("request", qRequest); err != nil {
+				return err
+			}
+		}
+
+	}
+
+	if o.Token != nil {
+
+		// query param token
+		var qrToken string
+		if o.Token != nil {
+			qrToken = *o.Token
+		}
+		qToken := qrToken
+		if qToken != "" {
+			if err := r.SetQueryParam("token", qToken); err != nil {
+				return err
+			}
+		}
+
+	}
+
 	if len(res) > 0 {
 		return errors.CompositeValidationError(res...)
 	}
diff --git a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go
index 6a9b6d1d3..e5d0f8bf9 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go
@@ -37,7 +37,7 @@ func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader) ReadResponse(
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_o_id_c_settings_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_settings_o_id_c_settings_flow_responses.go
index ddebc8a6c..ca637dbb2 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_settings_o_id_c_settings_flow_responses.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_settings_o_id_c_settings_flow_responses.go
@@ -37,7 +37,7 @@ func (o *CompleteSelfServiceBrowserSettingsOIDCSettingsFlowReader) ReadResponse(
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go
index e0fee737b..5a0a8ffc3 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_settings_password_strategy_flow_responses.go
@@ -37,7 +37,7 @@ func (o *CompleteSelfServiceBrowserSettingsPasswordStrategyFlowReader) ReadRespo
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go
index fe112eaa4..2df539415 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_settings_profile_strategy_flow_responses.go
@@ -37,7 +37,7 @@ func (o *CompleteSelfServiceBrowserSettingsProfileStrategyFlowReader) ReadRespon
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go
index ea3d53ff7..418ce0651 100644
--- a/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go
+++ b/internal/httpclient/client/public/complete_self_service_browser_verification_flow_responses.go
@@ -37,7 +37,7 @@ func (o *CompleteSelfServiceBrowserVerificationFlowReader) ReadResponse(response
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go
index 071d74715..2ffa4c434 100644
--- a/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_browser_login_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceBrowserLoginFlowReader) ReadResponse(response runt
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go
index 30e222afa..0ae132d7d 100644
--- a/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_browser_logout_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceBrowserLogoutFlowReader) ReadResponse(response run
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go
index e3328029e..5b65b6864 100644
--- a/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_browser_registration_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceBrowserRegistrationFlowReader) ReadResponse(respon
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go
index 78bf0eb6d..a6fa50d3b 100644
--- a/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_browser_verification_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceBrowserVerificationFlowReader) ReadResponse(respon
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_recovery_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_recovery_flow_responses.go
index 69ff637a9..1995abf0e 100644
--- a/internal/httpclient/client/public/initialize_self_service_recovery_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_recovery_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceRecoveryFlowReader) ReadResponse(response runtime.
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go b/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go
index 0e4a6a692..bb6bc9a14 100644
--- a/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go
+++ b/internal/httpclient/client/public/initialize_self_service_settings_flow_responses.go
@@ -37,7 +37,7 @@ func (o *InitializeSelfServiceSettingsFlowReader) ReadResponse(response runtime.
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/public_client.go b/internal/httpclient/client/public/public_client.go
index 1528187f0..6ea4411cd 100644
--- a/internal/httpclient/client/public/public_client.go
+++ b/internal/httpclient/client/public/public_client.go
@@ -74,7 +74,7 @@ func (a *Client) CompleteSelfServiceBrowserRecoveryLinkStrategyFlow(params *Comp
 		Method:             "POST",
 		PathPattern:        "/self-service/browser/flows/recovery/link",
 		ProducesMediaTypes: []string{"application/json"},
-		ConsumesMediaTypes: []string{"application/json", "application/x-www-form-urlencoded"},
+		ConsumesMediaTypes: []string{"application/x-www-form-urlencoded"},
 		Schemes:            []string{"http", "https"},
 		Params:             params,
 		Reader:             &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader{formats: a.formats},
diff --git a/internal/httpclient/client/public/self_service_browser_verify_responses.go b/internal/httpclient/client/public/self_service_browser_verify_responses.go
index 980071a46..8681ff7d7 100644
--- a/internal/httpclient/client/public/self_service_browser_verify_responses.go
+++ b/internal/httpclient/client/public/self_service_browser_verify_responses.go
@@ -37,7 +37,7 @@ func (o *SelfServiceBrowserVerifyReader) ReadResponse(response runtime.ClientRes
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/public/whoami_responses.go b/internal/httpclient/client/public/whoami_responses.go
index 17df465a2..c98c93040 100644
--- a/internal/httpclient/client/public/whoami_responses.go
+++ b/internal/httpclient/client/public/whoami_responses.go
@@ -43,7 +43,7 @@ func (o *WhoamiReader) ReadResponse(response runtime.ClientResponse, consumer ru
 		return nil, result
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/client/version/get_version_responses.go b/internal/httpclient/client/version/get_version_responses.go
index cc0995099..35b785613 100644
--- a/internal/httpclient/client/version/get_version_responses.go
+++ b/internal/httpclient/client/version/get_version_responses.go
@@ -31,7 +31,7 @@ func (o *GetVersionReader) ReadResponse(response runtime.ClientResponse, consume
 		return result, nil
 
 	default:
-		return nil, runtime.NewAPIError("unknown error", response, response.Code())
+		return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code())
 	}
 }
 
diff --git a/internal/httpclient/models/create_recovery_link_response.go b/internal/httpclient/models/create_recovery_link_response.go
new file mode 100644
index 000000000..2f7a4c6db
--- /dev/null
+++ b/internal/httpclient/models/create_recovery_link_response.go
@@ -0,0 +1,89 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+	"github.com/go-openapi/validate"
+)
+
+// CreateRecoveryLinkResponse create recovery link response
+//
+// swagger:model createRecoveryLinkResponse
+type CreateRecoveryLinkResponse struct {
+
+	// Recovery Link Expires At
+	//
+	// The timestamp when the recovery link expires.
+	// Format: date-time
+	ExpiresAt strfmt.DateTime `json:"expires_at,omitempty"`
+
+	// Recovery Link
+	//
+	// This link can be used to recover the account.
+	// Required: true
+	RecoveryLink *string `json:"recovery_link"`
+}
+
+// Validate validates this create recovery link response
+func (m *CreateRecoveryLinkResponse) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	if err := m.validateExpiresAt(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if err := m.validateRecoveryLink(formats); err != nil {
+		res = append(res, err)
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
+
+func (m *CreateRecoveryLinkResponse) validateExpiresAt(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.ExpiresAt) { // not required
+		return nil
+	}
+
+	if err := validate.FormatOf("expires_at", "body", "date-time", m.ExpiresAt.String(), formats); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *CreateRecoveryLinkResponse) validateRecoveryLink(formats strfmt.Registry) error {
+
+	if err := validate.Required("recovery_link", "body", m.RecoveryLink); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *CreateRecoveryLinkResponse) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *CreateRecoveryLinkResponse) UnmarshalBinary(b []byte) error {
+	var res CreateRecoveryLinkResponse
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/persistence/reference.go b/persistence/reference.go
index 10962bbd9..1c4695799 100644
--- a/persistence/reference.go
+++ b/persistence/reference.go
@@ -14,7 +14,7 @@ import (
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/selfservice/flow/registration"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/selfservice/strategy/recoverytoken"
 	"github.com/ory/kratos/session"
 )
@@ -32,7 +32,7 @@ type Persister interface {
 	courier.Persister
 	session.Persister
 	errorx.Persister
-	verify.Persister
+	verification.Persister
 	recovery.RequestPersister
 	recoverytoken.Persister
 
diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go
index 629639ff3..6e0348fea 100644
--- a/persistence/sql/persister_identity.go
+++ b/persistence/sql/persister_identity.go
@@ -157,6 +157,14 @@ func createRecoveryAddresses(ctx context.Context, tx *pop.Connection, i *identit
 	return nil
 }
 
+func (p *Persister) CountIdentities(ctx context.Context) (int64, error) {
+	count, err := p.c.WithContext(ctx).Count(new(identity.Identity))
+	if err != nil {
+		return 0, sqlcon.HandleError(err)
+	}
+	return int64(count), nil
+}
+
 func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) error {
 	if i.SchemaID == "" {
 		i.SchemaID = configuration.DefaultIdentityTraitsSchemaID
@@ -191,12 +199,11 @@ func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) er
 	})
 }
 
-func (p *Persister) ListIdentities(ctx context.Context, limit, offset int) ([]identity.Identity, error) {
+func (p *Persister) ListIdentities(ctx context.Context, page, perPage int) ([]identity.Identity, error) {
 	is := make([]identity.Identity, 0)
 
 	/* #nosec G201 TableName is static */
-	if err := sqlcon.HandleError(p.GetConnection(ctx).
-		RawQuery(fmt.Sprintf("SELECT * FROM %s LIMIT ? OFFSET ?", new(identity.Identity).TableName()), limit, offset).
+	if err := sqlcon.HandleError(p.GetConnection(ctx).Paginate(page, perPage).Order("id DESC").
 		Eager("VerifiableAddresses", "RecoveryAddresses").All(&is)); err != nil {
 		return nil, err
 	}
diff --git a/persistence/sql/persister_verify.go b/persistence/sql/persister_verify.go
index e294954e9..3def71b0e 100644
--- a/persistence/sql/persister_verify.go
+++ b/persistence/sql/persister_verify.go
@@ -7,25 +7,25 @@ import (
 
 	"github.com/ory/x/sqlcon"
 
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 )
 
-var _ verify.Persister = new(Persister)
+var _ verification.Persister = new(Persister)
 
-func (p Persister) CreateVerificationRequest(ctx context.Context, r *verify.Request) error {
+func (p Persister) CreateVerificationRequest(ctx context.Context, r *verification.Request) error {
 	// This should not create the request eagerly because otherwise we might accidentally create an address
 	// that isn't supposed to be in the database.
 	return p.GetConnection(ctx).Create(r)
 }
 
-func (p Persister) GetVerificationRequest(ctx context.Context, id uuid.UUID) (*verify.Request, error) {
-	var r verify.Request
+func (p Persister) GetVerificationRequest(ctx context.Context, id uuid.UUID) (*verification.Request, error) {
+	var r verification.Request
 	if err := p.GetConnection(ctx).Find(&r, id); err != nil {
 		return nil, sqlcon.HandleError(err)
 	}
 	return &r, nil
 }
 
-func (p Persister) UpdateVerificationRequest(ctx context.Context, r *verify.Request) error {
+func (p Persister) UpdateVerificationRequest(ctx context.Context, r *verification.Request) error {
 	return sqlcon.HandleError(p.GetConnection(ctx).Update(r))
 }
diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go
index 188e4fcd6..f86179cc6 100644
--- a/selfservice/flow/recovery/strategy.go
+++ b/selfservice/flow/recovery/strategy.go
@@ -15,9 +15,14 @@ const (
 type (
 	Strategy interface {
 		RecoveryStrategyID() string
-		RegisterRecoveryRoutes(*x.RouterPublic)
 		PopulateRecoveryMethod(*http.Request, *Request) error
 	}
+	AdminHandler interface {
+		RegisterAdminRecoveryRoutes(admin *x.RouterAdmin)
+	}
+	PublicHandler interface {
+		RegisterPublicRecoveryRoutes(public *x.RouterPublic)
+	}
 	Strategies       []Strategy
 	StrategyProvider interface {
 		RecoveryStrategies() Strategies
@@ -46,6 +51,16 @@ func (s Strategies) MustStrategy(id string) Strategy {
 
 func (s Strategies) RegisterPublicRoutes(r *x.RouterPublic) {
 	for _, ss := range s {
-		ss.RegisterRecoveryRoutes(r)
+		if h, ok := ss.(PublicHandler); ok {
+			h.RegisterPublicRecoveryRoutes(r)
+		}
+	}
+}
+
+func (s Strategies) RegisterAdminRoutes(r *x.RouterAdmin) {
+	for _, ss := range s {
+		if h, ok := ss.(AdminHandler); ok {
+			h.RegisterAdminRecoveryRoutes(r)
+		}
 	}
 }
diff --git a/selfservice/flow/verify/error.go b/selfservice/flow/verification/error.go
similarity index 99%
rename from selfservice/flow/verify/error.go
rename to selfservice/flow/verification/error.go
index d7d35e0cc..76c516430 100644
--- a/selfservice/flow/verify/error.go
+++ b/selfservice/flow/verification/error.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"net/http"
diff --git a/selfservice/flow/verify/handler.go b/selfservice/flow/verification/handler.go
similarity index 99%
rename from selfservice/flow/verify/handler.go
rename to selfservice/flow/verification/handler.go
index 1439c3104..0de202dd9 100644
--- a/selfservice/flow/verify/handler.go
+++ b/selfservice/flow/verification/handler.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"net/http"
diff --git a/selfservice/flow/verify/persistence.go b/selfservice/flow/verification/persistence.go
similarity index 99%
rename from selfservice/flow/verify/persistence.go
rename to selfservice/flow/verification/persistence.go
index 3851b2de7..26dbd8d23 100644
--- a/selfservice/flow/verify/persistence.go
+++ b/selfservice/flow/verification/persistence.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"context"
diff --git a/selfservice/flow/verify/request.go b/selfservice/flow/verification/request.go
similarity index 99%
rename from selfservice/flow/verify/request.go
rename to selfservice/flow/verification/request.go
index ed177b681..060c3704a 100644
--- a/selfservice/flow/verify/request.go
+++ b/selfservice/flow/verification/request.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"net/http"
diff --git a/selfservice/flow/verify/sender.go b/selfservice/flow/verification/sender.go
similarity index 99%
rename from selfservice/flow/verify/sender.go
rename to selfservice/flow/verification/sender.go
index e7d2e4c67..90706cad3 100644
--- a/selfservice/flow/verify/sender.go
+++ b/selfservice/flow/verification/sender.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"context"
diff --git a/selfservice/flow/verify/stub/extension/schema.json b/selfservice/flow/verification/stub/extension/schema.json
similarity index 100%
rename from selfservice/flow/verify/stub/extension/schema.json
rename to selfservice/flow/verification/stub/extension/schema.json
diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go
index 2be6e363e..6307dec63 100644
--- a/selfservice/hook/verification.go
+++ b/selfservice/hook/verification.go
@@ -6,7 +6,7 @@ import (
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/selfservice/flow/registration"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/session"
 )
 
@@ -15,7 +15,7 @@ var _ settings.PostHookPostPersistExecutor = new(Verifier)
 
 type (
 	verifierDependencies interface {
-		verify.SenderProvider
+		verification.SenderProvider
 	}
 	Verifier struct {
 		r verifierDependencies
diff --git a/selfservice/strategy/password/validator.go b/selfservice/strategy/password/validator.go
index 3c474a493..5799f4904 100644
--- a/selfservice/strategy/password/validator.go
+++ b/selfservice/strategy/password/validator.go
@@ -34,6 +34,8 @@ type ValidationProvider interface {
 }
 
 var _ Validator = new(DefaultPasswordValidator)
+var ErrNetworkFailure = errors.New("unable to check if password has been leaked because an unexpected network error occurred")
+var ErrUnexpectedStatusCode = errors.New("unexpected status code")
 
 // DefaultPasswordValidator implements Validator. It is based on best
 // practices as defined in the following blog posts:
@@ -104,18 +106,12 @@ func (s *DefaultPasswordValidator) fetch(hpw []byte) error {
 	loc := fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", prefix)
 	res, err := s.c.Get(loc)
 	if err != nil {
-		if s.ignoreNetworkErrors {
-			return nil
-		}
-		return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to check if password has been breached before: %s", err))
+		return errors.Wrapf(ErrNetworkFailure, "%s", err)
 	}
 	defer res.Body.Close()
 
 	if res.StatusCode != http.StatusOK {
-		if s.ignoreNetworkErrors {
-			return nil
-		}
-		return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to check if password has been breached before, expected status code 200 but got %d", res.StatusCode))
+		return errors.Wrapf(ErrUnexpectedStatusCode, "%d", res.StatusCode)
 	}
 
 	s.Lock()
@@ -172,7 +168,10 @@ func (s *DefaultPasswordValidator) Validate(identifier, password string) error {
 	s.RUnlock()
 
 	if !ok {
-		if err := s.fetch(hpw); err != nil {
+		err := s.fetch(hpw)
+		if (errors.Is(err, ErrNetworkFailure) || errors.Is(err, ErrUnexpectedStatusCode)) && s.ignoreNetworkErrors {
+			return nil
+		} else if err != nil {
 			return err
 		}
 
diff --git a/selfservice/strategy/recoverytoken/strategy.go b/selfservice/strategy/recoverytoken/strategy.go
index e46ac27c6..f4d84fce4 100644
--- a/selfservice/strategy/recoverytoken/strategy.go
+++ b/selfservice/strategy/recoverytoken/strategy.go
@@ -10,10 +10,11 @@ import (
 	"github.com/julienschmidt/httprouter"
 	"github.com/pkg/errors"
 
+	"github.com/ory/x/decoderx"
+
 	"github.com/ory/x/sqlxx"
 
 	"github.com/ory/herodot"
-	"github.com/ory/x/randx"
 	"github.com/ory/x/sqlcon"
 	"github.com/ory/x/urlx"
 
@@ -32,10 +33,13 @@ import (
 )
 
 const (
-	PublicRecoveryTokenPath = "/self-service/browser/flows/recovery/link" // #nosec G101
+	PublicPath = "/self-service/browser/flows/recovery/link" // #nosec G101
+	AdminPath  = "/recovery/link"
 )
 
 var _ recovery.Strategy = new(Strategy)
+var _ recovery.AdminHandler = new(Strategy)
+var _ recovery.PublicHandler = new(Strategy)
 
 type (
 	// swagger:model strategyRecoveryLinkMethodConfig
@@ -70,28 +74,33 @@ type (
 	}
 
 	Strategy struct {
-		c configuration.Provider
-		d strategyDependencies
+		c  configuration.Provider
+		d  strategyDependencies
+		dx *decoderx.HTTP
 	}
 )
 
 func NewStrategy(d strategyDependencies, c configuration.Provider) *Strategy {
-	return &Strategy{c: c, d: d}
+	return &Strategy{c: c, d: d, dx: decoderx.NewHTTP()}
 }
 
 func (s *Strategy) RecoveryStrategyID() string {
 	return recovery.StrategyRecoveryTokenName
 }
 
-func (s *Strategy) RegisterRecoveryRoutes(public *x.RouterPublic) {
+func (s *Strategy) RegisterPublicRecoveryRoutes(public *x.RouterPublic) {
 	redirect := session.RedirectOnAuthenticated(s.c)
-	public.GET(PublicRecoveryTokenPath, s.d.SessionHandler().IsNotAuthenticated(s.handleSubmit, redirect))
-	public.POST(PublicRecoveryTokenPath, s.d.SessionHandler().IsNotAuthenticated(s.handleSubmit, redirect))
+	public.GET(PublicPath, s.d.SessionHandler().IsNotAuthenticated(s.handleSubmit, redirect))
+	public.POST(PublicPath, s.d.SessionHandler().IsNotAuthenticated(s.handleSubmit, redirect))
+}
+
+func (s *Strategy) RegisterAdminRecoveryRoutes(admin *x.RouterAdmin) {
+	admin.POST(AdminPath, s.createRecoveryLink)
 }
 
 func (s *Strategy) PopulateRecoveryMethod(r *http.Request, req *recovery.Request) error {
 	f := form.NewHTMLForm(urlx.CopyWithQuery(
-		urlx.AppendPaths(s.c.SelfPublicURL(), PublicRecoveryTokenPath),
+		urlx.AppendPaths(s.c.SelfPublicURL(), PublicPath),
 		url.Values{"request": {req.ID.String()}},
 	).String())
 
@@ -105,19 +114,165 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, req *recovery.Request
 	return nil
 }
 
-// swagger:model completeSelfServiceBrowserRecoveryLinkStrategyFlowPayload
+// swagger:parameters createRecoveryLink
+//
+// nolint
+type createRecoveryLinkParams struct {
+	// in: body
+	Body struct {
+		// Identity to Recover
+		//
+		// The identity's ID you wish to recover.
+		//
+		// required: true
+		IdentityID uuid.UUID `json:"identity_id"`
+
+		// Link Expires In
+		//
+		// The recovery link will expire at that point in time. Defaults to the configuration value of
+		// `selfservice.flows.recovery.request_lifespan`.
+		//
+		//
+		// pattern: ^[0-9]+(ns|us|ms|s|m|h)$
+		// example:
+		//	- 1h
+		//	- 1m
+		//	- 1s
+		ExpiresIn string `json:"expires_in"`
+	}
+}
+
+// swagger:model createRecoveryLinkResponse
 //
 // nolint
-type completeSelfServiceBrowserRecoveryLinkStrategyFlowPayload struct {
-	// Email
+type createRecoveryLinkResponse struct {
+	// Recovery Link
+	//
+	// This link can be used to recover the account.
+	//
+	// required: true
+	// format: uri
+	RecoveryLink string `json:"recovery_link"`
+
+	// Recovery Link Expires At
+	//
+	// The timestamp when the recovery link expires.
+	ExpiresAt time.Time `json:"expires_at"`
+}
+
+// swagger:route POST /recovery/link admin createRecoveryLink
+//
+// Create a Recovery Link
+//
+// This endpoint creates a recovery link which should be given to the user in order for them to recover
+// (or activate) their account.
+//
+//     Consumes:
+//     - application/json
+//
+//     Produces:
+//     - application/json
+//
+//     Schemes: http, https
+//
+//     Responses:
+//       200: createRecoveryLinkResponse
+//       404: genericError
+//       400: genericError
+//       500: genericError
+func (s *Strategy) createRecoveryLink(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+	var p createRecoveryLinkParams
+	if err := s.dx.Decode(r, &p.Body, decoderx.HTTPJSONDecoder()); err != nil {
+		s.d.Writer().WriteError(w, r, err)
+		return
+	}
+
+	expiresIn := s.c.SelfServiceFlowRecoveryRequestLifespan()
+	if len(p.Body.ExpiresIn) > 0 {
+		var err error
+		expiresIn, err = time.ParseDuration(p.Body.ExpiresIn)
+		if err != nil {
+			s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to parse "expires_in" whose format should match "[0-9]+(ns|us|ms|s|m|h)" but did not: %s`, p.Body.ExpiresIn)))
+			return
+		}
+	}
+
+	if time.Now().Add(expiresIn).Before(time.Now()) {
+		s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must be result to a future time: %s`, p.Body.ExpiresIn)))
+		return
+	}
+
+	req, err := recovery.NewRequest(expiresIn, "", r, nil)
+	if err != nil {
+		s.d.Writer().WriteError(w, r, err)
+		return
+	}
+
+	if err := s.d.RecoveryRequestPersister().CreateRecoveryRequest(r.Context(), req); err != nil {
+		s.d.Writer().WriteError(w, r, err)
+		return
+	}
+
+	id, err := s.d.IdentityPool().GetIdentity(r.Context(), p.Body.IdentityID)
+	if err != nil {
+		s.d.Writer().WriteError(w, r, err)
+		return
+	}
+
+	if len(id.RecoveryAddresses) == 0 {
+		s.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The identity does not have any recovery addresses set.")))
+		return
+	}
+
+	address := id.RecoveryAddresses[0]
+	token := NewToken(&address, req)
+	if err := s.d.RecoveryTokenPersister().CreateRecoveryToken(r.Context(), token); err != nil {
+		s.d.Writer().WriteError(w, r, err)
+		return
+	}
+
+	s.d.Audit().
+		WithField("via", address.Via).
+		WithField("identity_id", address.IdentityID).
+		WithSensitiveField("email_address", address.Value).
+		WithSensitiveField("recovery_link_token", token).
+		Info("A recovery link has been created.")
+
+	s.d.Writer().Write(w, r, &createRecoveryLinkResponse{
+		ExpiresAt: req.ExpiresAt.UTC(),
+		RecoveryLink: urlx.CopyWithQuery(
+			urlx.AppendPaths(s.c.SelfPublicURL(), PublicPath),
+			url.Values{
+				"token":   {token.Token},
+				"request": {req.ID.String()},
+			}).String()})
+}
+
+// swagger:parameters completeSelfServiceBrowserRecoveryLinkStrategyFlow
+//
+// nolint
+type completeSelfServiceBrowserRecoveryLinkStrategyFlowParams struct {
+	// Email to Recover
+	//
+	// Needs to be set when initiating the flow. If the email is a registered
+	// recovery email, a recovery link will be sent. If the email is not known,
+	// a email with details on what happened will be sent instead.
 	//
 	// in: body
 	Email string `json:"email"`
 
-	// RequestID is request ID.
+	// Recovery Token
+	//
+	// The recovery token which completes the recovery request. If the token
+	// is invalid (e.g. expired) an error will be shown to the end-user.
+	//
+	// in: query
+	Token string `json:"token"`
+
+	// Recovery Request ID
 	//
 	// in: query
-	RequestID string `json:"request_id"`
+	Request string `json:"request"`
 }
 
 // swagger:route POST /self-service/browser/flows/recovery/link public completeSelfServiceBrowserRecoveryLinkStrategyFlow
@@ -129,7 +284,6 @@ type completeSelfServiceBrowserRecoveryLinkStrategyFlowPayload struct {
 // More information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).
 //
 //     Consumes:
-//     - application/json
 //     - application/x-www-form-urlencoded
 //
 //     Schemes: http, https
@@ -262,10 +416,10 @@ func (s *Strategy) issueAndSendRecoveryToken(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	s.d.Logger().
+	s.d.Audit().
 		WithField("via", identity.RecoveryAddressTypeEmail).
 		WithSensitiveField("address", email).
-		Debug("Preparing account recovery token.")
+		Info("Preparing account recovery token.")
 
 	a, err := s.d.IdentityPool().FindRecoveryAddressByValue(r.Context(), identity.RecoveryAddressTypeEmail, email)
 	if err != nil {
@@ -305,10 +459,10 @@ func (s *Strategy) issueAndSendRecoveryToken(w http.ResponseWriter, r *http.Requ
 }
 
 func (s *Strategy) sendToUnknownAddress(ctx context.Context, address string) error {
-	s.d.Logger().
+	s.d.Audit().
 		WithField("via", identity.RecoveryAddressTypeEmail).
 		WithSensitiveField("email_address", address).
-		Debug("Sending out stub recovery email because address is not linked to any account.")
+		Info("Sending out stub recovery email because address is not linked to any account.")
 	return s.run(identity.RecoveryAddressTypeEmail, func() error {
 		_, err := s.d.Courier().QueueEmail(ctx,
 			templates.NewRecoveryInvalid(s.c, &templates.RecoveryInvalidModel{To: address}))
@@ -317,22 +471,23 @@ func (s *Strategy) sendToUnknownAddress(ctx context.Context, address string) err
 }
 
 func (s *Strategy) sendCodeToKnownAddress(ctx context.Context, req *recovery.Request, address *identity.RecoveryAddress) error {
-	token := randx.MustString(32, randx.AlphaNum)
-	if err := s.d.RecoveryTokenPersister().CreateRecoveryToken(ctx, NewToken(token, address, req)); err != nil {
+	token := NewToken(address, req)
+	if err := s.d.RecoveryTokenPersister().CreateRecoveryToken(ctx, token); err != nil {
 		return err
 	}
 
-	s.d.Logger().
+	s.d.Audit().
 		WithField("via", address.Via).
 		WithField("identity_id", address.IdentityID).
+		WithField("recovery_link_id", token.ID).
 		WithSensitiveField("email_address", address.Value).
-		WithSensitiveField("token", token).
-		Debug("Sending out recovery email with recovery link.")
+		WithSensitiveField("recovery_link_token", token.Token).
+		Info("Sending out recovery email with recovery link.")
 	return s.run(address.Via, func() error {
 		_, err := s.d.Courier().QueueEmail(ctx, templates.NewRecoveryValid(s.c,
 			&templates.RecoveryValidModel{To: address.Value, RecoveryURL: urlx.CopyWithQuery(
-				urlx.AppendPaths(s.c.SelfPublicURL(), PublicRecoveryTokenPath),
-				url.Values{"token": {token}}).String()}))
+				urlx.AppendPaths(s.c.SelfPublicURL(), PublicPath),
+				url.Values{"token": {token.Token}}).String()}))
 		return err
 	})
 }
diff --git a/selfservice/strategy/recoverytoken/token.go b/selfservice/strategy/recoverytoken/token.go
index 9cbae9cbe..3e16a1d7c 100644
--- a/selfservice/strategy/recoverytoken/token.go
+++ b/selfservice/strategy/recoverytoken/token.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/gofrs/uuid"
 
+	"github.com/ory/x/randx"
+
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/x"
@@ -41,10 +43,10 @@ func (Token) TableName() string {
 	return "identity_recovery_tokens"
 }
 
-func NewToken(token string, ra *identity.RecoveryAddress, req *recovery.Request) *Token {
+func NewToken(ra *identity.RecoveryAddress, req *recovery.Request) *Token {
 	return &Token{
 		ID:              x.NewUUID(),
-		Token:           token,
+		Token:           randx.MustString(32, randx.AlphaNum),
 		RecoveryAddress: ra,
 		Request:         req,
 	}
diff --git a/session/manager_http.go b/session/manager_http.go
index e938650df..cdb85f3bb 100644
--- a/session/manager_http.go
+++ b/session/manager_http.go
@@ -23,7 +23,7 @@ type (
 		x.CSRFProvider
 	}
 	managerHTTPConfiguration interface {
-		SessionLifespan() time.Duration
+		SessionLifespan() *time.Duration
 		SecretsSession() [][]byte
 		SessionSameSiteMode() http.SameSite
 		SessionDomain() string
@@ -71,6 +71,11 @@ func (s *ManagerHTTP) SaveToRequest(ctx context.Context, w http.ResponseWriter,
 	if s.c.SessionSameSiteMode() != 0 {
 		cookie.Options.SameSite = s.c.SessionSameSiteMode()
 	}
+
+	if s.c.SessionLifespan() != nil {
+		cookie.Options.MaxAge = int(s.c.SessionLifespan().Seconds())
+	}
+
 	cookie.Values["sid"] = session.ID.String()
 	if err := cookie.Save(r, w); err != nil {
 		return errors.WithStack(err)
diff --git a/session/session.go b/session/session.go
index f804adf2d..2658c4382 100644
--- a/session/session.go
+++ b/session/session.go
@@ -39,11 +39,16 @@ func (s Session) TableName() string {
 }
 
 func NewSession(i *identity.Identity, c interface {
-	SessionLifespan() time.Duration
+	SessionLifespan() *time.Duration
 }, authenticatedAt time.Time) *Session {
+	ttl := time.Hour * 6
+	if c.SessionLifespan() != nil {
+		ttl = *c.SessionLifespan()
+	}
+
 	return &Session{
 		ID:              x.NewUUID(),
-		ExpiresAt:       authenticatedAt.Add(c.SessionLifespan()),
+		ExpiresAt:       authenticatedAt.Add(ttl),
 		AuthenticatedAt: authenticatedAt,
 		IssuedAt:        time.Now().UTC(),
 		Identity:        i,
diff --git a/x/nocache.go b/x/nocache.go
new file mode 100644
index 000000000..324e80c8e
--- /dev/null
+++ b/x/nocache.go
@@ -0,0 +1,20 @@
+package x
+
+import (
+	"net/http"
+
+	"github.com/julienschmidt/httprouter"
+)
+
+// NoCache adds `Cache-Control: 0` to the response header.
+func NoCache(w http.ResponseWriter) {
+	w.Header().Set("Cache-Control", "0")
+}
+
+// NoCacheHandler wraps httprouter.Handle with `Cache-Control: 0` headers.
+func NoCacheHandler(handle httprouter.Handle) httprouter.Handle {
+	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+		NoCache(w)
+		handle(w, r, ps)
+	}
+}
diff --git a/x/pagination.go b/x/pagination.go
new file mode 100644
index 000000000..f8234a6a0
--- /dev/null
+++ b/x/pagination.go
@@ -0,0 +1,113 @@
+package x
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+const paginationMaxItems = 1000
+const paginationDefaultItems = 250
+
+// ParsePagination parses limit and page from *http.Request with given limits and defaults.
+func ParsePagination(r *http.Request) (page, itemsPerPage int) {
+	if offsetParam := r.URL.Query().Get("page"); offsetParam == "" {
+		page = 0
+	} else {
+		if offset64, err := strconv.ParseInt(offsetParam, 10, 64); err != nil {
+			page = 0
+		} else {
+			page = int(offset64)
+		}
+	}
+
+	if limitParam := r.URL.Query().Get("per_page"); limitParam == "" {
+		itemsPerPage = paginationDefaultItems
+	} else {
+		if limit64, err := strconv.ParseInt(limitParam, 10, 64); err != nil {
+			itemsPerPage = paginationDefaultItems
+		} else {
+			itemsPerPage = int(limit64)
+		}
+	}
+
+	if itemsPerPage > paginationMaxItems {
+		itemsPerPage = paginationMaxItems
+	}
+
+	if itemsPerPage < 1 {
+		itemsPerPage = 1
+	}
+
+	if page < 0 {
+		page = 0
+	}
+
+	return
+}
+
+func header(u *url.URL, rel string, limit, page int64) string {
+	q := u.Query()
+	q.Set("per_page", fmt.Sprintf("%d", limit))
+	q.Set("page", fmt.Sprintf("%d", page/limit))
+	u.RawQuery = q.Encode()
+	return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel)
+}
+
+func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) {
+	if itemsPerPage <= 0 {
+		itemsPerPage = 1
+	}
+
+	itemsPerPage64 := int64(itemsPerPage)
+	offset := int64(page) * itemsPerPage64
+
+	// lastOffset will either equal the offset required to contain the remainder,
+	// or the limit.
+	var lastOffset int64
+	if total%itemsPerPage64 == 0 {
+		lastOffset = total - itemsPerPage64
+	} else {
+		lastOffset = (total / itemsPerPage64) * itemsPerPage64
+	}
+
+	// Check for last page
+	if offset >= lastOffset {
+		if total == 0 {
+			w.Header().Set("Link", strings.Join([]string{
+				header(u, "first", itemsPerPage64, 0),
+				header(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64),
+				header(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64),
+			}, ","))
+			return
+		}
+
+		if total < itemsPerPage64 {
+			w.Header().Set("link", header(u, "first", total, 0))
+			return
+		}
+
+		w.Header().Set("Link", strings.Join([]string{
+			header(u, "first", itemsPerPage64, 0),
+			header(u, "prev", itemsPerPage64, lastOffset-itemsPerPage64),
+		}, ","))
+		return
+	}
+
+	if offset < itemsPerPage64 {
+		w.Header().Set("Link", strings.Join([]string{
+			header(u, "next", itemsPerPage64, itemsPerPage64),
+			header(u, "last", itemsPerPage64, lastOffset),
+		}, ","))
+		return
+	}
+
+	w.Header().Set("Link", strings.Join([]string{
+		header(u, "first", itemsPerPage64, 0),
+		header(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64),
+		header(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64),
+		header(u, "last", itemsPerPage64, lastOffset),
+	}, ","))
+}
diff --git a/x/router.go b/x/router.go
index 5f0fb5ff1..c871d4dbb 100644
--- a/x/router.go
+++ b/x/router.go
@@ -21,3 +21,33 @@ func NewRouterAdmin() *RouterAdmin {
 		Router: httprouter.New(),
 	}
 }
+
+// GET is a shortcut for router.Handle("GET", path, handle)
+func (r *RouterPublic) GET(path string, handle httprouter.Handle) {
+	r.Handle("GET", path, NoCacheHandler(handle))
+}
+
+// HEAD is a shortcut for router.Handle("HEAD", path, handle)
+func (r *RouterPublic) HEAD(path string, handle httprouter.Handle) {
+	r.Handle("HEAD", path, NoCacheHandler(handle))
+}
+
+// POST is a shortcut for router.Handle("POST", path, handle)
+func (r *RouterPublic) POST(path string, handle httprouter.Handle) {
+	r.Handle("POST", path, NoCacheHandler(handle))
+}
+
+// PUT is a shortcut for router.Handle("PUT", path, handle)
+func (r *RouterPublic) PUT(path string, handle httprouter.Handle) {
+	r.Handle("PUT", path, NoCacheHandler(handle))
+}
+
+// PATCH is a shortcut for router.Handle("PATCH", path, handle)
+func (r *RouterPublic) PATCH(path string, handle httprouter.Handle) {
+	r.Handle("PATCH", path, NoCacheHandler(handle))
+}
+
+// DELETE is a shortcut for router.Handle("DELETE", path, handle)
+func (r *RouterPublic) DELETE(path string, handle httprouter.Handle) {
+	r.Handle("DELETE", path, NoCacheHandler(handle))
+}


Test patch:
diff --git a/driver/configuration/provider_viper_test.go b/driver/configuration/provider_viper_test.go
index bdad91ba8..a84c57edb 100644
--- a/driver/configuration/provider_viper_test.go
+++ b/driver/configuration/provider_viper_test.go
@@ -79,6 +79,10 @@ func TestViperProvider(t *testing.T) {
 
 			assert.Equal(t, "https://self-service/settings/password/return_to", p.SelfServiceFlowSettingsReturnTo("password", p.SelfServiceBrowserDefaultReturnTo()).String())
 			assert.Equal(t, "https://self-service/settings/return_to", p.SelfServiceFlowSettingsReturnTo("profile", p.SelfServiceBrowserDefaultReturnTo()).String())
+
+			assert.Equal(t, "http://test.kratos.ory.sh:4000/", p.SelfServiceFlowLogoutRedirectURL().String())
+			viper.Set(configuration.ViperKeySelfServiceLogoutBrowserDefaultReturnTo, "")
+			assert.Equal(t, "http://return-to-3-test.ory.sh/", p.SelfServiceFlowLogoutRedirectURL().String())
 		})
 
 		t.Run("group=identity", func(t *testing.T) {
diff --git a/driver/driver_default_test.go b/driver/driver_default_test.go
index 878449fac..90b89ad15 100644
--- a/driver/driver_default_test.go
+++ b/driver/driver_default_test.go
@@ -1,10 +1,15 @@
 package driver_test
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ory/viper"
+	"github.com/ory/x/logrusx"
 
 	driver "github.com/ory/kratos/driver"
 	"github.com/ory/kratos/driver/configuration"
@@ -27,3 +32,12 @@ func TestDriverDefault_SQLiteMemoryMode(t *testing.T) {
 		}
 	})
 }
+
+func TestDriverNew(t *testing.T) {
+	viper.Set("dsn", "memory")
+	d, err := driver.NewDefaultDriver(logrusx.New("", ""),
+		"", "", "", true)
+	require.NoError(t, err)
+	assert.EqualValues(t, configuration.DefaultSQLiteMemoryDSN, d.Configuration().DSN())
+	require.NoError(t, d.Registry().Persister().Ping(context.Background()))
+}
diff --git a/identity/handler_test.go b/identity/handler_test.go
index 9be178902..43ebf3646 100644
--- a/identity/handler_test.go
+++ b/identity/handler_test.go
@@ -234,10 +234,30 @@ func TestHandler(t *testing.T) {
 		assert.EqualValues(t, "ory street", res.Get("traits.address").String(), "%s", res.Raw)
 	})
 
+	t.Run("case=should be able to update multiple identities", func(t *testing.T) {
+		for i := 0; i <= 5; i++ {
+			var cr identity.CreateIdentityRequestPayload
+			cr.SchemaID = "employee"
+			cr.Traits = []byte(`{"department": "ory"}`)
+			res := send(t, "POST", "/identities", http.StatusCreated, &cr)
+
+			id := res.Get("id").String()
+			res = send(t, "PUT", "/identities/"+id, http.StatusOK, &identity.UpdateIdentityRequestPayload{
+				SchemaID: "employee",
+				Traits:   []byte(`{"email":"` + x.NewUUID().String() + `@ory.sh"}`),
+			})
+
+			res = send(t, "PUT", "/identities/"+id, http.StatusOK, &identity.UpdateIdentityRequestPayload{
+				SchemaID: "employee",
+				Traits:   []byte(`{}`),
+			})
+		}
+	})
+
 	t.Run("case=should list all identities", func(t *testing.T) {
 		res := get(t, "/identities", http.StatusOK)
 		assert.Empty(t, res.Get("0.credentials").String(), "%s", res.Raw)
-		assert.EqualValues(t, "baz", res.Get("0.traits.bar").String(), "%s", res.Raw)
+		assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw)
 	})
 
 	t.Run("case=should not be able to update an identity that does not exist yet", func(t *testing.T) {
diff --git a/internal/testhelpers/json.go b/internal/testhelpers/json.go
index a0a59ce4a..dc1b8d885 100644
--- a/internal/testhelpers/json.go
+++ b/internal/testhelpers/json.go
@@ -15,3 +15,9 @@ func JSONEq(t *testing.T, expected, actual interface{}, messageAndArgs ...interf
 	require.NoError(t, json.NewEncoder(&ab).Encode(actual))
 	assert.JSONEq(t, eb.String(), ab.String(), messageAndArgs...)
 }
+
+func LogJSON(t *testing.T, v interface{}) {
+	out, err := json.MarshalIndent(v, "", "  ")
+	require.NoError(t, err)
+	t.Logf("\n%s\n---", out)
+}
diff --git a/internal/testhelpers/sql.go b/internal/testhelpers/sql.go
index 6e6c96781..3f9f1c46e 100644
--- a/internal/testhelpers/sql.go
+++ b/internal/testhelpers/sql.go
@@ -14,7 +14,7 @@ import (
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/selfservice/flow/registration"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/selfservice/strategy/recoverytoken"
 	"github.com/ory/kratos/session"
 )
@@ -32,7 +32,7 @@ func CleanSQL(t *testing.T, c *pop.Connection) {
 		new(recoverytoken.Token).TableName(),
 		new(recovery.RequestMethods).TableName(),
 		new(recovery.Request).TableName(),
-		new(verify.Request).TableName(),
+		new(verification.Request).TableName(),
 		new(session.Session).TableName(),
 		new(identity.CredentialIdentifierCollection).TableName(),
 		new(identity.CredentialsCollection).TableName(),
diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go
index 26ae8a917..4e3468713 100644
--- a/persistence/sql/migratest/migration_test.go
+++ b/persistence/sql/migratest/migration_test.go
@@ -114,7 +114,7 @@ func TestMigrations(t *testing.T) {
 
 			t.Run("suite=fixtures", func(t *testing.T) {
 				t.Run("case=identity", func(t *testing.T) {
-					ids, err := d.Registry().PrivilegedIdentityPool().ListIdentities(context.Background(), 100, 0)
+					ids, err := d.Registry().PrivilegedIdentityPool().ListIdentities(context.Background(), 0, 1000)
 					require.NoError(t, err)
 
 					for _, id := range ids {
diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go
index a75d9c766..6063bce3a 100644
--- a/persistence/sql/persister_test.go
+++ b/persistence/sql/persister_test.go
@@ -36,7 +36,7 @@ import (
 	"github.com/ory/kratos/selfservice/flow/login"
 	"github.com/ory/kratos/selfservice/flow/registration"
 	"github.com/ory/kratos/selfservice/flow/settings"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/session"
 )
 
@@ -162,7 +162,7 @@ func TestPersister(t *testing.T) {
 			})
 			t.Run("contract=verification.TestPersister", func(t *testing.T) {
 				pop.SetLogger(pl(t))
-				verify.TestPersister(p)(t)
+				verification.TestPersister(p)(t)
 			})
 			t.Run("contract=recovery.TestRequestPersister", func(t *testing.T) {
 				pop.SetLogger(pl(t))
diff --git a/selfservice/flow/verify/handler_test.go b/selfservice/flow/verification/handler_test.go
similarity index 94%
rename from selfservice/flow/verify/handler_test.go
rename to selfservice/flow/verification/handler_test.go
index c71aacb67..9a578451a 100644
--- a/selfservice/flow/verify/handler_test.go
+++ b/selfservice/flow/verification/handler_test.go
@@ -1,4 +1,4 @@
-package verify_test
+package verification_test
 
 import (
 	"context"
@@ -27,7 +27,7 @@ import (
 	"github.com/ory/kratos/internal/httpclient/client"
 	"github.com/ory/kratos/internal/httpclient/client/common"
 	"github.com/ory/kratos/internal/testhelpers"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 	"github.com/ory/kratos/x"
 )
 
@@ -90,12 +90,12 @@ func TestHandler(t *testing.T) {
 	t.Run("case=request verification for unknown via", func(t *testing.T) {
 		res, body := x.EasyGet(t,
 			&http.Client{Jar: x.EasyCookieJar(t, nil)},
-			publicTS.URL+strings.Replace(verify.PublicVerificationInitPath, ":via", "notemail", 1))
+			publicTS.URL+strings.Replace(verification.PublicVerificationInitPath, ":via", "notemail", 1))
 		assert.Contains(t, res.Request.URL.String(), errTS.URL)
 		assert.EqualValues(t, http.StatusBadRequest, gjson.GetBytes(body, "0.code").Int())
 	})
 
-	initURL := publicTS.URL + strings.Replace(verify.PublicVerificationInitPath, ":via", "email", 1)
+	initURL := publicTS.URL + strings.Replace(verification.PublicVerificationInitPath, ":via", "email", 1)
 
 	t.Run("case=init and validate request payload", func(t *testing.T) {
 		hc := &http.Client{Jar: x.EasyCookieJar(t, nil)}
@@ -113,7 +113,7 @@ func TestHandler(t *testing.T) {
 		assert.True(t, time.Time(svr.Payload.ExpiresAt).After(time.Now()))
 		assert.Contains(t, svr.Payload.RequestURL, initURL)
 		assert.Contains(t, svr.Payload.ID, rid)
-		assert.Equal(t, publicTS.URL+strings.Replace(verify.PublicVerificationCompletePath, ":via", "email", 1)+"?request="+rid, *svr.Payload.Form.Action)
+		assert.Equal(t, publicTS.URL+strings.Replace(verification.PublicVerificationCompletePath, ":via", "email", 1)+"?request="+rid, *svr.Payload.Form.Action)
 		assert.Contains(t, "csrf_token", *svr.Payload.Form.Fields[0].Name)
 		assert.Contains(t, "to_verify", *svr.Payload.Form.Fields[1].Name)
 		assert.Contains(t, "email", *svr.Payload.Form.Fields[1].Type)
@@ -215,7 +215,7 @@ func TestHandler(t *testing.T) {
 		hc := &http.Client{Jar: x.EasyCookieJar(t, nil)}
 		res, _ := x.EasyGet(t, hc,
 			publicTS.URL+strings.ReplaceAll(
-				strings.ReplaceAll(verify.PublicVerificationConfirmPath, ":code", "unknown-code"),
+				strings.ReplaceAll(verification.PublicVerificationConfirmPath, ":code", "unknown-code"),
 				":via", "email"))
 		assert.Contains(t, res.Request.URL.String(), verifyTS.URL)
 
diff --git a/selfservice/flow/verify/request_test.go b/selfservice/flow/verification/request_test.go
similarity index 98%
rename from selfservice/flow/verify/request_test.go
rename to selfservice/flow/verification/request_test.go
index 2b62f7c41..b265bd64e 100644
--- a/selfservice/flow/verify/request_test.go
+++ b/selfservice/flow/verification/request_test.go
@@ -1,4 +1,4 @@
-package verify
+package verification
 
 import (
 	"crypto/tls"
diff --git a/selfservice/flow/verify/sender_test.go b/selfservice/flow/verification/sender_test.go
similarity index 92%
rename from selfservice/flow/verify/sender_test.go
rename to selfservice/flow/verification/sender_test.go
index dd6a94258..20c6cb4f3 100644
--- a/selfservice/flow/verify/sender_test.go
+++ b/selfservice/flow/verification/sender_test.go
@@ -1,4 +1,4 @@
-package verify_test
+package verification_test
 
 import (
 	"context"
@@ -13,7 +13,7 @@ import (
 	"github.com/ory/kratos/driver/configuration"
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/internal"
-	"github.com/ory/kratos/selfservice/flow/verify"
+	"github.com/ory/kratos/selfservice/flow/verification"
 )
 
 func TestManager(t *testing.T) {
@@ -36,7 +36,7 @@ func TestManager(t *testing.T) {
 		require.NoError(t, err)
 
 		_, err = reg.VerificationSender().SendCode(context.Background(), address.Via, "not-tracked@ory.sh")
-		require.EqualError(t, err, verify.ErrUnknownAddress.Error())
+		require.EqualError(t, err, verification.ErrUnknownAddress.Error())
 
 		messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
 		require.NoError(t, err)
diff --git a/selfservice/strategy/password/validator_test.go b/selfservice/strategy/password/validator_test.go
index f551added..d7a62970b 100644
--- a/selfservice/strategy/password/validator_test.go
+++ b/selfservice/strategy/password/validator_test.go
@@ -1,7 +1,11 @@
 package password
 
 import (
+	"bytes"
+	"errors"
 	"fmt"
+	"io/ioutil"
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -75,4 +79,156 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
 			}
 		})
 	}
+
+	fakeClient := NewFakeHTTPClient()
+	s.c = &fakeClient.Client
+
+	t.Run("case=should send request to pwnedpasswords.com", func(t *testing.T) {
+		s.ignoreNetworkErrors = false
+		s.Validate("mohutdesub", "damrumukuh")
+		require.Contains(t, fakeClient.RequestedURLs(), "https://api.pwnedpasswords.com/range/BCBA9")
+	})
+
+	t.Run("case=should fail if request fails and ignoreNetworkErrors is not set", func(t *testing.T) {
+		s.ignoreNetworkErrors = false
+		fakeClient.RespondWithError("Network request failed")
+		require.Error(t, s.Validate("", "sumdarmetp"))
+	})
+
+	t.Run("case=should not fail if request fails and ignoreNetworkErrors is set", func(t *testing.T) {
+		s.ignoreNetworkErrors = true
+		fakeClient.RespondWithError("Network request failed")
+		require.NoError(t, s.Validate("", "pepegtawni"))
+	})
+
+	t.Run("case=should fail if response has non 200 code and ignoreNetworkErrors is not set", func(t *testing.T) {
+		s.ignoreNetworkErrors = false
+		fakeClient.RespondWith(http.StatusForbidden, "")
+		require.Error(t, s.Validate("", "jolhakowef"))
+	})
+
+	t.Run("case=should not fail if response has non 200 code code and ignoreNetworkErrors is set", func(t *testing.T) {
+		s.ignoreNetworkErrors = true
+		fakeClient.RespondWith(http.StatusInternalServerError, "")
+		require.NoError(t, s.Validate("", "jenuzuhjoj"))
+	})
+
+	for _, tc := range []struct {
+		cs   string
+		pw   string
+		res  string
+		pass bool
+	}{
+		{
+			cs:   "contains invalid data",
+			pw:   "lufsokpugo",
+			res:  "0225BDB8F106B1B4A5DF4C31B80AC695874:2\ninvalid",
+			pass: false,
+		},
+		{
+			cs:   "contains invalid hash count",
+			pw:   "gimekvizec",
+			res:  "0248B3D6077106761CC84F4B9CF680C6D84:text\n1A34C526A9D14832C6ACFEAE90261ED78F8:2",
+			pass: false,
+		},
+		{
+			cs:   "is missing hash count",
+			pw:   "bofulosasm",
+			res:  "1D29CF237A57F6FEA8F29E8D907DCF1EBBA\n026364A8EE59DEDCF9E2DC80B9D7BAB7389:2",
+			pass: false,
+		},
+		{
+			cs:   "response contains no matches",
+			pw:   "lizrafakha",
+			res:  "0D6CF6289C9CA71B47D2167EB7FE89690E7:57",
+			pass: true,
+		},
+		{
+			cs:   "contains less than maxBreachesThreshold",
+			pw:   "tafpabdopa",
+			res:  fmt.Sprintf("280915F3B572F94217D86F1D63BED53F66A:%d\n0F76A7D21E7C3E653E98236897AD7888937:%d", s.maxBreachesThreshold, s.maxBreachesThreshold+1),
+			pass: true,
+		},
+		{
+			cs:   "contains more than maxBreachesThreshold",
+			pw:   "hicudsumla",
+			res:  fmt.Sprintf("5656812AA72561AAA6663E486A46D5711BE:%d", s.maxBreachesThreshold+1),
+			pass: false,
+		},
+	} {
+		fakeClient.RespondWith(http.StatusOK, tc.res)
+		format := "case=shuold not fail if response %s"
+		if !tc.pass {
+			format = "case=shuold fail if response %s"
+		}
+		t.Run(fmt.Sprintf(format, tc.cs), func(t *testing.T) {
+			err := s.Validate("", tc.pw)
+			if tc.pass {
+				require.NoError(t, err)
+			} else {
+				require.Error(t, err)
+			}
+		})
+	}
+}
+
+type fakeHttpClient struct {
+	http.Client
+
+	requestedURLs []string
+	responder     func(*http.Request) (*http.Response, error)
+}
+
+func NewFakeHTTPClient() *fakeHttpClient {
+	client := fakeHttpClient{
+		responder: func(*http.Request) (*http.Response, error) {
+			return nil, errors.New("No responder defined in fake HTTP client")
+		},
+	}
+	client.Client = http.Client{
+		Transport: &fakeRoundTripper{&client},
+	}
+	return &client
+}
+
+func (c *fakeHttpClient) RespondWith(status int, body string) {
+	c.responder = func(request *http.Request) (*http.Response, error) {
+		buffer := bytes.NewBufferString(body)
+		return &http.Response{
+			StatusCode:    status,
+			Body:          ioutil.NopCloser(buffer),
+			ContentLength: int64(buffer.Len()),
+			Request:       request,
+		}, nil
+	}
+}
+
+func (c *fakeHttpClient) RespondWithError(err string) {
+	c.responder = func(*http.Request) (*http.Response, error) {
+		return nil, errors.New(err)
+	}
+}
+
+func (c *fakeHttpClient) Reset() {
+	c.requestedURLs = nil
+}
+
+func (c *fakeHttpClient) RequestedURLs() []string {
+	return c.requestedURLs
+}
+
+func (c *fakeHttpClient) handle(request *http.Request) (*http.Response, error) {
+	c.requestedURLs = append(c.requestedURLs, request.URL.String())
+	if request.Body != nil {
+		request.Body.Close()
+	}
+	return c.responder(request)
+}
+
+type fakeRoundTripper struct {
+	client *fakeHttpClient
+}
+
+func (rt *fakeRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
+	return rt.client.handle(request)
 }
diff --git a/selfservice/strategy/recoverytoken/strategy_test.go b/selfservice/strategy/recoverytoken/strategy_test.go
index 209c4f6e1..69fcb7535 100644
--- a/selfservice/strategy/recoverytoken/strategy_test.go
+++ b/selfservice/strategy/recoverytoken/strategy_test.go
@@ -10,6 +10,8 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/ory/x/urlx"
+
 	"github.com/ory/x/sqlxx"
 
 	"github.com/ory/x/assertx"
@@ -20,12 +22,14 @@ import (
 	"github.com/ory/kratos/driver/configuration"
 	"github.com/ory/kratos/identity"
 	"github.com/ory/kratos/internal"
+	"github.com/ory/kratos/internal/httpclient/client/admin"
 	"github.com/ory/kratos/internal/httpclient/client/common"
 	"github.com/ory/kratos/internal/httpclient/models"
 	"github.com/ory/kratos/internal/testhelpers"
 	"github.com/ory/kratos/selfservice/flow/recovery"
 	"github.com/ory/kratos/selfservice/strategy/recoverytoken"
 	"github.com/ory/kratos/text"
+	"github.com/ory/kratos/x"
 )
 
 func init() {
@@ -39,13 +43,104 @@ var identityToRecover = &identity.Identity{
 	SchemaID: configuration.DefaultIdentityTraitsSchemaID,
 }
 
-func TestStrategy(t *testing.T) {
-	conf, reg := internal.NewFastRegistryWithMocks(t)
+func initViper() {
 	viper.Set(configuration.ViperKeyDefaultIdentitySchemaURL, "file://./stub/default.schema.json")
 	viper.Set(configuration.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh")
 	viper.Set(configuration.ViperKeySelfServiceStrategyConfig+"."+identity.CredentialsTypePassword.String()+".enabled", true)
 	viper.Set(configuration.ViperKeySelfServiceStrategyConfig+"."+recovery.StrategyRecoveryTokenName+".enabled", true)
 	viper.Set(configuration.ViperKeySelfServiceRecoveryEnabled, true)
+}
+
+func TestAdminStrategy(t *testing.T) {
+	conf, reg := internal.NewFastRegistryWithMocks(t)
+	initViper()
+
+	_ = testhelpers.NewRecoveryUITestServer(t)
+	_ = testhelpers.NewLoginUIRequestEchoServer(t, reg)
+	_ = testhelpers.NewErrorTestServer(t, reg)
+
+	publicTS, adminTS := testhelpers.NewKratosServer(t, reg)
+	adminSDK := testhelpers.NewSDKClient(adminTS)
+
+	checkLink := func(t *testing.T, link *admin.CreateRecoveryLinkOK, isBefore time.Time) {
+		require.Contains(t, *link.Payload.RecoveryLink, publicTS.URL+recoverytoken.PublicPath)
+		rl := urlx.ParseOrPanic(*link.Payload.RecoveryLink)
+		assert.NotEmpty(t, rl.Query().Get("token"))
+		assert.NotEmpty(t, rl.Query().Get("request"))
+		require.True(t, time.Time(link.Payload.ExpiresAt).Before(isBefore))
+	}
+
+	t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) {
+		_, err := adminSDK.Admin.CreateRecoveryLink(admin.NewCreateRecoveryLinkParams().WithBody(
+			admin.CreateRecoveryLinkBody{IdentityID: models.UUID(x.NewUUID().String())}))
+		require.IsType(t, err, new(admin.CreateRecoveryLinkNotFound), "%T", err)
+	})
+
+	t.Run("description=should not be able to recover an account that does not have a recovery email", func(t *testing.T) {
+		id := identity.Identity{Traits: identity.Traits(`{}`)}
+		require.NoError(t, reg.IdentityManager().Create(context.Background(),
+			&id, identity.ManagerAllowWriteProtectedTraits))
+
+		_, err := adminSDK.Admin.CreateRecoveryLink(admin.NewCreateRecoveryLinkParams().WithBody(
+			admin.CreateRecoveryLinkBody{IdentityID: models.UUID(id.ID.String())}))
+		require.IsType(t, err, new(admin.CreateRecoveryLinkBadRequest), "%T", err)
+	})
+
+	t.Run("description=should create a valid recovery link and set the expiry time and not e able to recover the account", func(t *testing.T) {
+		id := identity.Identity{Traits: identity.Traits(`{"email":"recover.expired@ory.sh"}`)}
+
+		require.NoError(t, reg.IdentityManager().Create(context.Background(),
+			&id, identity.ManagerAllowWriteProtectedTraits))
+
+		link, err := adminSDK.Admin.CreateRecoveryLink(admin.NewCreateRecoveryLinkParams().
+			WithBody(admin.CreateRecoveryLinkBody{
+				IdentityID: models.UUID(id.ID.String()),
+				ExpiresIn:  "100ms",
+			}))
+		require.NoError(t, err)
+
+		time.Sleep(time.Millisecond * 100)
+		checkLink(t, link, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan()))
+		res, err := publicTS.Client().Get(*link.Payload.RecoveryLink)
+		require.NoError(t, err)
+
+		require.Equal(t, http.StatusNoContent, res.StatusCode)
+
+		// We end up here because the link is expired.
+		assert.Contains(t, res.Request.URL.Path, "/recover")
+	})
+
+	t.Run("description=should create a valid recovery link and set the expiry time as well and recover the account", func(t *testing.T) {
+		id := identity.Identity{Traits: identity.Traits(`{"email":"recover@ory.sh"}`)}
+
+		require.NoError(t, reg.IdentityManager().Create(context.Background(),
+			&id, identity.ManagerAllowWriteProtectedTraits))
+
+		link, err := adminSDK.Admin.CreateRecoveryLink(admin.NewCreateRecoveryLinkParams().
+			WithBody(admin.CreateRecoveryLinkBody{IdentityID: models.UUID(id.ID.String())}))
+		require.NoError(t, err)
+
+		checkLink(t, link, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan()+time.Second))
+		res, err := publicTS.Client().Get(*link.Payload.RecoveryLink)
+		require.NoError(t, err)
+
+		assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowSettingsUI().String())
+		assert.Equal(t, http.StatusAccepted, res.StatusCode)
+		testhelpers.LogJSON(t, link.Payload)
+
+		sr, err := adminSDK.Common.GetSelfServiceBrowserSettingsRequest(
+			common.NewGetSelfServiceBrowserSettingsRequestParams().
+				WithRequest(res.Request.URL.Query().Get("request")))
+		require.NoError(t, err)
+
+		require.Len(t, sr.Payload.Messages, 1)
+		assert.Equal(t, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.", sr.Payload.Messages[0].Text)
+	})
+}
+
+func TestStrategy(t *testing.T) {
+	conf, reg := internal.NewFastRegistryWithMocks(t)
+	initViper()
 
 	_ = testhelpers.NewRecoveryUITestServer(t)
 	_ = testhelpers.NewLoginUIRequestEchoServer(t, reg)
@@ -56,6 +151,7 @@ func TestStrategy(t *testing.T) {
 
 	require.NoError(t, reg.IdentityManager().Create(context.Background(), identityToRecover,
 		identity.ManagerAllowWriteProtectedTraits))
+
 	var csrfField = &models.FormField{Name: pointerx.String("csrf_token"), Required: true,
 		Type: pointerx.String("hidden"), Value: "nosurf"}
 
@@ -68,7 +164,7 @@ func TestStrategy(t *testing.T) {
 		assert.EqualValues(t, models.FormFields{csrfField,
 			{Name: pointerx.String("email"), Required: true, Type: pointerx.String("email")},
 		}, method.Config.Fields)
-		assert.EqualValues(t, public.URL+recoverytoken.PublicRecoveryTokenPath+"?request="+string(rs.Payload.ID), *method.Config.Action)
+		assert.EqualValues(t, public.URL+recoverytoken.PublicPath+"?request="+string(rs.Payload.ID), *method.Config.Action)
 		assert.Empty(t, method.Config.Messages)
 		assert.Empty(t, rs.Payload.Messages)
 	})
@@ -132,7 +228,7 @@ func TestStrategy(t *testing.T) {
 
 		recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1)
 
-		assert.Contains(t, recoveryLink, public.URL+recoverytoken.PublicRecoveryTokenPath)
+		assert.Contains(t, recoveryLink, public.URL+recoverytoken.PublicPath)
 		assert.Contains(t, recoveryLink, "token=")
 		res, err := c.Get(recoveryLink)
 		require.NoError(t, err)
@@ -152,7 +248,7 @@ func TestStrategy(t *testing.T) {
 
 	t.Run("description=should not be able to use an invalid link", func(t *testing.T) {
 		c := testhelpers.NewClientWithCookies(t)
-		res, err := c.Get(public.URL + recoverytoken.PublicRecoveryTokenPath + "?token=i-do-not-exist")
+		res, err := c.Get(public.URL + recoverytoken.PublicPath + "?token=i-do-not-exist")
 		require.NoError(t, err)
 
 		assert.Equal(t, http.StatusNoContent, res.StatusCode)
diff --git a/test/e2e/run.sh b/test/e2e/run.sh
index 2f0508a21..6511281fd 100755
--- a/test/e2e/run.sh
+++ b/test/e2e/run.sh
@@ -112,7 +112,7 @@ run() {
 
   yq merge test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.generated.yml
   ($kratos serve --dev -c test/e2e/kratos.generated.yml > "${base}/test/e2e/kratos.${profile}.e2e.log" 2>&1 &)
-  
+
   npm run wait-on -- -t 10000 http-get://127.0.0.1:4434/health/ready \
     http-get://127.0.0.1:4455/health \
     http-get://127.0.0.1:4445/health/ready \
@@ -189,10 +189,6 @@ case "$1" in
           db="${TEST_DATABASE_SQLITE}"
           ;;
 
-        memory)
-          db="${TEST_DATABASE_MEMORY}"
-          ;;
-
         mysql)
           db="${TEST_DATABASE_MYSQL}"
           ;;
diff --git a/x/pagination_test.go b/x/pagination_test.go
new file mode 100644
index 000000000..335dbad36
--- /dev/null
+++ b/x/pagination_test.go
@@ -0,0 +1,133 @@
+/*
+ * Copyright © 2017-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @author		Aeneas Rekkas <aeneas+oss@aeneas.io>
+ * @copyright 	2017-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
+ * @license 	Apache-2.0
+ */
+package x
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/ory/x/urlx"
+)
+
+func TestPaginationHeader(t *testing.T) {
+	u := urlx.ParseOrPanic("http://example.com")
+
+	t.Run("Create previous and first but not next or last if at the end", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 120, 2, 50)
+
+		expect := strings.Join([]string{
+			"<http://example.com?page=0&per_page=50>; rel=\"first\"",
+			"<http://example.com?page=1&per_page=50>; rel=\"prev\"",
+		}, ",")
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+
+	t.Run("Create next and last, but not previous or first if at the beginning", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 120, 0, 50)
+
+		expect := strings.Join([]string{
+			"<http://example.com?page=1&per_page=50>; rel=\"next\"",
+			"<http://example.com?page=2&per_page=50>; rel=\"last\"",
+		}, ",")
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+
+	t.Run("Create previous, next, first, and last if in the middle", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 300, 3, 50)
+
+		expect := strings.Join([]string{
+			"<http://example.com?page=0&per_page=50>; rel=\"first\"",
+			"<http://example.com?page=4&per_page=50>; rel=\"next\"",
+			"<http://example.com?page=2&per_page=50>; rel=\"prev\"",
+			"<http://example.com?page=5&per_page=50>; rel=\"last\"",
+		}, ",")
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+
+	t.Run("Header should default limit to 1 no limit was provided", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 100, 20, 0)
+
+		expect := strings.Join([]string{
+			"<http://example.com?page=0&per_page=1>; rel=\"first\"",
+			"<http://example.com?page=21&per_page=1>; rel=\"next\"",
+			"<http://example.com?page=19&per_page=1>; rel=\"prev\"",
+			"<http://example.com?page=99&per_page=1>; rel=\"last\"",
+		}, ",")
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+
+	t.Run("Create previous, next, first, but not last if in the middle and no total was provided", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 0, 3, 50)
+
+		expect := strings.Join([]string{
+			"<http://example.com?page=0&per_page=50>; rel=\"first\"",
+			"<http://example.com?page=4&per_page=50>; rel=\"next\"",
+			"<http://example.com?page=2&per_page=50>; rel=\"prev\"",
+		}, ",")
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+
+	t.Run("Create only first if the limits provided exceeds the number of clients found", func(t *testing.T) {
+		r := httptest.NewRecorder()
+		PaginationHeader(r, u, 5, 0, 50)
+
+		expect := "<http://example.com?page=0&per_page=5>; rel=\"first\""
+
+		assert.EqualValues(t, expect, r.Result().Header.Get("Link"))
+	})
+}
+
+func TestParsePagination(t *testing.T) {
+	for _, tc := range []struct {
+		d                    string
+		url                  string
+		expectedItemsPerPage int
+		expectedPage         int
+	}{
+		{"normal", "http://localhost/foo?per_page=10&page=10", 10, 10},
+		{"defaults", "http://localhost/foo", paginationDefaultItems, 0},
+		{"limits", "http://localhost/foo?per_page=2000", paginationMaxItems, 0},
+		{"negatives", "http://localhost/foo?per_page=-1&page=-1", 1, 0},
+		{"invalid_params", "http://localhost/foo?per_page=a&page=b", paginationDefaultItems, 0},
+	} {
+		t.Run(fmt.Sprintf("case=%s", tc.d), func(t *testing.T) {
+			u, _ := url.Parse(tc.url)
+			page, perPage := ParsePagination(&http.Request{URL: u})
+			assert.EqualValues(t, perPage, tc.expectedItemsPerPage, "per_page")
+			assert.EqualValues(t, page, tc.expectedPage, "page")
+		})
+	}
+}
diff --git a/x/router_test.go b/x/router_test.go
index c9f5a7f46..90803c3fa 100644
--- a/x/router_test.go
+++ b/x/router_test.go
@@ -1,12 +1,45 @@
 package x
 
 import (
+	"net/http"
 	"testing"
 
+	"github.com/gobuffalo/httptest"
+	"github.com/julienschmidt/httprouter"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-func TestNewRouterAdminAdmin(t *testing.T) {
+func TestNewRouterAdmin(t *testing.T) {
 	require.NotEmpty(t, NewRouterAdmin())
 	require.NotEmpty(t, NewRouterPublic())
 }
+
+func TestCacheHandling(t *testing.T) {
+	router := NewRouterPublic()
+	ts := httptest.NewServer(router)
+	t.Cleanup(ts.Close)
+
+	router.GET("/foo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		w.WriteHeader(http.StatusNoContent)
+	})
+	router.DELETE("/foo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		w.WriteHeader(http.StatusNoContent)
+	})
+	router.POST("/foo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		w.WriteHeader(http.StatusNoContent)
+	})
+	router.PUT("/foo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		w.WriteHeader(http.StatusNoContent)
+	})
+	router.PATCH("/foo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	for _, method := range []string{} {
+		req, _ := http.NewRequest(method, ts.URL+"/foo", nil)
+		res, err := ts.Client().Do(req)
+		require.NoError(t, err)
+		assert.EqualValues(t, "0", res.Header.Get("Cache-Control"))
+	}
+}