|
| 1 | +# Keyper Approval Workflow |
| 2 | + |
| 3 | +> **Note:** This documentation describes the **future state** of the GDS platform. Some features and `gds-preview` links are not yet available. |
| 4 | +
|
| 5 | +This guide is for **David** — a data service consumer who needs to request building owner approval before accessing building sensor data. It covers how to use the Keyper API to create approval requests and track their status. |
| 6 | + |
| 7 | +## When to use this |
| 8 | + |
| 9 | +[Requesting API Access](requesting-api-access.md) grants your application the ability to *call* a provider's API. This guide covers the next step: obtaining **data-level authorization** for specific buildings through building owner approval. |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +| Requirement | Description | |
| 14 | +|-------------|-------------| |
| 15 | +| API access approved | Charlie has approved your application's access to their API | |
| 16 | +| Keyper credentials | OAuth2 client credentials for Keyper (contact Poort8 at hello@poort8.nl) | |
| 17 | +| Building details known | VBO ID of the target building and building owner's organization | |
| 18 | + |
| 19 | +## Overview |
| 20 | + |
| 21 | +```likec4 |
| 22 | +// view: approval_workflow |
| 23 | +specification { |
| 24 | + element actor |
| 25 | + element system |
| 26 | +} |
| 27 | +
|
| 28 | +model { |
| 29 | + alice = actor 'Building Manager (Alice)' |
| 30 | + david = actor 'Your Platform' |
| 31 | + keyper = system 'Keyper Approve' |
| 32 | + bob = actor 'Building Owner (Bob)' |
| 33 | + ar = system 'GDS Authorization Registry' |
| 34 | +} |
| 35 | +
|
| 36 | +views { |
| 37 | + dynamic view approval_workflow { |
| 38 | + title 'Keyper Approval Workflow' |
| 39 | + variant sequence |
| 40 | +
|
| 41 | + alice -> david 'Requests access to building data' |
| 42 | + david -> keyper 'POST /v1/api/approval-links' |
| 43 | + keyper -> david '201 Created (approval link ID)' |
| 44 | + keyper -> bob 'Email with approval link' |
| 45 | + bob -> keyper 'Opens link, reviews request' |
| 46 | + bob -> keyper 'Approves + enters verification code' |
| 47 | + keyper -> ar 'Registers policies' |
| 48 | + ar -> keyper 'Policies stored' |
| 49 | + david -> keyper 'GET /v1/api/approval-links/{id}' |
| 50 | + keyper -> david 'Status = Approved' |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +## Step 1 — Authenticate with Keyper |
| 56 | + |
| 57 | +Obtain an access token for the Keyper API: |
| 58 | + |
| 59 | +```http |
| 60 | +POST https://poort8.eu.auth0.com/oauth/token |
| 61 | +Content-Type: application/json |
| 62 | +
|
| 63 | +{ |
| 64 | + "client_id": "<YOUR_KEYPER_CLIENT_ID>", |
| 65 | + "client_secret": "<YOUR_KEYPER_CLIENT_SECRET>", |
| 66 | + "audience": "Poort8-Dataspace-Keyper-Preview", |
| 67 | + "grant_type": "client_credentials" |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +Response: |
| 72 | +```json |
| 73 | +{ |
| 74 | + "access_token": "eyJhbGciOiJSUzI1NiIs...", |
| 75 | + "token_type": "Bearer", |
| 76 | + "expires_in": 3600 |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +Store the token and reuse it for multiple requests until it expires (1 hour). |
| 81 | + |
| 82 | +## Step 2 — Prepare required data |
| 83 | + |
| 84 | +Gather the following before creating an approval request: |
| 85 | + |
| 86 | +### Requester information (your platform's user) |
| 87 | + |
| 88 | +| Field | Description | Example | |
| 89 | +|-------|-------------|---------| |
| 90 | +| `name` | Person initiating the request | `Alice Anderson` | |
| 91 | +| `email` | Their email address | `alice@building-opt.nl` | |
| 92 | +| `organization` | Consumer organization name | `Building Optimization Corp` | |
| 93 | +| `organizationId` | Consumer organization EUID | `NLNHR.12345678` | |
| 94 | + |
| 95 | +### Approver information (building owner) |
| 96 | + |
| 97 | +| Field | Description | Example | |
| 98 | +|-------|-------------|---------| |
| 99 | +| `name` | Building owner who will approve | `Bob Johnson` | |
| 100 | +| `email` | Where the approval link is sent | `bob@building-owner.nl` | |
| 101 | +| `organization` | Building owner's organization name | `Johnson Properties BV` | |
| 102 | +| `organizationId` | Building owner's organization EUID | `NLNHR.87654321` | |
| 103 | + |
| 104 | +### Building and policy information |
| 105 | + |
| 106 | +| Field | Description | Example | |
| 107 | +|-------|-------------|---------| |
| 108 | +| `resourceId` | VBO ID (16 digits) from the Dutch BAG registry | `0363010000659001` | |
| 109 | +| `serviceProvider` | IoT platform's organization EUID | `NLNHR.23456789` | |
| 110 | +| `action` | `GET` (read) or `POST` (write/control) | `GET` | |
| 111 | + |
| 112 | +> **VBO ID lookup:** Use the [PDOK BAG API ➚](https://www.pdok.nl/introductie/-/article/basisregistratie-adressen-en-gebouwen-ba-1) to find VBO IDs by building address. |
| 113 | +
|
| 114 | +## Step 3 — Understand policy levels |
| 115 | + |
| 116 | +| Level | Resource type | Resource ID | Actions | Description | |
| 117 | +|-------|--------------|-------------|---------|-------------| |
| 118 | +| Building | `building` | VBO ID | `GET` / `POST` | Access to entire building and all assets | |
| 119 | +| Asset | `asset` | Asset ID | `GET` / `POST` | Access to a specific sensor or control point | |
| 120 | + |
| 121 | +**Action semantics:** |
| 122 | +- `GET` — Read data (sensor measurements, metadata) |
| 123 | +- `POST` — Write data or send control commands. Implicitly grants `GET` access |
| 124 | + |
| 125 | +> **Pilot scope:** During the test phase, only a `GET` policy on building level (VBO ID) is used. |
| 126 | +
|
| 127 | +### Bundling policies |
| 128 | + |
| 129 | +You can request multiple permissions in a single approval request. The building owner receives one email and makes one approval decision for all bundled policies. |
| 130 | + |
| 131 | +Example: bundle `GET` and `POST` on the same building, or request access to multiple resource types. |
| 132 | + |
| 133 | +## Step 4 — Create approval request |
| 134 | + |
| 135 | +```http |
| 136 | +POST https://keyper-preview.poort8.nl/v1/api/approval-links |
| 137 | +Authorization: Bearer <ACCESS_TOKEN> |
| 138 | +Content-Type: application/json |
| 139 | +``` |
| 140 | + |
| 141 | +### Request body |
| 142 | + |
| 143 | +```json |
| 144 | +{ |
| 145 | + "requester": { |
| 146 | + "name": "Alice Anderson", |
| 147 | + "email": "alice@building-opt.nl", |
| 148 | + "organization": "Building Optimization Corp", |
| 149 | + "organizationId": "NLNHR.12345678" |
| 150 | + }, |
| 151 | + "approver": { |
| 152 | + "name": "Bob Johnson", |
| 153 | + "email": "bob@building-owner.nl", |
| 154 | + "organization": "Johnson Properties BV", |
| 155 | + "organizationId": "NLNHR.87654321" |
| 156 | + }, |
| 157 | + "dataspace": { |
| 158 | + "baseUrl": "https://gds-preview.poort8.nl" |
| 159 | + }, |
| 160 | + "reference": "SENSOR-OPT-2025-Q4-001", |
| 161 | + "addPolicyTransactions": [ |
| 162 | + { |
| 163 | + "useCase": "ishare", |
| 164 | + "license": "0005", |
| 165 | + "type": "building", |
| 166 | + "issuerId": "NLNHR.87654321", |
| 167 | + "subjectId": "NLNHR.12345678", |
| 168 | + "serviceProvider": "NLNHR.23456789", |
| 169 | + "action": "GET", |
| 170 | + "resourceId": "0363010000659001", |
| 171 | + "attribute": "*", |
| 172 | + "notBefore": 1730736000, |
| 173 | + "expiration": 2147483647 |
| 174 | + } |
| 175 | + ], |
| 176 | + "orchestration": { |
| 177 | + "flow": "gds.klimaatsensoren@v1" |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +### Key fields |
| 183 | + |
| 184 | +| Field | Description | |
| 185 | +|-------|-------------| |
| 186 | +| `dataspace.baseUrl` | GDS Authorization Registry URL | |
| 187 | +| `reference` | Your internal tracking reference | |
| 188 | +| `addPolicyTransactions` | Array of policies to register upon approval | |
| 189 | +| `orchestration.flow` | Approval workflow identifier (`gds.klimaatsensoren@v1`) | |
| 190 | +| `notBefore` | Policy start time (Unix timestamp) | |
| 191 | +| `expiration` | Policy end time (`2147483647` for no expiration) | |
| 192 | + |
| 193 | +## Step 5 — Handle the response |
| 194 | + |
| 195 | +### Success (201 Created) |
| 196 | + |
| 197 | +```json |
| 198 | +{ |
| 199 | + "id": "474e19af-8165-4b85-ad03-be81f9f8dcc2", |
| 200 | + "reference": "SENSOR-OPT-2025-Q4-001", |
| 201 | + "url": "https://keyper-preview.poort8.nl/approve/474e19af-8165-4b85-ad03-be81f9f8dcc2", |
| 202 | + "expiresAtUtc": 1730739600, |
| 203 | + "status": "Active" |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +| Field | Description | |
| 208 | +|-------|-------------| |
| 209 | +| `id` | Unique approval link ID | |
| 210 | +| `reference` | Your tracking reference (echoed) | |
| 211 | +| `url` | Approval link sent to building owner | |
| 212 | +| `expiresAtUtc` | When the link expires (Unix timestamp) | |
| 213 | +| `status` | Current status | |
| 214 | + |
| 215 | +**What happens next:** |
| 216 | +1. Keyper emails Bob with the approval link |
| 217 | +2. Bob opens the link and reviews the request |
| 218 | +3. Bob approves or rejects |
| 219 | +4. If approved, Bob enters an email verification code |
| 220 | +5. Keyper registers policies in the GDS Authorization Registry |
| 221 | + |
| 222 | +### Error responses |
| 223 | + |
| 224 | +| Code | Cause | Action | |
| 225 | +|------|-------|--------| |
| 226 | +| `400 Bad Request` | Missing or invalid fields | Check `errors` object, correct fields, retry | |
| 227 | +| `401 Unauthorized` | Missing or expired token | Request a new token, retry | |
| 228 | +| `500 Internal Server Error` | Server-side error | Retry after short delay; contact Poort8 if persistent | |
| 229 | + |
| 230 | +## Step 6 — Check approval status |
| 231 | + |
| 232 | +Poll the approval link status: |
| 233 | + |
| 234 | +```http |
| 235 | +GET https://keyper-preview.poort8.nl/v1/api/approval-links/{APPROVAL_LINK_ID} |
| 236 | +Authorization: Bearer <ACCESS_TOKEN> |
| 237 | +``` |
| 238 | + |
| 239 | +Response: |
| 240 | +```json |
| 241 | +{ |
| 242 | + "id": "474e19af-8165-4b85-ad03-be81f9f8dcc2", |
| 243 | + "reference": "SENSOR-OPT-2025-Q4-001", |
| 244 | + "status": "Approved" |
| 245 | +} |
| 246 | +``` |
| 247 | + |
| 248 | +> **Note:** The GET response returns the full approval link entity. The `status` field is the most relevant for polling purposes. |
| 249 | +
|
| 250 | +**Status values:** |
| 251 | + |
| 252 | +| Status | Meaning | |
| 253 | +|--------|---------| |
| 254 | +| `Active` | Awaiting building owner's decision | |
| 255 | +| `Approved` | Approved — policies are registered | |
| 256 | +| `Rejected` | Building owner rejected the request | |
| 257 | +| `Expired` | Link expired before a decision was made | |
| 258 | + |
| 259 | +**Polling strategy:** Check every 5–10 minutes while status is `Active`. Stop when status changes. |
| 260 | + |
| 261 | +## After approval |
| 262 | + |
| 263 | +Once status is `Approved`: |
| 264 | +1. Policies are registered in the GDS Authorization Registry |
| 265 | +2. Your platform can request building data from Charlie's API |
| 266 | +3. Charlie will verify the policy via [Authorization Enforcement](authorization.md) and deliver data |
| 267 | + |
| 268 | +## Support |
| 269 | + |
| 270 | +For questions or issues with the Keyper approval workflow: |
| 271 | + |
| 272 | +- **Contact:** hello@poort8.nl |
| 273 | +- **API reference:** [Keyper API documentation ➚](https://keyper-preview.poort8.nl/scalar/?api=v1) |
0 commit comments