Repository: TBD54566975/ssi-service
Pull request #161 (2022-11-04 16:30:23)

[OSE-178] Added /v1/presentation/definition endpoint
This PR adds the the endpoint with Create, Get, and Delete operations. 

Unit tests were also added to ensure things work correctly.

This is part of supporting the [Presentation Exchange](https://identity.foundation/presentation-exchange/#presentation-definition) spec. 

codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/161?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#161](https://codecov.io/gh/TBD54566975/ssi-service/pull/161?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (00e2595) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/7143d2d0fb919a46646265b1d27bf1edf692516d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (7143d2d) will **decrease** coverage by `0.72%`.
> The diff coverage is `16.36%`.

```diff
@@            Coverage Diff             @@
##             main     #161      +/-   ##
==========================================
- Coverage   26.88%   26.16%   -0.73%     
==========================================
  Files          19       20       +1     
  Lines        1484     1594     +110     
==========================================
+ Hits          399      417      +18     
- Misses       1015     1106      +91     
- Partials       70       71       +1     
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/161?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://codecov.io/gh/TBD54566975/ssi-service/pull/161/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `8.79% <0.00%> (-0.85%)` | :arrow_down: |
| [pkg/server/router/presentation.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/161/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvcHJlc2VudGF0aW9uLmdv) | `8.98% <8.98%> (ø)` | |
| [pkg/server/server.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/161/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `80.00% <76.92%> (-0.33%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


Patch:
diff --git a/README.md b/README.md
index dcc91da..35590ee 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@ The command below will tell if you all the services (credential, did, and schema
 
 ## HTTP Endpoints
 
-You can find more HTTP endpoints by checking out the swagger docs at: `http://localhost:8002/doc`
+You can find more HTTP endpoints by checking out the swagger docs at: `http://localhost:8002/docs`
 
 Note: Your port by differ, the range of the ports for swagger are between `8002` and `8080`.
 
diff --git a/config/config.go b/config/config.go
index 5d09275..34a6502 100644
--- a/config/config.go
+++ b/config/config.go
@@ -48,11 +48,12 @@ type ServicesConfig struct {
 
 	// Embed all service-specific configs here. The order matters: from which should be instantiated first, to last
 
-	KeyStoreConfig   KeyStoreServiceConfig   `toml:"keystore,omitempty"`
-	DIDConfig        DIDServiceConfig        `toml:"did,omitempty"`
-	SchemaConfig     SchemaServiceConfig     `toml:"schema,omitempty"`
-	CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"`
-	ManifestConfig   ManifestServiceConfig   `toml:"manifest,omitempty"`
+	KeyStoreConfig     KeyStoreServiceConfig     `toml:"keystore,omitempty"`
+	DIDConfig          DIDServiceConfig          `toml:"did,omitempty"`
+	SchemaConfig       SchemaServiceConfig       `toml:"schema,omitempty"`
+	CredentialConfig   CredentialServiceConfig   `toml:"credential,omitempty"`
+	ManifestConfig     ManifestServiceConfig     `toml:"manifest,omitempty"`
+	PresentationConfig PresentationServiceConfig `toml:"presentation,omitempty"`
 }
 
 // BaseServiceConfig represents configurable properties for a specific component of the SSI Service
@@ -122,6 +123,17 @@ func (m *ManifestServiceConfig) IsEmpty() bool {
 	return reflect.DeepEqual(m, &ManifestServiceConfig{})
 }
 
+type PresentationServiceConfig struct {
+	*BaseServiceConfig
+}
+
+func (p *PresentationServiceConfig) IsEmpty() bool {
+	if p == nil {
+		return true
+	}
+	return reflect.DeepEqual(p, &PresentationServiceConfig{})
+}
+
 // LoadConfig attempts to load a TOML config file from the given path, and coerce it into our object model.
 // Before loading, defaults are applied on certain properties, which are overwritten if specified in the TOML file.
 func LoadConfig(path string) (*SSIServiceConfig, error) {
@@ -183,6 +195,9 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
 			ManifestConfig: ManifestServiceConfig{
 				BaseServiceConfig: &BaseServiceConfig{Name: "manifest"},
 			},
+			PresentationConfig: PresentationServiceConfig{
+				BaseServiceConfig: &BaseServiceConfig{Name: "presentation"},
+			},
 		}
 	} else {
 		// load from TOML file
diff --git a/config/config.toml b/config/config.toml
index d1d777d..b176e72 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -37,4 +37,7 @@ name = "schema"
 name = "credential"
 
 [services.manifest]
-name = "manifest"
\ No newline at end of file
+name = "manifest"
+
+[services.presentation]
+name = "presentation"
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index fafd42f..9828486 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -223,7 +223,7 @@ definitions:
     - id
     - type
     type: object
-  dwn.DWNPublishManifestResponse:
+  dwn.PublishManifestResponse:
     properties:
       response:
         type: string
@@ -315,6 +315,8 @@ definitions:
     type: object
   exchange.Filter:
     properties:
+      additionalProperties:
+        type: boolean
       allOf: {}
       const: {}
       enum:
@@ -334,6 +336,11 @@ definitions:
       oneOf: {}
       pattern:
         type: string
+      properties: {}
+      required:
+        items:
+          type: string
+        type: array
       type:
         type: string
     type: object
@@ -542,6 +549,18 @@ definitions:
       manifestJwt:
         type: string
     type: object
+  github.com_tbd54566975_ssi-service_pkg_server_router.CreatePresentationDefinitionRequest:
+    properties:
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    required:
+    - presentation_definition
+    type: object
+  github.com_tbd54566975_ssi-service_pkg_server_router.CreatePresentationDefinitionResponse:
+    properties:
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.CreateSchemaRequest:
     properties:
       author:
@@ -649,6 +668,13 @@ definitions:
           $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestResponse'
         type: array
     type: object
+  github.com_tbd54566975_ssi-service_pkg_server_router.GetPresentationDefinitionResponse:
+    properties:
+      id:
+        type: string
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    type: object
   github.com_tbd54566975_ssi-service_pkg_server_router.GetResponseResponse:
     properties:
       id:
@@ -687,7 +713,7 @@ definitions:
   github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestResponse:
     properties:
       dwnResponse:
-        $ref: '#/definitions/dwn.DWNPublishManifestResponse'
+        $ref: '#/definitions/dwn.PublishManifestResponse'
       manifest:
         $ref: '#/definitions/manifest.CredentialManifest'
     required:
@@ -950,6 +976,18 @@ definitions:
       manifestJwt:
         type: string
     type: object
+  pkg_server_router.CreatePresentationDefinitionRequest:
+    properties:
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    required:
+    - presentation_definition
+    type: object
+  pkg_server_router.CreatePresentationDefinitionResponse:
+    properties:
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    type: object
   pkg_server_router.CreateSchemaRequest:
     properties:
       author:
@@ -1057,6 +1095,13 @@ definitions:
           $ref: '#/definitions/pkg_server_router.GetManifestResponse'
         type: array
     type: object
+  pkg_server_router.GetPresentationDefinitionResponse:
+    properties:
+      id:
+        type: string
+      presentation_definition:
+        $ref: '#/definitions/exchange.PresentationDefinition'
+    type: object
   pkg_server_router.GetResponseResponse:
     properties:
       id:
@@ -1095,7 +1140,7 @@ definitions:
   pkg_server_router.PublishManifestResponse:
     properties:
       dwnResponse:
-        $ref: '#/definitions/dwn.DWNPublishManifestResponse'
+        $ref: '#/definitions/dwn.PublishManifestResponse'
       manifest:
         $ref: '#/definitions/manifest.CredentialManifest'
     required:
@@ -1309,7 +1354,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetHealthCheckResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetHealthCheckResponse'
       summary: Health Check
       tags:
       - HealthCheck
@@ -1486,7 +1531,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDMethodsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDMethodsResponse'
       summary: Get DID Methods
       tags:
       - DecentralizedIdentityAPI
@@ -1507,7 +1552,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDsByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDsByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1525,7 +1570,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest'
       - description: Method
         in: path
         name: method
@@ -1537,7 +1582,7 @@ paths:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateDIDByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1560,7 +1605,7 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest'
       - description: Method
         in: path
         name: method
@@ -1577,7 +1622,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetDIDByMethodResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDByMethodResponse'
         "400":
           description: Bad request
           schema:
@@ -1602,7 +1647,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.ResolveDIDResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.ResolveDIDResponse'
         "400":
           description: Bad request
           schema:
@@ -1621,14 +1666,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestRequest'
+          $ref: '#/definitions/pkg_server_router.PublishManifestRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestResponse'
+            $ref: '#/definitions/pkg_server_router.PublishManifestResponse'
         "400":
           description: Bad request
           schema:
@@ -1651,12 +1696,12 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest'
+          $ref: '#/definitions/pkg_server_router.StoreKeyRequest'
       produces:
       - application/json
       responses:
         "201":
-          description: ""
+          description: Created
         "400":
           description: Bad request
           schema:
@@ -1685,7 +1730,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse'
+            $ref: '#/definitions/pkg_server_router.GetKeyDetailsResponse'
         "400":
           description: Bad request
           schema:
@@ -1718,7 +1763,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetManifestsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestsResponse'
         "400":
           description: Bad request
           schema:
@@ -1740,14 +1785,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateManifestRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateManifestResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestResponse'
         "400":
           description: Bad request
           schema:
@@ -1804,7 +1849,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetManifestResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestResponse'
         "400":
           description: Bad request
           schema:
@@ -1824,7 +1869,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetApplicationsResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationsResponse'
         "400":
           description: Bad request
           schema:
@@ -1847,14 +1892,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.SubmitApplicationRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.SubmitApplicationResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationResponse'
         "400":
           description: Bad request
           schema:
@@ -1911,7 +1956,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetApplicationResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationResponse'
         "400":
           description: Bad request
           schema:
@@ -1931,7 +1976,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetResponsesResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponsesResponse'
         "400":
           description: Bad request
           schema:
@@ -1988,7 +2033,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetResponseResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponseResponse'
         "400":
           description: Bad request
           schema:
@@ -1996,6 +2041,89 @@ paths:
       summary: Get response
       tags:
       - ResponseAPI
+  /v1/presentation/definition:
+    put:
+      consumes:
+      - application/json
+      description: Create presentation definition
+      parameters:
+      - description: request body
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/pkg_server_router.CreatePresentationDefinitionRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/pkg_server_router.CreatePresentationDefinitionResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: Create PresentationDefinition
+      tags:
+      - PresentationDefinitionAPI
+  /v1/presentation/definition/{id}:
+    delete:
+      consumes:
+      - application/json
+      description: Delete a presentation definition by its ID
+      parameters:
+      - description: ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: Delete PresentationDefinition
+      tags:
+      - PresentationDefinitionAPI
+    get:
+      consumes:
+      - application/json
+      description: Get a presentation definition by its ID
+      parameters:
+      - description: ID
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/pkg_server_router.GetPresentationDefinitionResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+      summary: Get PresentationDefinition
+      tags:
+      - PresentationDefinitionAPI
   /v1/schemas:
     get:
       consumes:
@@ -2007,7 +2135,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSchemasResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemasResponse'
         "500":
           description: Internal server error
           schema:
@@ -2025,14 +2153,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.CreateSchemaRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateSchemaRequest'
       produces:
       - application/json
       responses:
         "201":
           description: Created
           schema:
-            $ref: '#/definitions/pkg_server_router.CreateSchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateSchemaResponse'
         "400":
           description: Bad request
           schema:
@@ -2089,7 +2217,7 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.GetSchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemaResponse'
         "400":
           description: Bad request
           schema:
@@ -2108,14 +2236,14 @@ paths:
         name: request
         required: true
         schema:
-          $ref: '#/definitions/pkg_server_router.VerifySchemaRequest'
+          $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaRequest'
       produces:
       - application/json
       responses:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/pkg_server_router.VerifySchemaResponse'
+            $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaResponse'
         "400":
           description: Bad request
           schema:
diff --git a/go.mod b/go.mod
index e980173..2d2598c 100644
--- a/go.mod
+++ b/go.mod
@@ -37,6 +37,7 @@ require (
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/validator/v10 v10.11.1 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
 	github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index e7a13e8..f964f52 100644
--- a/go.sum
+++ b/go.sum
@@ -44,6 +44,7 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
 github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go
new file mode 100644
index 0000000..ac87b6c
--- /dev/null
+++ b/pkg/server/router/presentation.go
@@ -0,0 +1,175 @@
+package router
+
+import (
+	"context"
+	"fmt"
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+	"github.com/tbd54566975/ssi-service/pkg/server/framework"
+	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"net/http"
+)
+
+type PresentationDefinitionRouter struct {
+	service *presentation.Service
+}
+
+func NewPresentationDefinitionRouter(s svcframework.Service) (*PresentationDefinitionRouter, error) {
+	if s == nil {
+		return nil, errors.New("service cannot be nil")
+	}
+	service, ok := s.(*presentation.Service)
+	if !ok {
+		return nil, fmt.Errorf("could not create presentation router with service type: %s", s.Type())
+	}
+	return &PresentationDefinitionRouter{service: service}, nil
+}
+
+type CreatePresentationDefinitionRequest struct {
+	Name                   string                           `json:"name,omitempty"`
+	Purpose                string                           `json:"purpose,omitempty"`
+	Format                 *exchange.ClaimFormat            `json:"format,omitempty" validate:"omitempty,dive"`
+	InputDescriptors       []exchange.InputDescriptor       `json:"inputDescriptors" validate:"required,dive"`
+	SubmissionRequirements []exchange.SubmissionRequirement `json:"submissionRequirements,omitempty" validate:"omitempty,dive"`
+}
+
+type CreatePresentationDefinitionResponse struct {
+	PresentationDefinition exchange.PresentationDefinition `json:"presentation_definition"`
+}
+
+// CreatePresentationDefinition godoc
+// @Summary      Create PresentationDefinition
+// @Description  Create presentation definition
+// @Tags         PresentationDefinitionAPI
+// @Accept       json
+// @Produce      json
+// @Param        request  body      CreatePresentationDefinitionRequest  true  "request body"
+// @Success      201      {object}  CreatePresentationDefinitionResponse
+// @Failure      400      {string}  string  "Bad request"
+// @Failure      500      {string}  string  "Internal server error"
+// @Router       /v1/presentation/definition [put]
+func (pdr PresentationDefinitionRouter) CreatePresentationDefinition(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+	var request CreatePresentationDefinitionRequest
+	errMsg := "Invalid Presentation Definition Request"
+	if err := framework.Decode(r, &request); err != nil {
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+
+	if err := framework.ValidateRequest(request); err != nil {
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+
+	def, err := definitionFromRequest(request)
+	if err != nil {
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+	serviceResp, err := pdr.service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *def})
+	if err != nil {
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
+	}
+
+	resp := CreatePresentationDefinitionResponse{
+		PresentationDefinition: serviceResp.PresentationDefinition,
+	}
+	return framework.Respond(ctx, w, resp, http.StatusCreated)
+}
+
+func definitionFromRequest(request CreatePresentationDefinitionRequest) (*exchange.PresentationDefinition, error) {
+	b := exchange.NewPresentationDefinitionBuilder()
+	if err := b.SetName(request.Name); err != nil {
+		return nil, err
+	}
+	if err := b.SetPurpose(request.Purpose); err != nil {
+		return nil, err
+	}
+	if request.Format != nil {
+		if err := b.SetClaimFormat(*request.Format); err != nil {
+			return nil, err
+		}
+	}
+	if len(request.SubmissionRequirements) > 0 {
+		if err := b.SetSubmissionRequirements(request.SubmissionRequirements); err != nil {
+			return nil, err
+		}
+	}
+	if len(request.InputDescriptors) > 0 {
+		if err := b.SetInputDescriptors(request.InputDescriptors); err != nil {
+			return nil, err
+		}
+	}
+
+	req, err := b.Build()
+	if err != nil {
+		return nil, err
+	}
+	return req, nil
+}
+
+type GetPresentationDefinitionResponse struct {
+	PresentationDefinition exchange.PresentationDefinition `json:"presentation_definition"`
+}
+
+// GetPresentationDefinition godoc
+// @Summary      Get PresentationDefinition
+// @Description  Get a presentation definition by its ID
+// @Tags         PresentationDefinitionAPI
+// @Accept       json
+// @Produce      json
+// @Param        id   path      string  true  "ID"
+// @Success      200  {object}  GetPresentationDefinitionResponse
+// @Failure      400  {string}  string  "Bad request"
+// @Router       /v1/presentation/definition/{id} [get]
+func (pdr PresentationDefinitionRouter) GetPresentationDefinition(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+	id := framework.GetParam(ctx, IDParam)
+	if id == nil {
+		errMsg := "cannot get presentation without ID parameter"
+		logrus.Error(errMsg)
+		return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
+	}
+
+	def, err := pdr.service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: *id})
+	if err != nil {
+		errMsg := fmt.Sprintf("could not get presentation with id: %s", *id)
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+
+	resp := GetPresentationDefinitionResponse{
+		PresentationDefinition: def.PresentationDefinition,
+	}
+	return framework.Respond(ctx, w, resp, http.StatusOK)
+}
+
+// DeletePresentationDefinition godoc
+// @Summary      Delete PresentationDefinition
+// @Description  Delete a presentation definition by its ID
+// @Tags         PresentationDefinitionAPI
+// @Accept       json
+// @Produce      json
+// @Param        id   path      string  true  "ID"
+// @Success      200  {string}  string  "OK"
+// @Failure      400  {string}  string  "Bad request"
+// @Failure      500  {string}  string  "Internal server error"
+// @Router       /v1/presentation/definition/{id} [delete]
+func (pdr PresentationDefinitionRouter) DeletePresentationDefinition(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
+	id := framework.GetParam(ctx, IDParam)
+	if id == nil {
+		errMsg := "cannot delete a presentation without an ID parameter"
+		logrus.Error(errMsg)
+		return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
+	}
+
+	if err := pdr.service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: *id}); err != nil {
+		errMsg := fmt.Sprintf("could not delete presentation with id: %s", *id)
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
+	}
+
+	return framework.Respond(ctx, w, nil, http.StatusOK)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index b10ad5c..ed6697a 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -20,17 +20,19 @@ import (
 )
 
 const (
-	HealthPrefix       = "/health"
-	ReadinessPrefix    = "/readiness"
-	V1Prefix           = "/v1"
-	DIDsPrefix         = "/dids"
-	SchemasPrefix      = "/schemas"
-	CredentialsPrefix  = "/credentials"
-	ManifestsPrefix    = "/manifests"
-	ApplicationsPrefix = "/applications"
-	ResponsesPrefix    = "/responses"
-	KeyStorePrefix     = "/keys"
-	VerificationPath   = "/verification"
+	HealthPrefix        = "/health"
+	ReadinessPrefix     = "/readiness"
+	V1Prefix            = "/v1"
+	DIDsPrefix          = "/dids"
+	SchemasPrefix       = "/schemas"
+	CredentialsPrefix   = "/credentials"
+	PresentationsPrefix = "/presentations"
+	DefinitionsPrefix   = "/definitions"
+	ManifestsPrefix     = "/manifests"
+	ApplicationsPrefix  = "/applications"
+	ResponsesPrefix     = "/responses"
+	KeyStorePrefix      = "/keys"
+	VerificationPath    = "/verification"
 )
 
 // SSIServer exposes all dependencies needed to run a http server and all its services
@@ -97,6 +99,8 @@ func (s *SSIServer) instantiateRouter(service svcframework.Service) error {
 		return s.KeyStoreAPI(service)
 	case svcframework.Manifest:
 		return s.ManifestAPI(service)
+	case svcframework.Presentation:
+		return s.PresentationAPI(service)
 	default:
 		return fmt.Errorf("could not instantiate API for service: %s", serviceType)
 	}
@@ -151,6 +155,20 @@ func (s *SSIServer) CredentialAPI(service svcframework.Service) (err error) {
 	return
 }
 
+func (s *SSIServer) PresentationAPI(service svcframework.Service) (err error) {
+	pRouter, err := router.NewPresentationDefinitionRouter(service)
+	if err != nil {
+		return util.LoggingErrorMsg(err, "could not create credential router")
+	}
+
+	handlerPath := V1Prefix + PresentationsPrefix + DefinitionsPrefix
+
+	s.Handle(http.MethodPut, handlerPath, pRouter.CreatePresentationDefinition)
+	s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), pRouter.GetPresentationDefinition)
+	s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), pRouter.DeletePresentationDefinition)
+	return
+}
+
 func (s *SSIServer) KeyStoreAPI(service svcframework.Service) (err error) {
 	keyStoreRouter, err := router.NewKeyStoreRouter(service)
 	if err != nil {
diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go
index dec17f2..d357e3e 100644
--- a/pkg/service/framework/framework.go
+++ b/pkg/service/framework/framework.go
@@ -8,11 +8,12 @@ type (
 const (
 	// List of all service
 
-	DID        Type = "did"
-	Schema     Type = "schema"
-	Credential Type = "credential"
-	KeyStore   Type = "keystore"
-	Manifest   Type = "manifest"
+	DID          Type = "did"
+	Schema       Type = "schema"
+	Credential   Type = "credential"
+	KeyStore     Type = "keystore"
+	Manifest     Type = "manifest"
+	Presentation Type = "presentation"
 
 	StatusReady    StatusState = "ready"
 	StatusNotReady StatusState = "not_ready"
diff --git a/pkg/service/presentation/model.go b/pkg/service/presentation/model.go
new file mode 100644
index 0000000..cfad0ce
--- /dev/null
+++ b/pkg/service/presentation/model.go
@@ -0,0 +1,31 @@
+package presentation
+
+import (
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/TBD54566975/ssi-sdk/util"
+)
+
+type CreatePresentationDefinitionRequest struct {
+	PresentationDefinition exchange.PresentationDefinition `json:"presentationDefinition" validate:"required"`
+}
+
+func (cpr CreatePresentationDefinitionRequest) IsValid() bool {
+	return util.IsValidStruct(cpr) == nil
+}
+
+type CreatePresentationDefinitionResponse struct {
+	PresentationDefinition exchange.PresentationDefinition `json:"presentationDefinition"`
+}
+
+type GetPresentationDefinitionRequest struct {
+	ID string `json:"id" validate:"required"`
+}
+
+type GetPresentationDefinitionResponse struct {
+	ID                     string                          `json:"id"`
+	PresentationDefinition exchange.PresentationDefinition `json:"presentationDefinition"`
+}
+
+type DeletePresentationDefinitionRequest struct {
+	ID string `json:"id" validate:"required"`
+}
diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go
new file mode 100644
index 0000000..c98b6cb
--- /dev/null
+++ b/pkg/service/presentation/service.go
@@ -0,0 +1,105 @@
+package presentation
+
+import (
+	"fmt"
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/framework"
+	presentationstorage "github.com/tbd54566975/ssi-service/pkg/service/presentation/storage"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+)
+
+type Service struct {
+	storage presentationstorage.Storage
+	config  config.PresentationServiceConfig
+}
+
+func (s Service) Type() framework.Type {
+	return framework.Presentation
+}
+
+func (s Service) Status() framework.Status {
+	ae := sdkutil.NewAppendError()
+	if s.storage == nil {
+		ae.AppendString("no storage configured")
+	}
+	if !ae.IsEmpty() {
+		return framework.Status{
+			Status:  framework.StatusNotReady,
+			Message: fmt.Sprintf("presentation service is not ready: %s", ae.Error().Error()),
+		}
+	}
+	return framework.Status{Status: framework.StatusReady}
+}
+
+func (s Service) Config() config.PresentationServiceConfig {
+	return s.config
+}
+
+func NewPresentationDefinitionService(config config.PresentationServiceConfig, s storage.ServiceStorage) (*Service, error) {
+	presentationStorage, err := presentationstorage.NewPresentationStorage(s)
+	if err != nil {
+		errMsg := "could not instantiate storage for the presentation definition service"
+		return nil, util.LoggingErrorMsg(err, errMsg)
+	}
+	service := Service{
+		storage: presentationStorage,
+		config:  config,
+	}
+	if !service.Status().IsReady() {
+		return nil, errors.New(service.Status().Message)
+	}
+	return &service, nil
+}
+
+// CreatePresentationDefinition houses the main service logic for presentation definition creation. It validates the input, and
+// produces a presentation definition value that conforms with the PresentationDefinition specification.
+func (s Service) CreatePresentationDefinition(request CreatePresentationDefinitionRequest) (*CreatePresentationDefinitionResponse, error) {
+	logrus.Debugf("creating presentation definition: %+v", request)
+
+	if !request.IsValid() {
+		return nil, util.LoggingNewErrorf("invalid create presentation definition request: %+v", request)
+	}
+
+	if err := exchange.IsValidPresentationDefinition(request.PresentationDefinition); err != nil {
+		return nil, util.LoggingErrorMsg(err, "provided value is not a valid presentation definition")
+	}
+
+	storedPresentation := presentationstorage.StoredPresentation{ID: request.PresentationDefinition.ID, PresentationDefinition: request.PresentationDefinition}
+
+	if err := s.storage.StorePresentation(storedPresentation); err != nil {
+		return nil, util.LoggingErrorMsg(err, "could not store presentation")
+	}
+
+	return &CreatePresentationDefinitionResponse{
+		PresentationDefinition: storedPresentation.PresentationDefinition,
+	}, nil
+}
+
+func (s Service) GetPresentationDefinition(request GetPresentationDefinitionRequest) (*GetPresentationDefinitionResponse, error) {
+	logrus.Debugf("getting presentation definition: %s", request.ID)
+
+	storedPresentation, err := s.storage.GetPresentation(request.ID)
+	if err != nil {
+		return nil, util.LoggingErrorMsgf(err, "error getting presentation definition: %s", request.ID)
+	}
+	if storedPresentation == nil {
+		return nil, util.LoggingNewErrorf("presentation definition with id<%s> could not be found", request.ID)
+	}
+	return &GetPresentationDefinitionResponse{ID: storedPresentation.ID, PresentationDefinition: storedPresentation.PresentationDefinition}, nil
+}
+
+func (s Service) DeletePresentationDefinition(request DeletePresentationDefinitionRequest) error {
+	logrus.Debugf("deleting presentation definition: %s", request.ID)
+
+	if err := s.storage.DeletePresentation(request.ID); err != nil {
+		return util.LoggingNewErrorf("could not delete presentation definition with id: %s", request.ID)
+	}
+
+	return nil
+}
diff --git a/pkg/service/presentation/storage/bolt.go b/pkg/service/presentation/storage/bolt.go
new file mode 100644
index 0000000..9013695
--- /dev/null
+++ b/pkg/service/presentation/storage/bolt.go
@@ -0,0 +1,72 @@
+package storage
+
+import (
+	"fmt"
+
+	"github.com/goccy/go-json"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+)
+
+const (
+	namespace = "presentation_definition"
+)
+
+type BoltPresentationStorage struct {
+	db *storage.BoltDB
+}
+
+func NewBoltPresentationStorage(db *storage.BoltDB) (*BoltPresentationStorage, error) {
+	if db == nil {
+		return nil, errors.New("bolt db reference is nil")
+	}
+	return &BoltPresentationStorage{db: db}, nil
+}
+
+func (b BoltPresentationStorage) StorePresentation(presentation StoredPresentation) error {
+	id := presentation.ID
+	if id == "" {
+		err := errors.New("could not store presentation definition without an ID")
+		logrus.WithError(err).Error()
+		return err
+	}
+	jsonBytes, err := json.Marshal(presentation)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not store presentation definition: %s", id)
+		logrus.WithError(err).Error(errMsg)
+		return errors.Wrapf(err, errMsg)
+	}
+	return b.db.Write(namespace, id, jsonBytes)
+}
+
+func (b BoltPresentationStorage) GetPresentation(id string) (*StoredPresentation, error) {
+	jsonBytes, err := b.db.Read(namespace, id)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not get presentation definition: %s", id)
+		logrus.WithError(err).Error(errMsg)
+		return nil, errors.Wrapf(err, errMsg)
+	}
+	if len(jsonBytes) == 0 {
+		err := fmt.Errorf("presentation definition not found with id: %s", id)
+		logrus.WithError(err).Error("could not get presentation definition from storage")
+		return nil, err
+	}
+	var stored StoredPresentation
+	if err := json.Unmarshal(jsonBytes, &stored); err != nil {
+		errMsg := fmt.Sprintf("could not unmarshal stored presentation definition: %s", id)
+		logrus.WithError(err).Error(errMsg)
+		return nil, errors.Wrapf(err, errMsg)
+	}
+	return &stored, nil
+}
+
+func (b BoltPresentationStorage) DeletePresentation(id string) error {
+	if err := b.db.Delete(namespace, id); err != nil {
+		errMsg := fmt.Sprintf("could not delete presentation definition: %s", id)
+		logrus.WithError(err).Error(errMsg)
+		return errors.Wrapf(err, errMsg)
+	}
+	return nil
+}
diff --git a/pkg/service/presentation/storage/storage.go b/pkg/service/presentation/storage/storage.go
new file mode 100644
index 0000000..5f4c193
--- /dev/null
+++ b/pkg/service/presentation/storage/storage.go
@@ -0,0 +1,36 @@
+package storage
+
+import (
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+)
+
+type StoredPresentation struct {
+	ID                     string                          `json:"id"`
+	PresentationDefinition exchange.PresentationDefinition `json:"presentationDefinition"`
+}
+
+type Storage interface {
+	StorePresentation(schema StoredPresentation) error
+	GetPresentation(id string) (*StoredPresentation, error)
+	DeletePresentation(id string) error
+}
+
+// NewPresentationStorage finds the presentation storage impl for a given ServiceStorage value
+func NewPresentationStorage(s storage.ServiceStorage) (Storage, error) {
+	switch s.Type() {
+	case storage.Bolt:
+		gotBolt, ok := s.(*storage.BoltDB)
+		if !ok {
+			return nil, util.LoggingNewErrorf("trouble instantiating : %s", s.Type())
+		}
+		boltStorage, err := NewBoltPresentationStorage(gotBolt)
+		if err != nil {
+			return nil, util.LoggingErrorMsg(err, "could not instantiate schema bolt storage")
+		}
+		return boltStorage, err
+	default:
+		return nil, util.LoggingNewErrorf("unsupported storage type: %s", s.Type())
+	}
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 29b27f8..7e30f9b 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -2,6 +2,7 @@ package service
 
 import (
 	"fmt"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
 
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/internal/util"
@@ -51,6 +52,9 @@ func validateServiceConfig(config config.ServicesConfig) error {
 	if config.ManifestConfig.IsEmpty() {
 		return fmt.Errorf("%s no config provided", framework.Manifest)
 	}
+	if config.PresentationConfig.IsEmpty() {
+		return fmt.Errorf("%s no config provided", framework.Presentation)
+	}
 	return nil
 }
 
@@ -92,5 +96,10 @@ func instantiateServices(config config.ServicesConfig) ([]framework.Service, err
 		return nil, util.LoggingErrorMsg(err, "could not instantiate the manifest service")
 	}
 
-	return []framework.Service{keyStoreService, didService, schemaService, credentialService, manifestService}, nil
+	presentationService, err := presentation.NewPresentationDefinitionService(config.PresentationConfig, storageProvider)
+	if err != nil {
+		return nil, util.LoggingErrorMsg(err, "could not instantiate the presentation service")
+	}
+
+	return []framework.Service{keyStoreService, didService, schemaService, credentialService, manifestService, presentationService}, nil
 }


Test patch:
diff --git a/pkg/server/router/presentation_test.go b/pkg/server/router/presentation_test.go
new file mode 100644
index 0000000..c9dde92
--- /dev/null
+++ b/pkg/server/router/presentation_test.go
@@ -0,0 +1,93 @@
+package router
+
+import (
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/stretchr/testify/assert"
+	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"os"
+	"testing"
+)
+
+func TestPresentationDefinitionRouter(t *testing.T) {
+	t.Run("Nil Service", func(tt *testing.T) {
+		pdRouter, err := NewPresentationDefinitionRouter(nil)
+		assert.Error(tt, err)
+		assert.Empty(tt, pdRouter)
+		assert.Contains(tt, err.Error(), "service cannot be nil")
+	})
+
+	t.Run("Bad Service", func(tt *testing.T) {
+		pdRouter, err := NewPresentationDefinitionRouter(&testService{})
+		assert.Error(tt, err)
+		assert.Empty(tt, pdRouter)
+		assert.Contains(tt, err.Error(), "could not create presentation router with service type: test")
+	})
+}
+
+func TestPresentationDefinitionService(t *testing.T) {
+	t.Cleanup(func() {
+		_ = os.Remove(storage.DBFile)
+	})
+
+	s, err := storage.NewStorage(storage.Bolt)
+	assert.NoError(t, err)
+
+	service, err := presentation.NewPresentationDefinitionService(config.PresentationServiceConfig{}, s)
+	assert.NoError(t, err)
+
+	t.Run("Create returns the created definition", func(t *testing.T) {
+		pd := createPresentationDefinition(t)
+		created, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		assert.NoError(t, err)
+		assert.Equal(t, pd, &created.PresentationDefinition)
+	})
+
+	t.Run("Get returns the created definition", func(t *testing.T) {
+		pd := createPresentationDefinition(t)
+		_, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		assert.NoError(t, err)
+
+		getPd, err := service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: pd.ID})
+
+		assert.NoError(t, err)
+		assert.Equal(t, pd.ID, getPd.ID)
+		assert.Equal(t, pd, &getPd.PresentationDefinition)
+	})
+
+	t.Run("Get does not return after deletion", func(t *testing.T) {
+		pd := createPresentationDefinition(t)
+		_, err := service.CreatePresentationDefinition(presentation.CreatePresentationDefinitionRequest{PresentationDefinition: *pd})
+		assert.NoError(t, err)
+
+		assert.NoError(t, service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: pd.ID}))
+
+		_, err = service.GetPresentationDefinition(presentation.GetPresentationDefinitionRequest{ID: pd.ID})
+		assert.Error(t, err)
+	})
+
+	t.Run("Delete can be called with any ID", func(t *testing.T) {
+		err := service.DeletePresentationDefinition(presentation.DeletePresentationDefinitionRequest{ID: "some crazy ID"})
+		assert.NoError(t, err)
+	})
+}
+
+func createPresentationDefinition(t *testing.T) *exchange.PresentationDefinition {
+	builder := exchange.NewPresentationDefinitionBuilder()
+	assert.NoError(t, builder.SetInputDescriptors([]exchange.InputDescriptor{
+		{
+			ID:      "id",
+			Name:    "name",
+			Purpose: "purpose",
+			Format: &exchange.ClaimFormat{
+				JWT: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}},
+			},
+			Constraints: &exchange.Constraints{SubjectIsIssuer: exchange.Preferred.Ptr()},
+		},
+	}))
+	pd, err := builder.Build()
+	assert.NoError(t, err)
+	return pd
+}
diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go
new file mode 100644
index 0000000..09f59b8
--- /dev/null
+++ b/pkg/server/server_presentation_test.go
@@ -0,0 +1,139 @@
+package server
+
+import (
+	"fmt"
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/goccy/go-json"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/stretchr/testify/assert"
+	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/pkg/server/router"
+	"github.com/tbd54566975/ssi-service/pkg/service/presentation"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+)
+
+func TestPresentationAPI(t *testing.T) {
+	builder := exchange.NewPresentationDefinitionBuilder()
+	inputDescriptors := []exchange.InputDescriptor{
+		{
+			ID:      "id",
+			Name:    "name",
+			Purpose: "purpose",
+			Format: &exchange.ClaimFormat{
+				JWT: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}},
+			},
+			Constraints: &exchange.Constraints{SubjectIsIssuer: exchange.Preferred.Ptr()},
+		},
+	}
+	assert.NoError(t, builder.SetInputDescriptors(inputDescriptors))
+	assert.NoError(t, builder.SetName("name"))
+	assert.NoError(t, builder.SetPurpose("purpose"))
+	pd, err := builder.Build()
+	assert.NoError(t, err)
+
+	s, err := storage.NewStorage(storage.Bolt)
+	assert.NoError(t, err)
+
+	service, err := presentation.NewPresentationDefinitionService(config.PresentationServiceConfig{}, s)
+	assert.NoError(t, err)
+
+	pRouter, err := router.NewPresentationDefinitionRouter(service)
+	assert.NoError(t, err)
+
+	t.Cleanup(func() {
+		_ = s.Close()
+		_ = os.Remove(storage.DBFile)
+	})
+
+	t.Run("Create, Get, and Delete PresentationDefinition", func(t *testing.T) {
+		var createdID string
+		{
+			// Create returns the expected PD.
+			request := router.CreatePresentationDefinitionRequest{
+				Name:                   "name",
+				Purpose:                "purpose",
+				Format:                 nil,
+				SubmissionRequirements: nil,
+				InputDescriptors:       inputDescriptors,
+			}
+			value := newRequestValue(t, request)
+			req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value)
+			w := httptest.NewRecorder()
+
+			err = pRouter.CreatePresentationDefinition(newRequestContext(), w, req)
+			assert.NoError(t, err)
+
+			var resp router.CreatePresentationDefinitionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" {
+				t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff)
+			}
+
+			w.Flush()
+			createdID = resp.PresentationDefinition.ID
+		}
+		{
+			// We can get the PD after it's created.
+			req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil)
+			w := httptest.NewRecorder()
+			assert.NoError(t, pRouter.GetPresentationDefinition(newRequestContextWithParams(map[string]string{"id": createdID}), w, req))
+
+			var resp router.GetPresentationDefinitionResponse
+			assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
+			assert.Equal(t, createdID, resp.PresentationDefinition.ID)
+			if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" {
+				t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff)
+			}
+
+			w.Flush()
+		}
+		{
+			// The PD can be deleted.
+			req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil)
+			w := httptest.NewRecorder()
+			assert.NoError(t, pRouter.DeletePresentationDefinition(newRequestContextWithParams(map[string]string{"id": createdID}), w, req))
+
+			w.Flush()
+		}
+		{
+			// And we cannot get the PD after it's been deleted.
+			req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", createdID), nil)
+			w := httptest.NewRecorder()
+			assert.Error(t, pRouter.GetPresentationDefinition(newRequestContextWithParams(map[string]string{"id": createdID}), w, req))
+
+			w.Flush()
+		}
+	})
+
+	t.Run("Create returns error without input descriptors", func(t *testing.T) {
+		request := router.CreatePresentationDefinitionRequest{}
+		value := newRequestValue(t, request)
+		req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value)
+		w := httptest.NewRecorder()
+
+		err = pRouter.CreatePresentationDefinition(newRequestContext(), w, req)
+
+		assert.Error(t, err)
+		w.Flush()
+	})
+
+	t.Run("Get without an ID returns error", func(t *testing.T) {
+		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
+		w := httptest.NewRecorder()
+		assert.Error(t, pRouter.GetPresentationDefinition(newRequestContext(), w, req))
+		w.Flush()
+	})
+
+	t.Run("Delete without an ID returns error", func(t *testing.T) {
+		req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil)
+		w := httptest.NewRecorder()
+		assert.Error(t, pRouter.DeletePresentationDefinition(newRequestContext(), w, req))
+		w.Flush()
+	})
+}