Repository: TBD54566975/ssi-service
Pull request #539 (2023-06-14 00:57:11)

Batch create for did:key dids.
# Overview
This includes the changes to support batch creation of did:key DIDs. 

# Description
Part of #347 . This PR also changes the endpoint names to be `batch` instead of `batchCreate`. 

# How Has This Been Tested?
Unit and integration tests. 



andresuribe87: > what's the use case for the batch creation of custodial DIDs?

An operator of SSI wanting to create DIDs for a list of users they already have. 
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#539](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (2556e0d) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/e11c94b0ed9fd9452d5c83d95c8cfde6e6aaff96?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (e11c94b) will **decrease** coverage by `0.29%`.
> The diff coverage is `17.85%`.

```diff
@@            Coverage Diff             @@
##             main     #539      +/-   ##
==========================================
- Coverage   24.21%   23.92%   -0.29%     
==========================================
  Files          46       46              
  Lines        5121     5245     +124     
==========================================
+ Hits         1240     1255      +15     
- Misses       3650     3758     +108     
- Partials      231      232       +1     
```


| [Impacted Files](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [config/config.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `26.72% <ø> (ø)` | |
| [integration/common.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-aW50ZWdyYXRpb24vY29tbW9uLmdv) | `1.57% <0.00%> (-0.06%)` | :arrow_down: |
| [pkg/server/router/credential.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvY3JlZGVudGlhbC5nbw==) | `2.93% <ø> (ø)` | |
| [pkg/server/router/did.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZGlkLmdv) | `3.44% <0.00%> (-0.93%)` | :arrow_down: |
| [pkg/service/did/ion.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2lvbi5nbw==) | `61.88% <ø> (ø)` | |
| [pkg/service/did/key.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2tleS5nbw==) | `0.00% <ø> (ø)` | |
| [pkg/service/did/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/service/did/web.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3dlYi5nbw==) | `0.00% <ø> (ø)` | |
| [pkg/service/keystore/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uva2V5c3RvcmUvc2VydmljZS5nbw==) | `42.38% <52.63%> (+1.16%)` | :arrow_up: |
| [pkg/service/did/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3N0b3JhZ2UuZ28=) | `56.12% <53.33%> (+0.64%)` | :arrow_up: |
| ... and [2 more](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/539?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | |



Patch:
diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go
index b79963e..890f54d 100644
--- a/cmd/ssiservice/main.go
+++ b/cmd/ssiservice/main.go
@@ -29,14 +29,14 @@ import (
 // main godoc
 //
 //	@title			SSI Service API
-//	@description	{{.SwaggerInfoDescription}}
+//	@description	{{.Desc}}
 //	@contact.name	TBD
 //	@contact.url	https://github.com/TBD54566975/ssi-service/issues
 //	@contact.email	tbd-developer@squareup.com
 //	@license.name	Apache 2.0
 //	@license.url	http://www.apache.org/licenses/LICENSE-2.0.html
-//	@host			{{.SwaggerInfoHost}}
-//	@version		{{.SwaggerInfoVersion}}
+//	@host			{{.Server.APIHost}}
+//	@version		{{.SVN}}
 func main() {
 	logrus.Info("Starting up...")
 
diff --git a/config/config.go b/config/config.go
index e63dfe0..ed709e8 100644
--- a/config/config.go
+++ b/config/config.go
@@ -121,6 +121,8 @@ type DIDServiceConfig struct {
 	UniversalResolverURL     string   `toml:"universal_resolver_url"`
 	UniversalResolverMethods []string `toml:"universal_resolver_methods"`
 	IONResolverURL           string   `toml:"ion_resolver_url"`
+	// BatchCreateMaxItems set's the maximum amount that can be.
+	BatchCreateMaxItems int `toml:"batch_create_max_items" conf:"default:100"`
 }
 
 func (d *DIDServiceConfig) IsEmpty() bool {
diff --git a/config/config.toml.example b/config/config.toml.example
index f8b03f0..b85c536 100644
--- a/config/config.toml.example
+++ b/config/config.toml.example
@@ -54,6 +54,7 @@ password = "default-password"
 name = "did"
 methods = ["key", "web"]
 local_resolution_methods = ["key", "web", "pkh", "peer"]
+batch_create_max_items = 100
 
 [services.schema]
 name = "schema"
diff --git a/config/dev.toml b/config/dev.toml
index ede113e..076c027 100644
--- a/config/dev.toml
+++ b/config/dev.toml
@@ -44,6 +44,7 @@ methods = ["key", "web"]
 local_resolution_methods = ["key", "web", "pkh", "peer"]
 universal_resolver_url = "https://dev.uniresolver.io/"
 universal_resolver_methods = ["ion"]
+batch_create_max_items = 100
 
 [services.schema]
 name = "schema"
diff --git a/config/prod.toml b/config/prod.toml
index 87d5345..ca831e7 100644
--- a/config/prod.toml
+++ b/config/prod.toml
@@ -49,6 +49,7 @@ local_resolution_methods = ["key", "web", "pkh", "peer"]
 universal_resolver_url = "http://uni-resolver-web:8080"
 universal_resolver_methods = ["ion"]
 ion_resolver_url = "https://ion.tbddev.org"
+batch_create_max_items = 100
 
 [services.schema]
 name = "schema"
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index e16c1c8..416727f 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -958,7 +958,7 @@ definitions:
     properties:
       requests:
         description: Required. The list of create credential requests. Cannot be more
-          than 1000 items.
+          than {{.Services.CredentialConfig.BatchCreateMaxItems}} items.
         items:
           $ref: '#/definitions/pkg_server_router.CreateCredentialRequest'
         type: array
@@ -973,6 +973,25 @@ definitions:
           $ref: '#/definitions/github_com_tbd54566975_ssi-service_internal_credential.Container'
         type: array
     type: object
+  pkg_server_router.BatchCreateDIDsRequest:
+    properties:
+      requests:
+        description: Required. The list of create credential requests. Cannot be more
+          than {{.Services.DIDConfig.BatchCreateMaxItems}} items.
+        items:
+          $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest'
+        type: array
+    required:
+    - requests
+    type: object
+  pkg_server_router.BatchCreateDIDsResponse:
+    properties:
+      dids:
+        description: The DID documents created.
+        items:
+          $ref: '#/definitions/did.Document'
+        type: array
+    type: object
   pkg_server_router.CreateCredentialRequest:
     properties:
       '@context':
@@ -2007,18 +2026,18 @@ definitions:
     - Second
     - Minute
     - Hour
-host: '{{.SwaggerInfoHost}}'
+host: '{{.Server.APIHost}}'
 info:
   contact:
     email: tbd-developer@squareup.com
     name: TBD
     url: https://github.com/TBD54566975/ssi-service/issues
-  description: '{{.SwaggerInfoDescription}}'
+  description: '{{.Desc}}'
   license:
     name: Apache 2.0
     url: http://www.apache.org/licenses/LICENSE-2.0.html
   title: SSI Service API
-  version: '{{.SwaggerInfoVersion}}'
+  version: '{{.SVN}}'
 paths:
   /health:
     get:
@@ -2235,7 +2254,7 @@ paths:
       summary: Update Credential Status
       tags:
       - CredentialAPI
-  /v1/credentials/batchCreate:
+  /v1/credentials/batch:
     put:
       consumes:
       - application/json
@@ -2500,6 +2519,43 @@ paths:
       summary: Get DID
       tags:
       - DecentralizedIdentityAPI
+  /v1/dids/{method}/batch:
+    put:
+      consumes:
+      - application/json
+      description: |-
+        Create a batch of verifiable credentials. The operation is atomic, meaning that all requests will
+        succeed or fail. This is currently only supported for the DID method named `did:key`.
+      parameters:
+      - description: Method. Only `key` is supported.
+        in: path
+        name: method
+        required: true
+        type: string
+      - description: The batch requests
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/pkg_server_router.BatchCreateDIDsRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/pkg_server_router.BatchCreateDIDsResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: Batch Create DIDs
+      tags:
+      - CredentialAPI
   /v1/dids/resolver/{id}:
     get:
       consumes:
diff --git a/integration/common.go b/integration/common.go
index 975e501..4935ea4 100644
--- a/integration/common.go
+++ b/integration/common.go
@@ -55,6 +55,15 @@ func CreateDIDKey() (string, error) {
 	return output, nil
 }
 
+func BatchCreateDIDKeys() (string, error) {
+	output, err := put(endpoint+version+"dids/key/batch", getJSONFromFile("batch-create-did-key-input.json"))
+	if err != nil {
+		return "", errors.Wrapf(err, "did endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
 func CreateDIDWeb() (string, error) {
 	logrus.Println("\n\nCreate a did:web")
 
@@ -189,7 +198,7 @@ func BatchCreateVerifiableCredentials(credentialInput batchCredInputParams) (str
 		return "", err
 	}
 
-	output, err := put(endpoint+version+"credentials/batchCreate", credentialJSON)
+	output, err := put(endpoint+version+"credentials/batch", credentialJSON)
 	if err != nil {
 		return "", errors.Wrap(err, "error writing batch credentials")
 	}
@@ -228,7 +237,7 @@ func BatchCreate100VerifiableCredentials(credentialInput credInputParams) (strin
 		return "", err
 	}
 
-	output, err := put(endpoint+version+"credentials/batchCreate", string(batchCreateData))
+	output, err := put(endpoint+version+"credentials/batch", string(batchCreateData))
 	if err != nil {
 		return "", errors.Wrap(err, "error writing batch credentials")
 	}
@@ -441,6 +450,12 @@ func getJSONElement(jsonString string, jsonPath string) (string, error) {
 		elementStr = fmt.Sprintf("%v", element)
 	case string:
 		elementStr = fmt.Sprintf("%v", element)
+	case []any:
+		data, err := json.Marshal(element)
+		if err != nil {
+			return "", err
+		}
+		elementStr = compactJSONOutput(string(data))
 	case map[string]any:
 		data, err := json.Marshal(element)
 		if err != nil {
diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go
index 4d7935d..68141b8 100644
--- a/pkg/server/router/credential.go
+++ b/pkg/server/router/credential.go
@@ -37,7 +37,7 @@ func NewCredentialRouter(s svcframework.Service) (*CredentialRouter, error) {
 }
 
 type BatchCreateCredentialsRequest struct {
-	// Required. The list of create credential requests. Cannot be more than 1000 items.
+	// Required. The list of create credential requests. Cannot be more than {{.Services.CredentialConfig.BatchCreateMaxItems}} items.
 	Requests []CreateCredentialRequest `json:"requests" maxItems:"1000" validate:"required,dive"`
 }
 
@@ -65,7 +65,7 @@ type BatchCreateCredentialsResponse struct {
 //	@Success		201		{object}	BatchCreateCredentialsResponse
 //	@Failure		400		{string}	string	"Bad request"
 //	@Failure		500		{string}	string	"Internal server error"
-//	@Router			/v1/credentials/batchCreate [put]
+//	@Router			/v1/credentials/batch [put]
 func (cr CredentialRouter) BatchCreateCredentials(c *gin.Context) {
 	invalidCreateCredentialRequest := "invalid batch create credential request"
 	var batchRequest BatchCreateCredentialsRequest
diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go
index 039c3ca..7b68c91 100644
--- a/pkg/server/router/did.go
+++ b/pkg/server/router/did.go
@@ -11,10 +11,9 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
-	"github.com/tbd54566975/ssi-service/pkg/server/pagination"
-
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/server/framework"
+	"github.com/tbd54566975/ssi-service/pkg/server/pagination"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 )
@@ -373,3 +372,90 @@ func (dr DIDRouter) ResolveDID(c *gin.Context) {
 	resp := ResolveDIDResponse{ResolutionMetadata: resolvedDID.ResolutionMetadata, DIDDocument: resolvedDID.DIDDocument, DIDDocumentMetadata: resolvedDID.DIDDocumentMetadata}
 	framework.Respond(c, resp, http.StatusOK)
 }
+
+type BatchCreateDIDsRequest struct {
+	// Required. The list of create credential requests. Cannot be more than {{.Services.DIDConfig.BatchCreateMaxItems}} items.
+	Requests []CreateDIDByMethodRequest `json:"requests" maxItems:"100" validate:"required,dive"`
+}
+
+func (r BatchCreateDIDsRequest) toServiceRequest(m didsdk.Method) (*did.BatchCreateDIDsRequest, error) {
+	var req did.BatchCreateDIDsRequest
+	for _, routerReq := range r.Requests {
+		serviceReq, err := toCreateDIDRequest(m, routerReq)
+		if err != nil {
+			return &req, err
+		}
+		req.Requests = append(req.Requests, *serviceReq)
+	}
+	return &req, nil
+}
+
+type BatchCreateDIDsResponse struct {
+	// The DID documents created.
+	DIDs []didsdk.Document `json:"dids"`
+}
+
+type BatchDIDRouter struct {
+	service *did.BatchService
+}
+
+func NewBatchDIDRouter(svc *did.BatchService) *BatchDIDRouter {
+	return &BatchDIDRouter{service: svc}
+}
+
+// BatchCreateDIDs godoc
+//
+//	@Summary		Batch Create DIDs
+//	@Description	Create a batch of verifiable credentials. The operation is atomic, meaning that all requests will
+//	@Description	succeed or fail. This is currently only supported for the DID method named `did:key`.
+//	@Tags			CredentialAPI
+//	@Accept			json
+//	@Produce		json
+//	@Param			method	path		string					true	"Method. Only `key` is supported."
+//	@Param			request	body		BatchCreateDIDsRequest	true	"The batch requests"
+//	@Success		201		{object}	BatchCreateDIDsResponse
+//	@Failure		400		{string}	string	"Bad request"
+//	@Failure		500		{string}	string	"Internal server error"
+//	@Router			/v1/dids/{method}/batch [put]
+func (dr BatchDIDRouter) BatchCreateDIDs(c *gin.Context) {
+	method := framework.GetParam(c, MethodParam)
+	if method == nil {
+		errMsg := "create DID request missing method parameter"
+		framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
+		return
+	}
+	if *method != "key" {
+		errMsg := "create DID request method parameter must be `key`"
+		framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
+		return
+	}
+	invalidCreateDIDRequest := "invalid batch create DID request"
+	var batchRequest BatchCreateDIDsRequest
+	if err := framework.Decode(c.Request, &batchRequest); err != nil {
+		framework.LoggingRespondErrWithMsg(c, err, invalidCreateDIDRequest, http.StatusBadRequest)
+		return
+	}
+
+	batchCreateMaxItems := dr.service.Config().BatchCreateMaxItems
+	if len(batchRequest.Requests) > batchCreateMaxItems {
+		framework.LoggingRespondErrMsg(c, fmt.Sprintf("max number of requests is %d", batchCreateMaxItems), http.StatusBadRequest)
+		return
+	}
+
+	req, err := batchRequest.toServiceRequest(didsdk.Method(*method))
+	if err != nil {
+		framework.LoggingRespondError(c, err, http.StatusBadRequest)
+		return
+	}
+	batchCreateDIDsResponse, err := dr.service.BatchCreateDIDs(c, *req)
+	if err != nil {
+		errMsg := "could not create credentials"
+		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
+		return
+	}
+
+	var resp BatchCreateDIDsResponse
+	resp.DIDs = append(resp.DIDs, batchCreateDIDsResponse.DIDs...)
+
+	framework.Respond(c, resp, http.StatusCreated)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 0f04059..26eec7e 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -94,7 +94,7 @@ func NewSSIServer(shutdown chan os.Signal, cfg config.SSIServiceConfig) (*SSISer
 	if err = KeyStoreAPI(v1, ssi.KeyStore); err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "unable to instantiate KeyStore API")
 	}
-	if err = DecentralizedIdentityAPI(v1, ssi.DID, ssi.Webhook); err != nil {
+	if err = DecentralizedIdentityAPI(v1, ssi.DID, ssi.BatchDID, ssi.Webhook); err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "unable to instantiate DID API")
 	}
 	if err = SchemaAPI(v1, ssi.Schema, ssi.Webhook); err != nil {
@@ -132,17 +132,8 @@ func writeSwaggerFile(cfg config.SSIServiceConfig) (*os.File, error) {
 		return nil, err
 	}
 
-	input := struct {
-		SwaggerInfoVersion     string
-		SwaggerInfoDescription string
-		SwaggerInfoHost        string
-	}{
-		SwaggerInfoVersion:     cfg.SVN,
-		SwaggerInfoDescription: cfg.Desc,
-		SwaggerInfoHost:        cfg.Server.APIHost,
-	}
 	var b bytes.Buffer
-	if err = t.Execute(&b, input); err != nil {
+	if err = t.Execute(&b, cfg); err != nil {
 		return nil, err
 	}
 	tmpFile, err := os.CreateTemp("", "swagger.yaml")
@@ -189,15 +180,17 @@ func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine {
 }
 
 // DecentralizedIdentityAPI registers all HTTP handlers for the DID Service
-func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, webhookService *webhook.Service) (err error) {
+func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did *didsvc.BatchService, webhookService *webhook.Service) (err error) {
 	didRouter, err := router.NewDIDRouter(service)
 	if err != nil {
 		return sdkutil.LoggingErrorMsg(err, "creating DID router")
 	}
+	batchDIDRouter := router.NewBatchDIDRouter(did)
 
 	didAPI := rg.Group(DIDsPrefix)
 	didAPI.GET("", didRouter.ListDIDMethods)
 	didAPI.PUT("/:method", middleware.Webhook(webhookService, webhook.DID, webhook.Create), didRouter.CreateDIDByMethod)
+	didAPI.PUT("/:method/batch", middleware.Webhook(webhookService, webhook.DID, webhook.BatchCreate), batchDIDRouter.BatchCreateDIDs)
 	didAPI.GET("/:method", didRouter.ListDIDsByMethod)
 	didAPI.GET("/:method/:id", didRouter.GetDIDByMethod)
 	didAPI.DELETE("/:method/:id", didRouter.SoftDeleteDIDByMethod)
@@ -230,7 +223,7 @@ func CredentialAPI(rg *gin.RouterGroup, service svcframework.Service, webhookSer
 	// Credentials
 	credentialAPI := rg.Group(CredentialsPrefix)
 	credentialAPI.PUT("", middleware.Webhook(webhookService, webhook.Credential, webhook.Create), credRouter.CreateCredential)
-	credentialAPI.PUT("/batchCreate", middleware.Webhook(webhookService, webhook.Credential, webhook.BatchCreate), credRouter.BatchCreateCredentials)
+	credentialAPI.PUT("/batch", middleware.Webhook(webhookService, webhook.Credential, webhook.BatchCreate), credRouter.BatchCreateCredentials)
 	credentialAPI.GET("", credRouter.ListCredentials)
 	credentialAPI.GET("/:id", credRouter.GetCredential)
 	credentialAPI.PUT(VerificationPath, credRouter.VerifyCredential)
diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go
index cb83970..db7908a 100644
--- a/pkg/service/did/ion.go
+++ b/pkg/service/did/ion.go
@@ -15,7 +15,6 @@ import (
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
-
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 )
 
diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go
index 167165a..421b423 100644
--- a/pkg/service/did/key.go
+++ b/pkg/service/did/key.go
@@ -11,7 +11,6 @@ import (
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
-
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 )
 
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 9e47a82..2873ba7 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -39,6 +39,14 @@ type CreateDIDResponse struct {
 	DID didsdk.Document `json:"did"`
 }
 
+type BatchCreateDIDsRequest struct {
+	Requests []CreateDIDRequest `json:"requests"`
+}
+
+type BatchCreateDIDsResponse struct {
+	DIDs []didsdk.Document `json:"dids"`
+}
+
 type GetDIDRequest struct {
 	Method didsdk.Method `json:"method" validate:"required"`
 	ID     string        `json:"id" validate:"required"`
diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go
index 0bc47f1..44af368 100644
--- a/pkg/service/did/service.go
+++ b/pkg/service/did/service.go
@@ -7,6 +7,7 @@ import (
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	didresolution "github.com/TBD54566975/ssi-sdk/did/resolution"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/service/did/resolution"
@@ -219,3 +220,78 @@ func (s *Service) getHandler(method didsdk.Method) (MethodHandler, error) {
 	}
 	return handler, nil
 }
+
+type BatchService struct {
+	config  config.DIDServiceConfig
+	storage *Storage
+
+	keyStoreFactory   keystore.ServiceFactory
+	didStorageFactory StorageFactory
+}
+
+func NewBatchDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, factory keystore.ServiceFactory) (*BatchService, error) {
+	didStorage, err := NewDIDStorage(s)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service")
+	}
+
+	service := BatchService{
+		config:            config,
+		storage:           didStorage,
+		keyStoreFactory:   factory,
+		didStorageFactory: NewDIDStorageFactory(s),
+	}
+	return &service, nil
+}
+
+func (s *BatchService) BatchCreateDIDs(ctx context.Context, batchReq BatchCreateDIDsRequest) (*BatchCreateDIDsResponse, error) {
+	watchKey := storage.WatchKey{
+		Namespace: "temporary",
+		Key:       "batch-create-dids-key-" + uuid.NewString(),
+	}
+	err := s.storage.db.Write(ctx, watchKey.Namespace, watchKey.Key, []byte("starting"))
+	if err != nil {
+		return nil, err
+	}
+	returnValue, err := s.storage.db.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) {
+		batchResponse := BatchCreateDIDsResponse{
+			DIDs: make([]didsdk.Document, 0, len(batchReq.Requests)),
+		}
+		keyStore, err := s.keyStoreFactory(tx)
+		if err != nil {
+			return nil, err
+		}
+		didStorage, err := s.didStorageFactory(tx)
+		if err != nil {
+			return nil, err
+		}
+		handler, err := NewKeyHandler(didStorage, keyStore)
+		if err != nil {
+			return nil, err
+		}
+		// watch some new key watchKey
+		// accumulate all writes
+		// execute all writes s.t. if one write fails, then watchKey is written from elsewhere
+		for _, request := range batchReq.Requests {
+			didResponse, err := handler.CreateDID(ctx, request)
+			if err != nil {
+				return nil, err
+			}
+			batchResponse.DIDs = append(batchResponse.DIDs, didResponse.DID)
+		}
+		return &batchResponse, nil
+	}, []storage.WatchKey{watchKey})
+	if err != nil {
+		return nil, err
+	}
+
+	batchResponse, ok := returnValue.(*BatchCreateDIDsResponse)
+	if !ok {
+		return nil, errors.New("problem casting to BatchCreateDIDsResponse")
+	}
+	return batchResponse, nil
+}
+
+func (s *BatchService) Config() config.DIDServiceConfig {
+	return s.config
+}
diff --git a/pkg/service/did/storage.go b/pkg/service/did/storage.go
index cb2e0ec..a1fc0f9 100644
--- a/pkg/service/did/storage.go
+++ b/pkg/service/did/storage.go
@@ -60,13 +60,28 @@ func (d DefaultStoredDID) IsSoftDeleted() bool {
 
 type Storage struct {
 	db storage.ServiceStorage
+	tx storage.Tx
 }
 
 func NewDIDStorage(db storage.ServiceStorage) (*Storage, error) {
-	if db == nil {
-		return nil, errors.New("db reference is nil")
+	return NewDIDStorageFactory(db)(db)
+}
+
+type StorageFactory func(tx storage.Tx) (*Storage, error)
+
+func NewDIDStorageFactory(db storage.ServiceStorage) StorageFactory {
+	return func(tx storage.Tx) (*Storage, error) {
+		if db == nil {
+			return nil, errors.New("db reference is nil")
+		}
+		if tx == nil {
+			return nil, errors.New("tx reference is nil")
+		}
+		return &Storage{
+			db: db,
+			tx: tx,
+		}, nil
 	}
-	return &Storage{db: db}, nil
 }
 
 func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error {
@@ -79,7 +94,7 @@ func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error {
 	if err != nil {
 		return sdkutil.LoggingErrorMsg(err, couldNotStoreDIDErr)
 	}
-	return ds.db.Write(ctx, ns, did.GetID(), didBytes)
+	return ds.tx.Write(ctx, ns, did.GetID(), didBytes)
 }
 
 // GetDID attempts to get a DID from the database. It will return an error if it cannot.
@@ -102,7 +117,7 @@ func (ds *Storage) GetDID(ctx context.Context, id string, out StoredDID) error {
 		return sdkutil.LoggingErrorMsg(err, couldNotGetDIDErr)
 	}
 	if err = json.Unmarshal(docBytes, out); err != nil {
-		return sdkutil.LoggingErrorMsgf(err, "could not ummarshal stored DID: %s", id)
+		return sdkutil.LoggingErrorMsgf(err, "could not unmarshal stored DID: %s", id)
 	}
 	return nil
 }
diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go
index b3d1d25..11601c9 100644
--- a/pkg/service/did/web.go
+++ b/pkg/service/did/web.go
@@ -12,7 +12,6 @@ import (
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
-
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 )
 
diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go
index 471a211..9e1b772 100644
--- a/pkg/service/keystore/service.go
+++ b/pkg/service/keystore/service.go
@@ -20,6 +20,8 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 )
 
+type ServiceFactory func(storage.Tx) (*Service, error)
+
 type Service struct {
 	storage *Storage
 	config  config.KeyStoreServiceConfig
@@ -48,25 +50,31 @@ func (s Service) Config() config.KeyStoreServiceConfig {
 }
 
 func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceStorage) (*Service, error) {
-	encrypter, decrypter, err := NewEncryption(s, config)
-	if err != nil {
-		return nil, errors.Wrap(err, "creating new encryption")
-	}
+	return NewKeyStoreServiceFactory(config, s)(s)
+}
 
-	// Next, instantiate the key storage
-	keyStoreStorage, err := NewKeyStoreStorage(s, encrypter, decrypter)
-	if err != nil {
-		return nil, sdkutil.LoggingErrorMsg(err, "instantiating storage for the keystore service")
-	}
+func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage) ServiceFactory {
+	return func(tx storage.Tx) (*Service, error) {
+		encrypter, decrypter, err := NewEncryption(s, tx, config)
+		if err != nil {
+			return nil, errors.Wrap(err, "creating new encryption")
+		}
 
-	service := Service{
-		storage: keyStoreStorage,
-		config:  config,
-	}
-	if !service.Status().IsReady() {
-		return nil, errors.New(service.Status().Message)
+		// Next, instantiate the key storage
+		keyStoreStorage, err := NewKeyStoreStorage(s, encrypter, decrypter, tx)
+		if err != nil {
+			return nil, sdkutil.LoggingErrorMsg(err, "instantiating storage for the keystore service")
+		}
+
+		service := Service{
+			storage: keyStoreStorage,
+			config:  config,
+		}
+		if !service.Status().IsReady() {
+			return nil, errors.New(service.Status().Message)
+		}
+		return &service, nil
 	}
-	return &service, nil
 }
 
 func (s Service) StoreKey(ctx context.Context, request StoreKeyRequest) error {
diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go
index 199afe6..caeac04 100644
--- a/pkg/service/keystore/storage.go
+++ b/pkg/service/keystore/storage.go
@@ -62,17 +62,22 @@ const (
 
 type Storage struct {
 	db        storage.ServiceStorage
+	tx        storage.Tx
 	encrypter Encrypter
 	decrypter Decrypter
 	Clock     clock.Clock
 }
 
-func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter) (*Storage, error) {
+func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, writer storage.Tx) (*Storage, error) {
 	s := &Storage{
 		db:        db,
 		encrypter: e,
 		decrypter: d,
 		Clock:     clock.New(),
+		tx:        db,
+	}
+	if writer != nil {
+		s.tx = writer
 	}
 
 	return s, nil
@@ -99,7 +104,7 @@ const (
 	awsKMSScheme = "aws-kms"
 )
 
-func NewEncryption(db storage.ServiceStorage, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) {
+func NewEncryption(db storage.ServiceStorage, tx storage.Tx, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
@@ -117,7 +122,7 @@ func NewEncryption(db storage.ServiceStorage, cfg config.KeyStoreServiceConfig)
 		Base58Key:  serviceKey,
 		Base58Salt: serviceKeySalt,
 	}
-	if err := storeServiceKey(ctx, db, key); err != nil {
+	if err := storeServiceKey(ctx, tx, key); err != nil {
 		return nil, nil, err
 	}
 	return &encrypter{db}, &decrypter{db}, nil
@@ -154,12 +159,12 @@ func NewExternalEncrypter(ctx context.Context, cfg config.KeyStoreServiceConfig)
 }
 
 // TODO(gabe): support more robust service key operations, including rotation, and caching
-func storeServiceKey(ctx context.Context, db storage.ServiceStorage, key ServiceKey) error {
+func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey) error {
 	keyBytes, err := json.Marshal(key)
 	if err != nil {
 		return sdkutil.LoggingErrorMsg(err, "could not marshal service key")
 	}
-	if err = db.Write(ctx, namespace, skKey, keyBytes); err != nil {
+	if err = tx.Write(ctx, namespace, skKey, keyBytes); err != nil {
 		return sdkutil.LoggingErrorMsg(err, "could store marshal service key")
 	}
 	return nil
@@ -269,7 +274,7 @@ func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error {
 		return sdkutil.LoggingErrorMsg(err, "marshalling JWK")
 	}
 
-	if err := kss.db.Write(ctx, namespace+publicNamespaceSuffix, id, publicBytes); err != nil {
+	if err := kss.tx.Write(ctx, namespace+publicNamespaceSuffix, id, publicBytes); err != nil {
 		return sdkutil.LoggingErrorMsgf(err, "writing public key")
 	}
 
@@ -279,7 +284,7 @@ func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error {
 		return sdkutil.LoggingErrorMsgf(err, "could not encrypt key: %s", key.ID)
 	}
 
-	return kss.db.Write(ctx, namespace, id, encryptedKey)
+	return kss.tx.Write(ctx, namespace, id, encryptedKey)
 }
 
 // RevokeKey revokes a key by setting the revoked flag to true.
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 31aecf5..3a851b1 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -31,6 +31,7 @@ type SSIService struct {
 	Operation    *operation.Service
 	Webhook      *webhook.Service
 	storage      storage.ServiceStorage
+	BatchDID     *did.BatchService
 }
 
 // InstantiateSSIService creates a new instance of the SSIS which instantiates all services and their
@@ -89,11 +90,18 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the webhook service")
 	}
 
-	keyStoreService, err := keystore.NewKeyStoreService(config.KeyStoreConfig, storageProvider)
+	keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider)
+
+	keyStoreService, err := keyStoreServiceFactory(storageProvider)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate KeyStore service")
 	}
 
+	batchDIDService, err := did.NewBatchDIDService(config.DIDConfig, storageProvider, keyStoreServiceFactory)
+	if err != nil {
+		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate batch DID service")
+	}
+
 	didService, err := did.NewDIDService(config.DIDConfig, storageProvider, keyStoreService)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the DID service")
@@ -133,6 +141,7 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) {
 	return &SSIService{
 		KeyStore:     keyStoreService,
 		DID:          didService,
+		BatchDID:     batchDIDService,
 		Schema:       schemaService,
 		Issuance:     issuanceService,
 		Credential:   credentialService,


Test patch:
diff --git a/config/test.toml b/config/test.toml
index 58030de..302854f 100644
--- a/config/test.toml
+++ b/config/test.toml
@@ -52,6 +52,7 @@ local_resolution_methods = ["key", "web", "pkh", "peer"]
 universal_resolver_url = "http://uni-resolver-web:8080"
 universal_resolver_methods = ["ion"]
 ion_resolver_url = "https://ion.tbddev.org"
+batch_create_max_items = 100
 
 [services.schema]
 name = "schema"
diff --git a/integration/did_integration_test.go b/integration/did_integration_test.go
new file mode 100644
index 0000000..4502bd1
--- /dev/null
+++ b/integration/did_integration_test.go
@@ -0,0 +1,21 @@
+package integration
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBatchCreateDIDKeys(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	didKeyOutput, err := BatchCreateDIDKeys()
+	assert.NoError(t, err)
+
+	issuedDIDs, err := getJSONElement(didKeyOutput, "$.dids[*].id")
+	assert.NoError(t, err)
+	assert.Equal(t, 7, strings.Count(issuedDIDs, "did:key"))
+}
diff --git a/integration/testdata/batch-create-did-key-input.json b/integration/testdata/batch-create-did-key-input.json
new file mode 100644
index 0000000..e6a8887
--- /dev/null
+++ b/integration/testdata/batch-create-did-key-input.json
@@ -0,0 +1,25 @@
+{
+  "requests": [
+    {
+      "keyType": "Ed25519"
+    },
+    {
+      "keyType": "X25519"
+    },
+    {
+      "keyType": "secp256k1"
+    },
+    {
+      "keyType": "P-256"
+    },
+    {
+      "keyType": "P-384"
+    },
+    {
+      "keyType": "P-521"
+    },
+    {
+      "keyType": "RSA"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/server/server_credential_test.go b/pkg/server/server_credential_test.go
index 49100f3..d5aa3d7 100644
--- a/pkg/server/server_credential_test.go
+++ b/pkg/server/server_credential_test.go
@@ -35,8 +35,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -74,7 +74,7 @@ func TestCredentialAPI(t *testing.T) {
 				}
 				tt.Run("Returns Many Credentials", func(ttt *testing.T) {
 					requestValue := newRequestValue(ttt, batchCreateCredentialsRequest)
-					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue)
+					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batch", requestValue)
 					w := httptest.NewRecorder()
 					c := newRequestContext(w, req)
 					credRouter.BatchCreateCredentials(c)
@@ -112,7 +112,7 @@ func TestCredentialAPI(t *testing.T) {
 					})
 
 					requestValue := newRequestValue(ttt, batchCreateCredentialsRequest)
-					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue)
+					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batch", requestValue)
 					w := httptest.NewRecorder()
 					c := newRequestContext(w, req)
 					credRouter.BatchCreateCredentials(c)
@@ -128,7 +128,7 @@ func TestCredentialAPI(t *testing.T) {
 					}
 
 					requestValue := newRequestValue(ttt, batchCreateCredentialsRequest)
-					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batchCreate", requestValue)
+					req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials/batch", requestValue)
 					w := httptest.NewRecorder()
 					c := newRequestContext(w, req)
 					credRouter.BatchCreateCredentials(c)
@@ -140,8 +140,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -225,8 +225,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -332,8 +332,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -408,8 +408,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -494,8 +494,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -552,8 +552,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -610,8 +610,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -670,8 +670,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -742,8 +742,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -814,8 +814,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -994,8 +994,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
@@ -1070,8 +1070,8 @@ func TestCredentialAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credRouter := testCredentialRouter(tt, db, keyStoreService, didService, schemaService)
 
diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go
index b942088..eba4466 100644
--- a/pkg/server/server_did_test.go
+++ b/pkg/server/server_did_test.go
@@ -30,8 +30,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStoreService := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStoreService, []string{"key", "web", "ion"})
+				_, keyStoreService, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"key", "web", "ion"}, nil)
 
 				// get DID method
 				req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids", nil)
@@ -55,8 +55,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStoreService := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStoreService, []string{"key"})
+				_, keyStoreService, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"key"}, nil)
 
 				// create DID by method - key - missing body
 				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", nil)
@@ -100,8 +100,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStoreService := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStoreService, []string{"web"})
+				_, keyStoreService, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"web"}, nil)
 
 				// create DID by method - web - missing body
 				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", nil)
@@ -173,8 +173,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStoreService := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStoreService, []string{"ion"})
+				_, keyStoreService, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, nil)
 
 				// create DID by method - ion - missing body
 				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil)
@@ -249,8 +249,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStoreService := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStoreService, []string{"web"})
+				_, keyStoreService, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"web"}, nil)
 
 				// reset recorder between calls
 				w := httptest.NewRecorder()
@@ -309,8 +309,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStore := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStore, []string{"key"})
+				_, keyStore, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStore, []string{"key"}, nil)
 
 				// get DID by method
 				req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad/worse", nil)
@@ -381,8 +381,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStore := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStore, []string{"key"})
+				_, keyStore, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStore, []string{"key"}, nil)
 
 				// soft delete DID by method
 				req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/dids/bad/worse", nil)
@@ -505,8 +505,8 @@ func TestDIDAPI(t *testing.T) {
 			t.Run("List DIDs made up token fails", func(tt *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
-				_, keyStore := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"})
+				_, keyStore, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStore, []string{"key", "web"}, nil)
 
 				w := httptest.NewRecorder()
 				badParams := url.Values{
@@ -524,8 +524,8 @@ func TestDIDAPI(t *testing.T) {
 				if !strings.Contains(test.Name, "Redis") {
 					db := test.ServiceStorage(t)
 					require.NotEmpty(tt, db)
-					_, keyStore := testKeyStore(tt, db)
-					didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"})
+					_, keyStore, _ := testKeyStore(tt, db)
+					didRouter, _ := testDIDRouter(tt, db, keyStore, []string{"key", "web"}, nil)
 
 					createDIDWithRouter(tt, didRouter)
 					createDIDWithRouter(tt, didRouter)
@@ -565,8 +565,8 @@ func TestDIDAPI(t *testing.T) {
 				if !strings.Contains(test.Name, "Redis") {
 					db := test.ServiceStorage(t)
 					require.NotEmpty(tt, db)
-					_, keyStore := testKeyStore(tt, db)
-					didRouter := testDIDRouter(tt, db, keyStore, []string{"key", "web"})
+					_, keyStore, _ := testKeyStore(tt, db)
+					didRouter, _ := testDIDRouter(tt, db, keyStore, []string{"key", "web"}, nil)
 					createDIDWithRouter(tt, didRouter)
 					createDIDWithRouter(tt, didRouter)
 
@@ -601,8 +601,8 @@ func TestDIDAPI(t *testing.T) {
 			t.Run("Test Get DIDs By Method", func(tt *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
-				_, keyStore := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"})
+				_, keyStore, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStore, []string{"key", "web"}, nil)
 
 				// get DIDs by method
 				req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil)
@@ -687,8 +687,8 @@ func TestDIDAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				_, keyStore := testKeyStore(tt, db)
-				didService := testDIDRouter(tt, db, keyStore, []string{"key", "web"})
+				_, keyStore, _ := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStore, []string{"key", "web"}, nil)
 
 				// bad resolution request
 				req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/bad", nil)
@@ -731,6 +731,81 @@ func TestDIDAPI(t *testing.T) {
 	}
 }
 
+func TestBatchCreateDIDs(t *testing.T) {
+	for _, test := range testutil.TestDatabases {
+		t.Run(test.Name, func(t *testing.T) {
+			db := test.ServiceStorage(t)
+			require.NotEmpty(t, db)
+			_, keyStoreService, factory := testKeyStore(t, db)
+
+			createDIDRequest := router.BatchCreateDIDsRequest{
+				Requests: []router.CreateDIDByMethodRequest{
+					{
+						KeyType: crypto.Ed25519,
+					},
+					{
+						KeyType: crypto.Ed25519,
+					},
+					{
+						KeyType: crypto.Ed25519,
+					},
+				},
+			}
+
+			_, batchDIDRouter := testDIDRouter(t, db, keyStoreService, []string{"key"}, factory)
+			t.Run(" Batch Create DID By Method: Key", func(tt *testing.T) {
+				w := httptest.NewRecorder()
+				requestReader := newRequestValue(tt, createDIDRequest)
+				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader)
+				params := map[string]string{"method": "key"}
+				c := newRequestContextWithParams(w, req, params)
+
+				batchDIDRouter.BatchCreateDIDs(c)
+
+				assert.True(tt, util.Is2xxResponse(w.Code))
+				var resp router.BatchCreateDIDsResponse
+				err := json.NewDecoder(w.Body).Decode(&resp)
+				assert.NoError(tt, err)
+				assert.Len(tt, resp.DIDs, 3)
+			})
+
+			t.Run("Fails with malformed request", func(ttt *testing.T) {
+				createDIDRequest := createDIDRequest
+				// missing the data field
+				createDIDRequest.Requests = append(createDIDRequest.Requests, router.CreateDIDByMethodRequest{
+					KeyType: "bad",
+				})
+
+				requestValue := newRequestValue(ttt, createDIDRequest)
+				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key/batch", requestValue)
+				w := httptest.NewRecorder()
+				params := map[string]string{"method": "key"}
+				c := newRequestContextWithParams(w, req, params)
+				batchDIDRouter.BatchCreateDIDs(c)
+				assert.Equal(ttt, http.StatusInternalServerError, w.Code)
+				assert.Contains(ttt, w.Body.String(), "unsupported did:key type: bad")
+			})
+
+			t.Run("Fails with more than 1000 requests", func(ttt *testing.T) {
+				createDIDRequest := createDIDRequest
+				// missing the data field
+				for i := 0; i < 1000; i++ {
+					createDIDRequest.Requests = append(createDIDRequest.Requests, createDIDRequest.Requests[0])
+				}
+
+				requestValue := newRequestValue(ttt, createDIDRequest)
+				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key/batch", requestValue)
+				w := httptest.NewRecorder()
+				params := map[string]string{"method": "key"}
+				c := newRequestContextWithParams(w, req, params)
+				batchDIDRouter.BatchCreateDIDs(c)
+				assert.Equal(ttt, http.StatusBadRequest, w.Code)
+				assert.Contains(ttt, w.Body.String(), "max number of requests is 100")
+			})
+		})
+	}
+}
+
 func createDIDWithRouter(tt *testing.T, didService *router.DIDRouter) {
 	w := httptest.NewRecorder()
 	createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
diff --git a/pkg/server/server_issuance_test.go b/pkg/server/server_issuance_test.go
index 9a7bc8b..f1c308b 100644
--- a/pkg/server/server_issuance_test.go
+++ b/pkg/server/server_issuance_test.go
@@ -400,8 +400,8 @@ func createSimpleTemplate(t *testing.T, manifest *model.CreateManifestResponse,
 }
 
 func setupAllThings(t *testing.T, s storage.ServiceStorage) (*did.CreateDIDResponse, *schema.CreateSchemaResponse, *model.CreateManifestResponse, *router.IssuanceRouter) {
-	_, keyStoreSvc := testKeyStore(t, s)
-	didSvc := testDIDService(t, s, keyStoreSvc)
+	_, keyStoreSvc, _ := testKeyStore(t, s)
+	didSvc, _ := testDIDService(t, s, keyStoreSvc, nil)
 	schemaSvc := testSchemaService(t, s, keyStoreSvc, didSvc)
 	credSvc := testCredentialService(t, s, keyStoreSvc, didSvc, schemaSvc)
 	_, manifestSvc := testManifest(t, s, keyStoreSvc, didSvc, credSvc)
diff --git a/pkg/server/server_keystore_test.go b/pkg/server/server_keystore_test.go
index 3dd8484..20ab159 100644
--- a/pkg/server/server_keystore_test.go
+++ b/pkg/server/server_keystore_test.go
@@ -25,7 +25,7 @@ func TestKeyStoreAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreRouter, _ := testKeyStore(tt, db)
+				keyStoreRouter, _, _ := testKeyStore(tt, db)
 				w := httptest.NewRecorder()
 
 				// bad key type
@@ -69,7 +69,7 @@ func TestKeyStoreAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService, _ := testKeyStore(tt, db)
+				keyStoreService, _, _ := testKeyStore(tt, db)
 				w := httptest.NewRecorder()
 
 				// store a valid key
diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go
index 2d6e3e5..e7206e6 100644
--- a/pkg/server/server_manifest_test.go
+++ b/pkg/server/server_manifest_test.go
@@ -39,8 +39,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, manifestService := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -111,8 +111,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -181,8 +181,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, manifestService := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -269,8 +269,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -338,9 +338,9 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
+				keyStoreService, _ := testKeyStoreService(tt, db)
 				issuanceService := testIssuanceService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, manifestSvc := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -491,8 +491,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -656,8 +656,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -805,8 +805,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
@@ -990,8 +990,8 @@ func TestManifestAPI(t *testing.T) {
 				db := test.ServiceStorage(t)
 				require.NotEmpty(tt, db)
 
-				keyStoreService := testKeyStoreService(tt, db)
-				didService := testDIDService(tt, db, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, db)
+				didService, _ := testDIDService(tt, db, keyStoreService, nil)
 				schemaService := testSchemaService(tt, db, keyStoreService, didService)
 				credentialService := testCredentialService(tt, db, keyStoreService, didService, schemaService)
 				manifestRouter, _ := testManifest(tt, db, keyStoreService, didService, credentialService)
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
index cd12b9d..7a242a6 100644
--- a/pkg/server/server_presentation_test.go
+++ b/pkg/server/server_presentation_test.go
@@ -727,8 +727,8 @@ func createPresentationRequest(t *testing.T, pRouter *router.PresentationRouter,
 }
 
 func setupPresentationRouter(t *testing.T, s storage.ServiceStorage) (*router.PresentationRouter, *did.Service) {
-	keyStoreService := testKeyStoreService(t, s)
-	didService := testDIDService(t, s, keyStoreService)
+	keyStoreService, _ := testKeyStoreService(t, s)
+	didService, _ := testDIDService(t, s, keyStoreService, nil)
 	schemaService := testSchemaService(t, s, keyStoreService, didService)
 
 	service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService, keyStoreService)
diff --git a/pkg/server/server_schema_test.go b/pkg/server/server_schema_test.go
index 572fcb6..090d7e3 100644
--- a/pkg/server/server_schema_test.go
+++ b/pkg/server/server_schema_test.go
@@ -27,8 +27,8 @@ func TestSchemaAPI(t *testing.T) {
 				bolt := test.ServiceStorage(t)
 				require.NotEmpty(tt, bolt)
 
-				keyStoreService := testKeyStoreService(tt, bolt)
-				didService := testDIDService(tt, bolt, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, bolt)
+				didService, _ := testDIDService(tt, bolt, keyStoreService, nil)
 				schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService)
 
 				simpleSchema := getTestSchema()
@@ -69,8 +69,8 @@ func TestSchemaAPI(t *testing.T) {
 				bolt := test.ServiceStorage(t)
 				require.NotEmpty(tt, bolt)
 
-				keyStoreService := testKeyStoreService(tt, bolt)
-				didService := testDIDService(tt, bolt, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, bolt)
+				didService, _ := testDIDService(tt, bolt, keyStoreService, nil)
 				schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService)
 
 				simpleSchema := getTestSchema()
@@ -146,8 +146,8 @@ func TestSchemaAPI(t *testing.T) {
 				bolt := test.ServiceStorage(t)
 				require.NotEmpty(tt, bolt)
 
-				keyStoreService := testKeyStoreService(tt, bolt)
-				didService := testDIDService(tt, bolt, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, bolt)
+				didService, _ := testDIDService(tt, bolt, keyStoreService, nil)
 				schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService)
 
 				// get schema that doesn't exist
@@ -238,8 +238,8 @@ func TestSchemaAPI(t *testing.T) {
 				bolt := test.ServiceStorage(t)
 				require.NotEmpty(tt, bolt)
 
-				keyStoreService := testKeyStoreService(tt, bolt)
-				didService := testDIDService(tt, bolt, keyStoreService)
+				keyStoreService, _ := testKeyStoreService(tt, bolt)
+				didService, _ := testDIDService(tt, bolt, keyStoreService, nil)
 				schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService)
 
 				w := httptest.NewRecorder()
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index ddd6501..1feee7d 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -212,28 +212,29 @@ func getValidApplicationRequest(manifestID, presDefID, submissionDescriptorID st
 	}
 }
 
-func testKeyStore(t *testing.T, bolt storage.ServiceStorage) (*router.KeyStoreRouter, *keystore.Service) {
-	keyStoreService := testKeyStoreService(t, bolt)
+func testKeyStore(t *testing.T, bolt storage.ServiceStorage) (*router.KeyStoreRouter, *keystore.Service, keystore.ServiceFactory) {
+	keyStoreService, keyStoreServiceFactory := testKeyStoreService(t, bolt)
 
 	// create router for service
 	keyStoreRouter, err := router.NewKeyStoreRouter(keyStoreService)
 	require.NoError(t, err)
 	require.NotEmpty(t, keyStoreRouter)
 
-	return keyStoreRouter, keyStoreService
+	return keyStoreRouter, keyStoreService, keyStoreServiceFactory
 }
 
-func testKeyStoreService(t *testing.T, db storage.ServiceStorage) *keystore.Service {
+func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Service, keystore.ServiceFactory) {
 	serviceConfig := config.KeyStoreServiceConfig{
 		BaseServiceConfig: &config.BaseServiceConfig{Name: "test-keystore"},
 		MasterKeyPassword: "test-password",
 	}
 
 	// create a keystore service
-	keystoreService, err := keystore.NewKeyStoreService(serviceConfig, db)
+	factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db)
+	keystoreService, err := factory(db)
 	require.NoError(t, err)
 	require.NotEmpty(t, keystoreService)
-	return keystoreService
+	return keystoreService, factory
 }
 
 func testIssuanceService(t *testing.T, db storage.ServiceStorage) *issuance.Service {
@@ -247,7 +248,7 @@ func testIssuanceService(t *testing.T, db storage.ServiceStorage) *issuance.Serv
 	return s
 }
 
-func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, methods ...string) *did.Service {
+func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, factory keystore.ServiceFactory, methods ...string) (*did.Service, *did.BatchService) {
 	if methods == nil {
 		methods = []string{"key"}
 	}
@@ -256,23 +257,29 @@ func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystor
 		Methods:                methods,
 		LocalResolutionMethods: []string{"key", "web", "peer", "pkh"},
 		IONResolverURL:         testIONResolverURL,
+		BatchCreateMaxItems:    100,
 	}
 
 	// create a did service
 	didService, err := did.NewDIDService(serviceConfig, bolt, keyStore)
 	require.NoError(t, err)
 	require.NotEmpty(t, didService)
-	return didService
+
+	batchDIDService, err := did.NewBatchDIDService(serviceConfig, bolt, factory)
+	require.NoError(t, err)
+	return didService, batchDIDService
 }
 
-func testDIDRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, methods []string) *router.DIDRouter {
-	didService := testDIDService(t, bolt, keyStore, methods...)
+func testDIDRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, methods []string, factory keystore.ServiceFactory) (*router.DIDRouter, *router.BatchDIDRouter) {
+	didService, batchDIDService := testDIDService(t, bolt, keyStore, factory, methods...)
 
 	// create router for service
 	didRouter, err := router.NewDIDRouter(didService)
 	require.NoError(t, err)
 	require.NotEmpty(t, didRouter)
-	return didRouter
+
+	batchDIDRouter := router.NewBatchDIDRouter(batchDIDService)
+	return didRouter, batchDIDRouter
 }
 
 func testSchemaService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, did *did.Service) *schema.Service {