diff --git a/public/llms.txt b/public/llms.txt index 68276138..56ee089c 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -18,8 +18,15 @@ - [sdks/go](https://docs.phase.dev/sdks/go.md) - [sdks/node](https://docs.phase.dev/sdks/node.md) - [public-api](https://docs.phase.dev/public-api.md) +- [public-api/apps](https://docs.phase.dev/public-api/apps.md) +- [public-api/environments](https://docs.phase.dev/public-api/environments.md) - [public-api/secrets](https://docs.phase.dev/public-api/secrets.md) - [public-api/dynamic-secrets](https://docs.phase.dev/public-api/dynamic-secrets.md) +- [public-api/members](https://docs.phase.dev/public-api/members.md) +- [public-api/invites](https://docs.phase.dev/public-api/invites.md) +- [public-api/service-accounts](https://docs.phase.dev/public-api/service-accounts.md) +- [public-api/teams](https://docs.phase.dev/public-api/teams.md) +- [public-api/roles](https://docs.phase.dev/public-api/roles.md) - [public-api/external-identities](https://docs.phase.dev/public-api/external-identities.md) - [public-api/errors](https://docs.phase.dev/public-api/errors.md) - [access-control](https://docs.phase.dev/access-control.md) diff --git a/public/public-api.md b/public/public-api.md index 4ee5c4d2..a6b3b8ea 100644 --- a/public/public-api.md +++ b/public/public-api.md @@ -19,7 +19,9 @@ You can use the Phase public REST API to access and manage secrets via a simple The Phase API is organized around [REST](https://en.wikipedia.org/wiki/Representational_State_Transfer). The API accepts data in the request body only in JSON-encoded format. It uses standard HTTP methods and response codes. -The API also returns specific error messages when something goes wrong. Check out the API [errors page](/public-api/errors) for more details. +Supported HTTP methods are `GET`, `POST`, `PUT`, and `DELETE`. `PATCH` is not supported on any endpoint and returns `405 Method Not Allowed`. + +Error responses are always JSON of the form `{"error": ""}`. Check out the API [errors page](/public-api/errors) for more details. ## Base URL diff --git a/public/public-api/apps.md b/public/public-api/apps.md new file mode 100644 index 00000000..6a9fd843 --- /dev/null +++ b/public/public-api/apps.md @@ -0,0 +1,397 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Apps API', + description: + 'Explore the Phase Apps API for managing applications programmatically.', +} + +API + +# Apps + +Apps are the top-level organizational unit in Phase. Each App contains Environments, which in turn hold Secrets. On this page, we'll look at the Apps API endpoints for listing, creating, updating, and deleting Apps. {{ className: 'lead' }} + + +Apps created via the API are SSE-enabled by default. The list endpoint returns metadata for all apps you have access to (SSE and E2EE) — check the `sseEnabled` field. Write operations against secrets and environments via the REST API require SSE; E2EE apps return `400 Bad Request`. + + + + +## The App model + +### Properties + + + + Unique identifier for the app. + + + The name of the app. + + + An optional description for the app. + + + Whether server-side encryption is enabled for this app. + + + Timestamp of when the app was created. + + + Timestamp of when the app was last updated. + + + +--- + +## List Apps {{ tag: 'GET', label: '/v1/apps' }} + + + + + Retrieve metadata for all apps (SSE and E2EE) that the authenticated account has access to. Use the `sseEnabled` field to distinguish; only SSE apps support secrets and environment writes via the REST API. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/apps/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "58006442-007b-4625-b8e2-80f7606484a0", + "name": "My App", + "description": "Production application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create App {{ tag: 'POST', label: '/v1/apps' }} + + + + + Create a new SSE-enabled App. By default, three environments are created automatically: Development, Staging, and Production. You can optionally supply a custom list of environment names. + + ### JSON Body + + #### Required fields + + + + The app name. Maximum 64 characters. + + + + #### Optional fields + + + + A description for the app. Maximum 10,000 characters. + + + A list of custom environment names to create instead of the defaults. Each name must contain only letters, numbers, hyphens, and underscores. Requires a paid plan. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My New App", + "description": "A new application" + }' + ``` + + ```fish {{ title: 'cURL (custom environments)' }} + curl -X POST https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My New App", + "environments": ["dev", "test", "staging", "prod"] + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/apps/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'My New App', + 'description': 'A new application' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My New App", + "description": "A new application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "environments": [ + { + "id": "e7d9cc21-f83c-441f-8887-8720ceab4c7e", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "c5b998fb-09cf-48ef-808a-46ed08a1f0ab", + "name": "Staging", + "envType": "staging", + "index": 1, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "538ac0e3-236a-48af-962f-69fab6449c2e", + "name": "Production", + "envType": "prod", + "index": 2, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + The `environments` array reflects whatever was created — either the three defaults (Development/Staging/Production) or the custom names from the request body, in request order. Each entry has the same shape as `GET /v1/environments/:id`. + + + + +--- + +## Get App {{ tag: 'GET', label: '/v1/apps/:id' }} + + + + + Retrieve a single app by its ID. + + ### URL parameters + + + + The unique identifier of the app. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "description": "Production application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update App {{ tag: 'PUT', label: '/v1/apps/:id' }} + + + + + Update an app's name and/or description. At least one field must be provided. + + ### URL parameters + + + + The unique identifier of the app. + + + + ### JSON Body + + + + The new app name. Maximum 64 characters. + + + The new app description. Maximum 10,000 characters. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Updated App Name", + "description": "Updated description" + }' + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Updated App Name' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "Updated App Name", + "description": "Updated description", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T10:30:00Z" + } + ``` + + + + +--- + +## Delete App {{ tag: 'DELETE', label: '/v1/apps/:id' }} + + + + + Permanently delete an app and all its associated data. + + + This action is irreversible. All environments, secrets, and access configurations associated with the app will be permanently deleted. + + + ### URL parameters + + + + The unique identifier of the app. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/public/public-api/environments.md b/public/public-api/environments.md new file mode 100644 index 00000000..e7cb7bfa --- /dev/null +++ b/public/public-api/environments.md @@ -0,0 +1,380 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Environments API', + description: + 'Explore the Phase Environments API for managing environments programmatically.', +} + +API + +# Environments + +Environments represent deployment stages within an App (e.g. Development, Staging, Production). Each Environment contains its own set of Secrets. On this page, we'll look at the Environments API endpoints for listing, creating, updating, and deleting Environments. {{ className: 'lead' }} + + +The Environments API requires server-side encryption (SSE) to be enabled for the parent App. An `app_id` query parameter is required for list and create operations — omitting it returns `403` as the API cannot resolve the app context. + + + + +## The Environment model + +### Properties + + + + Unique identifier for the environment. + + + The name of the environment. + + + The type of environment: `dev`, `staging`, `prod`, or `custom`. + + + The display order of the environment within its app. + + + Timestamp of when the environment was created. + + + Timestamp of when the environment was last updated. + + + +--- + +## List Environments {{ tag: 'GET', label: '/v1/environments' }} + + + + + Retrieve all environments for a given app that the authenticated account has access to. + + ### Required parameters + + + + Unique identifier for the Phase App. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl "https://api.phase.dev/v1/environments/?app_id=72b9ddd5-8fce-49ab-89d9-c431d53a9552" \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/environments/' + params = { + 'app_id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + } + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, params=params, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", + "name": "Staging", + "envType": "staging", + "index": 1, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod", + "index": 2, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Environment {{ tag: 'POST', label: '/v1/environments' }} + + + + + Create a new environment within an app. The environment name must contain only letters, numbers, hyphens, and underscores (max 64 characters). + + ### Required parameters + + + + Unique identifier for the Phase App. + + + + ### JSON Body + + #### Required fields + + + + The environment name. Must match `[a-zA-Z0-9\-_]{1,64}`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST "https://api.phase.dev/v1/environments/?app_id=72b9ddd5-8fce-49ab-89d9-c431d53a9552" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "canary" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/environments/' + params = { + 'app_id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + } + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'canary' + } + + response = requests.post(url, params=params, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "d34e5f6a-7890-12cd-ef34-567890123456", + "name": "canary", + "envType": "custom", + "index": 3, + "createdAt": "2024-06-02T10:00:00Z", + "updatedAt": "2024-06-02T10:00:00Z" + } + ``` + + + + +--- + +## Get Environment {{ tag: 'GET', label: '/v1/environments/:id' }} + + + + + Retrieve a single environment by its ID. + + ### URL parameters + + + + The unique identifier of the environment. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update Environment {{ tag: 'PUT', label: '/v1/environments/:id' }} + + + + + Update an environment's name. + + ### URL parameters + + + + The unique identifier of the environment. + + + + ### JSON Body + + + + The new environment name. Must match `[a-zA-Z0-9\-_]{1,64}`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "dev-v2" + }' + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'dev-v2' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "dev-v2", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T11:00:00Z" + } + ``` + + + + +--- + +## Delete Environment {{ tag: 'DELETE', label: '/v1/environments/:id' }} + + + + + Permanently delete an environment and all its secrets. + + + This action is irreversible. All secrets and access keys in the environment will be permanently deleted. + + + ### URL parameters + + + + The unique identifier of the environment. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/public/public-api/errors.md b/public/public-api/errors.md index e5fba145..cef16707 100644 --- a/public/public-api/errors.md +++ b/public/public-api/errors.md @@ -27,8 +27,17 @@ Here is a list of the different categories of status codes returned by the Proto A 200 status code indicates a successful response. + + A 201 status code indicates that a new resource was created successfully (returned by POST endpoints). + + + A 204 status code indicates that the request succeeded with no response body. Used for DELETE endpoints. + - A 400 status code indicates a bad request. This is typically due to a request that's missing some data requried to process the request, such as missing `id` fields. + A 400 status code indicates a bad request. This is typically due to missing required fields, invalid input types, values exceeding length limits, or invalid email formats. + + + A 401 status code indicates that no authentication credentials were provided or the token has expired or been deleted. A 403 status code indicates an authentication or access error. Check your [authentication](/public-api#authentication) credentials if you see this error, and make sure the token you're using has the approriate scope for the App and Environment you're trying to access. @@ -36,8 +45,17 @@ Here is a list of the different categories of status codes returned by the Proto This error may also occur due to a [Network Access Policy](/access-control/network#network-access-policies) that restricts access from your IP address. [Read more](https://docs.phase.dev/access-control/network#access-denied-exceptions) about Network Access Policy exceptions. + + A 404 status code indicates that the requested resource does not exist, has been deleted, or belongs to a different organisation. The API does not distinguish between these cases to avoid leaking cross-organisation information. + + + A 405 status code indicates that the HTTP method is not supported by the endpoint. The Phase API supports `GET`, `POST`, `PUT`, and `DELETE` only — `PATCH` is not supported on any endpoint. + - A 409 status code indicates that the requested operation will create a conflict in your secret configuration. This is generally due to attempting to set the `key` of a secret at a given path where this key already exists. + A 409 status code indicates that the requested operation would conflict with the current state of the resource. Examples: attempting to set a secret `key` at a path where it already exists; inviting an email that already has a pending invite; deleting a role that has members or service accounts assigned to it. + + + A 429 status code indicates that you have exceeded the rate limit for your plan. The `retry-after` response header indicates how long to wait before retrying. See [rate limits](/public-api#rate-limits) for per-plan thresholds. A 5xx status code indicates a server error — something went wrong with the Phase API. diff --git a/public/public-api/invites.md b/public/public-api/invites.md new file mode 100644 index 00000000..a0e41868 --- /dev/null +++ b/public/public-api/invites.md @@ -0,0 +1,262 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Invites API', + description: + 'Explore the Phase Invites API for creating, listing, and cancelling pending organisation member invitations.', +} + +API + +# Invites + +Invites are pending membership requests sent to an email address. When an invite is accepted through the Phase console, the recipient becomes an organisation member with the assigned role. On this page, we'll look at the API endpoints for creating, listing, and cancelling pending invites. {{ className: 'lead' }} + + +Invites live under the Members resource — all endpoints are namespaced as `/v1/members/invites/`. The `POST /v1/members/` endpoint is reserved for the future direct-member-creation flow and currently returns `405 Method Not Allowed`. + + + + +## The Invite model + +### Properties + + + + Unique identifier for the invite. + + + The email address the invite was sent to. + + + The role that will be assigned on acceptance, with `id` and `name`. + + + Who sent the invite. Contains `type` (`"member"` or `"service_account"`) and either `email` (for members) or `name` (for service accounts). `null` if the sender has since been removed. + + + Timestamp of when the invite was created. + + + Timestamp of when the invite expires. Invites are valid for 14 days. + + + Whether the invite is still valid (has not been cancelled or accepted). + + + +--- + +## Create Invite {{ tag: 'POST', label: '/v1/members/invites' }} + + + + + Send an invitation to a new member. An invite email is sent to the specified address, and the invite expires after 14 days. + + + This endpoint creates an **invite**, not a direct membership. The invited user must accept the invite via the console to become a member. + + + ### Constraints + + - The role must not have global access (i.e. Owner and Admin roles cannot be invited to). + - The role must not permit creating service account tokens. + - The email is validated against RFC format; whitespace is trimmed and the local + domain parts are lowercased. Invalid emails return `400 Bad Request`. + - The email must not already belong to an active member or a pending invite. Duplicate invites return `409 Conflict` with `{"error": "An active invite already exists for ''."}`. + + + App and environment access cannot be scoped at invite time. Because Phase is end-to-end encrypted, granting access requires the invitee's identity key, which doesn't exist until they accept the invite and complete their key ceremony. Use [Manage Access](/public-api/members#manage-access) after acceptance. Sending an `apps` field returns `400 Bad Request`. + + + ### JSON Body + + #### Required fields + + + + The email address of the person to invite. + + + The ID of the role to assign on acceptance. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/members/invites/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "bob@example.com", + "role_id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/invites/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'email': 'bob@example.com', + 'role_id': '6aec9df5-cd75-4645-a9d0-8b6f6aff78d6' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "inviteeEmail": "bob@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "invitedBy": { + "type": "member", + "email": "alice@example.com" + }, + "createdAt": "2024-06-02T10:00:00Z", + "expiresAt": "2024-06-16T10:00:00Z", + "valid": true + } + ``` + + + + +--- + +## List Invites {{ tag: 'GET', label: '/v1/members/invites' }} + + + + + Retrieve all pending (valid, non-expired) invites for the organisation, ordered by most recent first. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/invites/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/invites/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "inviteeEmail": "bob@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "invitedBy": { + "type": "member", + "email": "alice@example.com" + }, + "createdAt": "2024-06-02T10:00:00Z", + "expiresAt": "2024-06-16T10:00:00Z", + "valid": true + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "inviteeEmail": "carol@example.com", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Manager" + }, + "invitedBy": { + "type": "service_account", + "name": "deploy-bot" + }, + "createdAt": "2024-06-01T08:00:00Z", + "expiresAt": "2024-06-15T08:00:00Z", + "valid": true + } + ] + } + ``` + + + + +--- + +## Cancel Invite {{ tag: 'DELETE', label: '/v1/members/invites/:id' }} + + + + + Cancel a pending invite. The invite is immediately invalidated and the invitee can no longer use the invite link to join the organisation. + + ### URL parameters + + + + The unique identifier of the invite. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/members/invites/a1b2c3d4-e5f6-7890-abcd-ef1234567890/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + invite_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + url = f'https://api.phase.dev/v1/members/invites/{invite_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/public/public-api/members.md b/public/public-api/members.md new file mode 100644 index 00000000..233b35af --- /dev/null +++ b/public/public-api/members.md @@ -0,0 +1,513 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Members API', + description: + 'Explore the Phase Members API for managing organisation members and their access programmatically.', +} + +API + +# Members + +Organisation members are human users who belong to your Phase organisation, each assigned a Role that governs their permissions. On this page, we'll look at the API endpoints for listing members, updating roles, managing app and environment access, and removing members. {{ className: 'lead' }} + + +To add a new member, send an invite via the [Invites API](/public-api/invites) — the invitee accepts the invite and completes a client-side key ceremony before they become an active organisation member. The `POST /v1/members/` endpoint is reserved for the future direct-member-creation flow and currently returns `405 Method Not Allowed`. + + + + +## The Member model + +### Properties + + + + Unique identifier for the organisation membership. + + + The member's username. + + + The member's full name, populated from their OAuth profile if available. + + + The member's email address. + + + The assigned role, with `id` and `name`. + + + Timestamp of when the member joined the organisation. + + + Timestamp of when the membership was last updated. + + + +--- + +## List Members {{ tag: 'GET', label: '/v1/members' }} + + + + + Retrieve all active members of the organisation. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Get Member {{ tag: 'GET', label: '/v1/members/:id' }} + + + + + Retrieve a single member by their membership ID. + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update Member Role {{ tag: 'PUT', label: '/v1/members/:id' }} + + + + + Update a member's assigned role. + + ### Constraints + + - **The Owner's role is immutable via the API.** Any attempt to PUT the Owner's membership returns `403 Forbidden` with `{"error": "The Owner's role cannot be changed via the API. Use the ownership transfer flow."}`. Ownership transfer is a console-only flow. + - Users cannot update their own role (`403`). + - User callers cannot update a member who holds a global-access role (e.g. Admin) unless they themselves hold a global-access role (`403`). + - Service Account callers cannot update any member who holds a global-access role (`403`), nor can they assign a global-access role to any member (`403`). + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + ### JSON Body + + #### Required fields + + + + The ID of the new role to assign. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "role_id": "d3a2124c-9770-42d5-abf8-599b4a372e9d" + }' + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'role_id': 'd3a2124c-9770-42d5-abf8-599b4a372e9d' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Manager" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T09:00:00Z" + } + ``` + + + + +--- + +## Remove Member {{ tag: 'DELETE', label: '/v1/members/:id' }} + + + + + Remove a member from the organisation. The user retains their account and can be re-invited later. + + ### Constraints + + - **The Owner cannot be removed via the API** (`403`). Ownership must be transferred first via the console. + - Users cannot remove themselves from the organisation (`403`). + - Service Account callers cannot remove a member who holds a global-access role (`403`). + + + Members provisioned via SCIM are routed through the SCIM deactivation flow on removal. This revokes all of the member's team-granted environment keys, wipes their wrapped keyring, and detaches their SCIM identity so the next provider sync can cleanly re-adopt them. Org owners cannot be deactivated this way (`403`); transfer ownership first. + + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/members/:id/access' }} + + + + + Set the app and environment access for a member. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list will have their access revoked. + - Each app entry must include at least one environment. + - To revoke all access for a member, send an empty `apps` array. + + The server handles cryptographic key wrapping for each environment — it re-encrypts environment keys for the member's identity key using server-side encryption. + + + This endpoint only works for apps with **Server-side Encryption (SSE) enabled**. SSE can be enabled from the App settings page. Non-SSE apps return `400 Bad Request`. + + The target member must also have logged in to the Phase console at least once so their identity key is registered. If the member's identity key is missing or blank, the endpoint returns `400 Bad Request` with `{"error": "Member has not set up their identity key yet. They must log in to the console first."}`. + + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + To revoke all access, pass an empty array. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "c23d4e5f-6789-01bc-def2-3456789012cd" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all access)' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [] + }' + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'c23d4e5f-6789-01bc-def2-3456789012cd' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T10:00:00Z", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Get Access {{ tag: 'GET', label: '/v1/members/:id/access' }} + + + + + Read the current app and environment access for a member. + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/access/' + headers = {'Authorization': f'Bearer {token}'} + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T10:00:00Z", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + } + ] + } + ] + } + ``` + + + diff --git a/public/public-api/roles.md b/public/public-api/roles.md new file mode 100644 index 00000000..1fcf5f42 --- /dev/null +++ b/public/public-api/roles.md @@ -0,0 +1,491 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Roles API', + description: + 'Explore the Phase Roles API for managing roles and permissions programmatically.', +} + +API + +# Roles + +Roles define the set of permissions granted to users and service accounts within your organisation. Phase includes five default roles (Owner, Admin, Manager, Developer, Service) and supports creating custom roles on paid plans. On this page, we'll look at the Roles API endpoints for listing, creating, updating, and deleting roles. {{ className: 'lead' }} + + + +## The Role model + +### Properties + + + + Unique identifier for the role. + + + The name of the role. + + + An optional description for the role. + + + A hex color code for the role (e.g. `#FF0000`). + + + Whether this is a built-in default role. Default roles cannot be modified or deleted. + + + Timestamp of when the role was created. + + + +### Permissions object + +When fetching a single role, the full permissions object is included. The permissions structure contains: + + + + Organisation-level permissions. Keys are resource class names, values are arrays of allowed actions (`create`, `read`, `update`, `delete`). See [Roles & Permissions](/access-control/roles) for the full list of valid resource classes. + + + App-level permissions. Keys are resource class names, values are arrays of allowed actions. See [Roles & Permissions](/access-control/roles) for the full list. + + + Read-only. Returned as `true` for the built-in `Owner` and `Admin` roles and `false` otherwise. Cannot be set on custom roles — POST and PUT reject requests that include `global_access` (or `globalAccess`) under `permissions`. + + + + +Responses use camelCase keys (`appPermissions`, `globalAccess`). On POST and PUT, `app_permissions` snake_case is also accepted; the `permissions` payload must contain only `permissions` and `app_permissions` keys. + + +--- + +## List Roles {{ tag: 'GET', label: '/v1/roles' }} + + + + + Retrieve all roles in the organisation, including both default and custom roles. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/roles/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/roles/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "226bf078-74d5-406e-ba5b-dd68bf23326c", + "name": "Owner", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + }, + { + "id": "151e4e7b-6e39-4ede-a064-1f7d228723c5", + "name": "Admin", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + }, + { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + } + ] + } + ``` + + + + +--- + +## Get Role {{ tag: 'GET', label: '/v1/roles/:id' }} + + + + + Retrieve a single role with its full permissions object. For default roles, the permissions are resolved from the built-in role definitions. For custom roles, the stored permissions are returned directly. + + ### URL parameters + + + + The unique identifier of the role. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/roles/6aec9df5-cd75-4645-a9d0-8b6f6aff78d6/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + role_id = '6aec9df5-cd75-4645-a9d0-8b6f6aff78d6' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z", + "permissions": { + "permissions": { + "Organisation": [], + "Billing": [], + "Apps": ["read"], + "Members": ["read"], + "ServiceAccounts": [], + "Roles": ["read"] + }, + "appPermissions": { + "Environments": ["read", "create", "update"], + "Secrets": ["create", "read", "update", "delete"], + "Tokens": ["read", "create"], + "Members": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Create Role {{ tag: 'POST', label: '/v1/roles' }} + + + + + Create a custom role with a specified set of permissions. + + + Custom roles are not available on the Free plan. You must be on a Pro or Enterprise plan to create custom roles. + + + ### JSON Body + + #### Required fields + + + + The role name. Maximum 64 characters. Must be unique within the organisation (case-insensitive). + + + The permissions object. Must contain exactly two keys: `permissions` (org-level) and `app_permissions` (app-level). The `global_access` flag cannot be set on custom roles — POST and PUT reject requests that include `global_access` (or `globalAccess`) under `permissions` with `400 Bad Request`. + + + + #### Optional fields + + + + A description for the role. Maximum 500 characters. + + + A hex color code. Maximum 7 characters (e.g. `#FF0000`). + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/roles/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Read Only", + "description": "Read-only access to all resources", + "color": "#64748b", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"] + }, + "app_permissions": { + "Secrets": ["read"], + "Environments": ["read"] + } + } + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/roles/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Read Only', + 'description': 'Read-only access to all resources', + 'color': '#64748b', + 'permissions': { + 'permissions': { + 'Organisation': ['read'], + 'Apps': ['read'], + 'Members': ['read'], + 'Roles': ['read'], + }, + 'app_permissions': { + 'Secrets': ['read'], + 'Environments': ['read'], + }, + } + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Read Only", + "description": "Read-only access to all resources", + "color": "#64748b", + "isDefault": false, + "createdAt": "2024-06-02T10:00:00Z", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"] + }, + "appPermissions": { + "Secrets": ["read"], + "Environments": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Update Role {{ tag: 'PUT', label: '/v1/roles/:id' }} + + + + + Update a custom role's name, description, color, and/or permissions. At least one field must be provided. Default roles cannot be modified (`403 Forbidden`). + + ### URL parameters + + + + The unique identifier of the role. + + + + ### JSON Body + + When `permissions` is provided, the full object replaces the stored permissions and must contain exactly two keys: `permissions` and `app_permissions`. The camelCase variant `appPermissions` is also accepted on input. Sending `global_access` (or `globalAccess`) under `permissions` returns `400 Bad Request`. + + + + The new role name. Maximum 64 characters. Must be unique within the organisation (case-insensitive). + + + The new description. Maximum 500 characters. + + + The new hex color code. Maximum 7 characters. + + + The updated permissions object. Replaces the stored permissions in full. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/roles/f47ac10b-58cc-4372-a567-0e02b2c3d479/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Auditor", + "description": "Read-only auditor role", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"], + "ServiceAccounts": ["read"] + }, + "app_permissions": { + "Secrets": ["read"], + "Environments": ["read"], + "Logs": ["read"] + } + } + }' + ``` + + ```python + import requests + + role_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Auditor', + 'description': 'Read-only auditor role' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Auditor", + "description": "Read-only auditor role", + "color": "#64748b", + "isDefault": false, + "createdAt": "2024-06-02T10:00:00Z", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"], + "ServiceAccounts": ["read"] + }, + "appPermissions": { + "Secrets": ["read"], + "Environments": ["read"], + "Logs": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Delete Role {{ tag: 'DELETE', label: '/v1/roles/:id' }} + + + + + Delete a custom role. + + - Default roles (Owner, Admin, Manager, Developer, Service) cannot be deleted — returns `403 Forbidden` with `{"error": "Default roles cannot be deleted."}`. + - A role with members or service accounts currently assigned to it cannot be deleted — returns `409 Conflict`. Reassign the affected accounts to a different role first. + + ### URL parameters + + + + The unique identifier of the role. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/roles/f47ac10b-58cc-4372-a567-0e02b2c3d479/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + role_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/public/public-api/secrets.md b/public/public-api/secrets.md index 2c22d948..9510a8c9 100644 --- a/public/public-api/secrets.md +++ b/public/public-api/secrets.md @@ -54,6 +54,9 @@ The secret model contains the basic key / value pairs that define your environme The absolute path for the secret. + + Unique identifier of the folder that holds this secret, or `null` if the secret lives at the root path (`/`). Derived from `path` — you only set `path` on writes; `folder` is read-only in responses. + The secret version. @@ -139,7 +142,7 @@ The secret model contains the basic key / value pairs that define your environme tags: 'aws,postgres' }; - fetch(url + new URLSearchParams(params), { + fetch(`${url}?${new URLSearchParams(params)}`, { method: 'GET', headers: headers }) @@ -183,7 +186,7 @@ The secret model contains the basic key / value pairs that define your environme import ( "fmt" "net/http" - "io/ioutil" + "io" ) func main() { @@ -208,7 +211,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -412,7 +415,7 @@ The secret model contains the basic key / value pairs that define your environme ] }) headers = { - 'Authorization': f"Bearer {token} ", + 'Authorization': f"Bearer {token}", 'Content-Type': 'application/json' } @@ -426,7 +429,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -470,7 +473,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -692,7 +695,7 @@ The secret model contains the basic key / value pairs that define your environme ] }) headers = { - 'Authorization': f"Bearer {token} ", + 'Authorization': f"Bearer {token}", 'Content-Type': 'application/json' } @@ -706,7 +709,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -748,7 +751,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -919,7 +922,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -949,7 +952,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return diff --git a/public/public-api/service-accounts.md b/public/public-api/service-accounts.md new file mode 100644 index 00000000..1a6b84db --- /dev/null +++ b/public/public-api/service-accounts.md @@ -0,0 +1,701 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Service Accounts API', + description: + 'Explore the Phase Service Accounts API for managing service accounts programmatically.', +} + +API + +# Service Accounts + +Service Accounts provide programmatic, non-human access to your Phase organisation. Each Service Account has its own Role, authentication tokens, and can be granted access to specific Apps and Environments. On this page, we'll look at the API endpoints for managing Service Accounts, their access, and their lifecycle. {{ className: 'lead' }} + + + +## The Service Account model + +### Properties + + + + Unique identifier for the service account. + + + The name of the service account. + + + The assigned role, with `id` and `name`. + + + Timestamp of when the service account was created. + + + Timestamp of when the service account was last updated. + + + +### Detail Properties + +When fetching a single service account, additional detail fields are included: + + + + Array of active tokens, each with `id`, `name`, `createdAt`, and `expiresAt`. + + + Array of accessible apps, each with `id`, `name`, and an `environments` array containing the environments the service account can access within that app. + + + +--- + +## List Service Accounts {{ tag: 'GET', label: '/v1/service-accounts' }} + + + + + Retrieve all active service accounts in the organisation. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/service-accounts/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/service-accounts/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Service Account {{ tag: 'POST', label: '/v1/service-accounts' }} + + + + + Create a new service account. The server generates all cryptographic keys and mints an initial authentication token, which is returned in the response. + + + The `initialToken.token` and `initialToken.bearerToken` strings are only returned once at creation time. Store them securely — they cannot be retrieved again. The token's `id` is returned alongside them so it can be referenced by the [Delete Token](#delete-token) endpoint later. + + + ### JSON Body + + #### Required fields + + + + The service account name. Maximum 64 characters. + + + The ID of the role to assign. Must not be a role with global access (e.g. Owner or Admin). + + + + #### Optional fields + + + + A name for the initial token. Defaults to `"Default"`. + + + Bind the service account to a [Team](/public-api/teams). Team-owned service accounts are visible only to team members (plus Owner / Admin), are auto-added as members of the team, are provisioned `EnvironmentKey` records for every SSE-enabled app the team has access to, and cannot later be transferred to a different team or be removed from the owning team's membership. Requires a Pro or Enterprise plan; the caller must be a member of the team (or hold global access). + + + + + Service accounts are visible to all org members with the `ServiceAccounts.read` permission **except** team-owned ones — those are only visible to members of the owning team and to Owner / Admin. The same scoping applies to `GET /v1/service-accounts/` and `GET /v1/service-accounts/:id/`. + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/service-accounts/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "deploy-bot", + "role_id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "token_name": "CI Token" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/service-accounts/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'deploy-bot', + 'role_id': 'd3a2124c-9770-42d5-abf8-599b4a372e9d', + 'token_name': 'CI Token' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "initialToken": { + "id": "f8621d1a-6903-4b60-8e8d-2085a2475871", + "name": "Default", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null, + "token": "pss_service:v2::::", + "bearerToken": "ServiceAccount " + } + } + ``` + + The `initialToken.token` and `initialToken.bearerToken` strings are only returned in this response — there's no way to recover them later. The `initialToken.id` is the same identifier used by the [Delete Token](#delete-token) endpoint to revoke this specific token. + + + + +--- + +## Get Service Account {{ tag: 'GET', label: '/v1/service-accounts/:id' }} + + + + + Retrieve a single service account with full detail, including tokens and app/environment access. + + ### URL parameters + + + + The unique identifier of the service account. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "tokens": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Update Service Account {{ tag: 'PUT', label: '/v1/service-accounts/:id' }} + + + + + Update a service account's name and/or role. At least one field must be provided. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + + + The new name. Maximum 64 characters. HTML tags and ASCII control characters are stripped; whitespace is trimmed. + + + The ID of the new role. Must not be a global-access role — service accounts cannot hold roles with `global_access: true`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "deploy-bot-v2", + "role_id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6" + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'deploy-bot-v2' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot-v2", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T14:00:00Z", + "tokens": [], + "apps": [] + } + ``` + + + + +--- + +## Delete Service Account {{ tag: 'DELETE', label: '/v1/service-accounts/:id' }} + + + + + Delete a service account. All associated tokens are immediately invalidated (subsequent requests with those tokens return `401 Unauthorized` with `{"error": "Token expired or deleted"}`), and all app/environment access grants are removed. + + ### URL parameters + + + + The unique identifier of the service account. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/service-accounts/:id/access' }} + + + + + Set the app and environment access for a service account. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list will have their access revoked. + - Each app entry must include at least one environment. + - To revoke all access for a service account, send an empty `apps` array. + - Only apps with **Server-side Encryption (SSE)** enabled are supported; the endpoint returns `400 Bad Request` for non-SSE apps. + - The service account's `identity_key` must be set (server-generated at creation). The endpoint returns `400 Bad Request` if it is missing or blank. + + The server automatically handles cryptographic key wrapping for each environment — decrypting environment keys with the server key and re-encrypting them for the service account's identity key. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "c23d4e5f-6789-01bc-def2-3456789012cd" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all access)' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [] + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'c23d4e5f-6789-01bc-def2-3456789012cd' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T15:00:00Z", + "tokens": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Create Token {{ tag: 'POST', label: '/v1/service-accounts/:id/tokens' }} + + + + + Mint an additional bearer token for an existing service account. The server uses its keyring to generate the token end-to-end, so the caller only needs to supply a name and an optional expiry. + + - Requires the service account to have **server-side key management (SSK)** enabled. SAs created via this API always do; client-side-only SAs return `400 Bad Request`. + - The `token` and `bearerToken` values in the response are only ever returned at creation time — store them securely. + - Expiry can be set as either an absolute timestamp (`expires_at`) or a relative TTL (`expires_in`). If both are supplied, `expires_at` takes priority. If neither is supplied, the token does not expire. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + #### Required fields + + + + A human-readable name for the token. Maximum 64 characters. + + + + #### Optional fields + + + + Absolute expiry as an ISO-8601 datetime **with a timezone offset** (e.g. `2026-12-31T23:59:59Z` or `2026-12-31T23:59:59+00:00`). Must be in the future. Naive datetimes (no offset) are rejected. + + + Token lifetime in seconds (positive integer). The server converts this to an absolute expiry at request time as `now + expires_in`. Ignored if `expires_at` is also supplied. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/tokens/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "CI Token", + "expires_at": "2026-12-31T23:59:59Z" + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/tokens/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + } + payload = { + 'name': 'CI Token', + 'expires_at': '2026-12-31T23:59:59Z', + # Or use a relative TTL instead: + # 'expires_in': 2592000, # 30 days + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "f8621d1a-6903-4b60-8e8d-2085a2475871", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": "2025-12-31T00:00:00Z", + "token": "pss_service:v2::::", + "bearerToken": "ServiceAccount " + } + ``` + + + + +--- + +## Delete Token {{ tag: 'DELETE', label: '/v1/service-accounts/:id/tokens/:token_id' }} + + + + + Revoke a service account token. Any subsequent requests using the token return `401 Unauthorized`. + + Returns `404 Not Found` if the token belongs to a different service account than the `:id` in the path — the API does not reveal token existence across service accounts. + + ### URL parameters + + + + The unique identifier of the service account. + + + The unique identifier of the token to revoke. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/tokens/f8621d1a-6903-4b60-8e8d-2085a2475871/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + token_id = 'f8621d1a-6903-4b60-8e8d-2085a2475871' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/tokens/{token_id}/' + headers = {'Authorization': f'Bearer {token}'} + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/public/public-api/teams.md b/public/public-api/teams.md new file mode 100644 index 00000000..2e95d46f --- /dev/null +++ b/public/public-api/teams.md @@ -0,0 +1,728 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Teams API', + description: + 'Explore the Phase Teams API for managing teams, team membership, and team-scoped app access programmatically.', +} + +API + +# Teams + +Teams group users and service accounts together and grant shared, scoped access to specific app environments. Each team can carry optional role overrides that apply only to apps accessed via that team's grants, so the same member can hold different effective permissions in different teams. On this page, we'll look at the Teams API endpoints for managing teams, their membership, and their app-environment scope. {{ className: 'lead' }} + + +Teams require a Pro or Enterprise plan. `POST /v1/teams/` returns `403` on the Free plan. + + + + +## The Team model + +### Properties + + + + Unique identifier for the team. + + + The team name. Maximum 64 characters. + + + An optional team description. + + + Whether this team is provisioned and synced by your SCIM identity provider. SCIM-managed teams cannot be renamed, deleted, or have user members added or removed via this API — those operations must be performed through the SCIM provider. Service account membership is unaffected and remains manageable via the API. + + + The optional role override for human members of the team. When set, the role unions with each member's organisation role for app-level permissions on apps the team has access to. `null` means no override — members keep their organisation role on team-accessed apps. + + + The optional role override for service-account members of the team. Same semantics as `memberRole`, applied to service accounts. + + + The OrganisationMember that owns the team (the team creator by default). Team owners can transfer access scope and add other members. Team owners retain their organisation role on team-accessed apps regardless of `memberRole`. + + + Timestamp of when the team was created. + + + Timestamp of when the team was last updated. + + + +### Permission model + +Team access is **additive**: granting team membership never reduces a member's effective permissions. For each app the team has access to, the request is permitted if **either** the member's individual organisation role grants the action **or** the team's effective role (override → org role) grants it. The same member can be in multiple teams; permissions union across all of them. + +Server-side Encryption (SSE) is required for an app to be granted to a team — the server provisions per-member `EnvironmentKey` records when teams are attached to apps or members are added to teams. Apps without SSE return `400 Bad Request` when added to a team's scope. + +--- + +## List Teams {{ tag: 'GET', label: '/v1/teams' }} + + + + + Retrieve all teams in the organisation. Requires the `Teams.read` org permission. Teams are returned without `members` or `apps` detail — fetch the [Team Detail](#get-team) endpoint for those. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/teams/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/teams/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": null, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Team {{ tag: 'POST', label: '/v1/teams' }} + + + + + Create a new team. The calling user becomes the team's owner and is automatically added as a member. Service-account callers create the team without an owner and no auto-membership. + + Requires the `Teams.create` org permission and a Pro or Enterprise plan. + + ### JSON Body + + #### Required fields + + + + The team name. Maximum 64 characters. HTML tags and ASCII control characters are stripped; whitespace is trimmed. + + + + #### Optional fields + + + + A description for the team. Maximum 10,000 characters. + + + Role ID to apply as the team's `memberRole` override. Must reference a role in the same organisation. + + + Role ID to apply as the team's `serviceAccountRole` override. Must reference a role in the same organisation. + + + + + The `is_scim_managed` flag is set automatically by the SCIM provisioning flow. Passing it in the request body returns `400 Bad Request`. + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/teams/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "backend-eng", + "description": "Backend engineering team" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/teams/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'backend-eng', + 'description': 'Backend engineering team' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": null, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + } + ], + "apps": [] + } + ``` + + + + +--- + +## Get Team {{ tag: 'GET', label: '/v1/teams/:id' }} + + + + + Retrieve a single team with its members and app-environment scope. Non-team-members can list teams via [List Teams](#list-teams) but cannot fetch detail unless they hold a global-access role (Owner or Admin). + + ### URL parameters + + + + The unique identifier of the team. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": { + "id": "5d880011-2fc9-4e78-9be2-80f4183c0eea", + "name": "Developer" + }, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T09:00:00Z", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + }, + { + "type": "service_account", + "id": "0df24d46-e057-4695-a519-eee3d34e291c", + "name": "deploy-bot" + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "web-frontend", + "environments": [ + { "id": "af6b7a8e-c268-48c2-967c-032e86e26110", "name": "Development" }, + { "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", "name": "Staging" } + ] + } + ] + } + ``` + + + + +--- + +## Update Team {{ tag: 'PUT', label: '/v1/teams/:id' }} + + + + + Update a team's name, description, or role overrides. At least one field must be provided. + + - SCIM-managed teams reject all field updates — name and description are synced from the SCIM provider, and role overrides are managed via the console. + - Pass an empty string (`""`) for `member_role_id` or `service_account_role_id` to clear an existing override and fall back to the org role on team-accessed apps. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + The new team name. Maximum 64 characters. + + + The new description. Maximum 10,000 characters. + + + New role override for human members. Pass `""` to clear the existing override. + + + New role override for service-account members. Pass `""` to clear the existing override. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "backend-engineering", + "member_role_id": "5d880011-2fc9-4e78-9be2-80f4183c0eea" + }' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'backend-engineering', + 'member_role_id': '5d880011-2fc9-4e78-9be2-80f4183c0eea' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": { + "id": "5d880011-2fc9-4e78-9be2-80f4183c0eea", + "name": "Developer" + }, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T14:00:00Z", + "members": [], + "apps": [] + } + ``` + + + + +--- + +## Delete Team {{ tag: 'DELETE', label: '/v1/teams/:id' }} + + + + + Delete a team. This soft-deletes the team and cascades: + + - All team-granted `EnvironmentKey` rows are revoked. Keys carrying an additional individual grant survive (their team grant is removed but the key itself is preserved). + - All team-owned service accounts are soft-deleted, including their tokens. + + SCIM-managed teams cannot be deleted via this endpoint — delete the corresponding group in your SCIM provider instead. + + ### URL parameters + + + + The unique identifier of the team. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Add Team Members {{ tag: 'POST', label: '/v1/teams/:id/members' }} + + + + + Add one or more members (users or service accounts) to a team. The server provisions per-member `EnvironmentKey` records for every SSE-enabled app the team already has access to. + + - **SCIM-managed teams** reject `user` additions — user membership is controlled by the SCIM provider. Service-account additions are permitted on SCIM-managed teams since service accounts are outside the SCIM scope. + - **Team-owned service accounts** (SAs created with a `team_id`) cannot be added to a different team — returns `409 Conflict`. The SA must first be transferred to org-level ownership. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + Either `user` or `service_account`. Defaults to `user`. + + + Array of OrganisationMember IDs (for `user`) or ServiceAccount IDs (for `service_account`). Must be a non-empty list. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/members/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "member_type": "user", + "member_ids": [ + "3f2e1d0c-9b8a-7654-3210-fedcba987654" + ] + }' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/members/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'member_type': 'user', + 'member_ids': ['3f2e1d0c-9b8a-7654-3210-fedcba987654'] + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + }, + { + "type": "user", + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "email": "bob@example.com", + "fullName": "Bob Jones" + } + ] + } + ``` + + + + +--- + +## Remove Team Member {{ tag: 'DELETE', label: '/v1/teams/:id/members/:member_id' }} + + + + + Remove a single member from a team. The server revokes the member's team-granted `EnvironmentKey` records; keys carrying an additional individual grant are preserved. + + - **SCIM-managed teams** reject `user` removals — user membership is controlled by the SCIM provider. + - **Team-owned service accounts** cannot be removed from their owning team — returns `409 Conflict`. Delete the service account or transfer ownership to org-level first. + + ### URL parameters + + + + The unique identifier of the team. + + + The OrganisationMember ID or ServiceAccount ID to remove. + + + + ### Query parameters + + + + Either `user` or `service_account`. Defaults to `user`. Required to disambiguate when the same ID exists in both tables. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE "https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/?member_type=user" \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/teams/{team_id}/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + params = {'member_type': 'user'} + + response = requests.delete(url, params=params, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/teams/:id/access' }} + + + + + Set the apps and environments the team has access to. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list have their team grants revoked; orphan `EnvironmentKey` records (no remaining grants) are soft-deleted. + - Each app entry must include at least one environment. To revoke a team's access to an app entirely, omit it from the body. + - To revoke all team access, send an empty `apps` array. + - Only apps with **Server-side Encryption (SSE)** enabled can be granted to a team. Non-SSE apps return `400 Bad Request`. + - The caller must individually have access to each app being granted to the team — returns `403 Forbidden` otherwise. + + For new (app, environment) pairs added by this call, the server provisions per-member `EnvironmentKey` records for every existing team member. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + To revoke all team access, pass an empty array. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "b12c3d4e-5678-90ab-cdef-1234567890ab" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all)' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{"apps": []}' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'b12c3d4e-5678-90ab-cdef-1234567890ab' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "web-frontend", + "environments": [ + { "id": "af6b7a8e-c268-48c2-967c-032e86e26110", "name": "Development" }, + { "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", "name": "Staging" } + ] + } + ] + } + ``` + + + diff --git a/public/sitemap.xml b/public/sitemap.xml index 46c06d83..de3f8000 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,88 +1,95 @@ -https://docs.phase.dev2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication/oauth-sso2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication/oidc-sso2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication/password2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication/sso2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/authentication/tokens2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/external-identities2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/network2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/provisioning/scim2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/roles2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/service-accounts2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/access-control/teams2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/cli2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/cli/commands2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/cli/install2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/cli/usage2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/apps2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/dynamic-secrets2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/environments2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/organisation2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/secrets2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/console/users2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/agents/claude-code2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/agents/codex2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/agents/cursor2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/agents/opencode2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/agents/vscode-copilot2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/frameworks2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/aws-codebuild2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/aws-elastic-container-service2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/aws-iam2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/aws-secrets-manager2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/azure-key-vault2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/azure-pipelines2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/bitbucket-pipelines2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/buildkite2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/circleci2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/cloudflare-pages2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/cloudflare-workers2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/docker2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/docker-compose2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/drone-ci2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/github-actions2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/github-dependabot2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/gitlab-ci2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/hashicorp-nomad2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/hashicorp-terraform2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/hashicorp-vault2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/jenkins2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/kubernetes2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/railway2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/render2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/teamcity2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/travis-ci2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/integrations/platforms/vercel2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/public-api2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/public-api/dynamic-secrets2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/public-api/errors2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/public-api/external-identities2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/public-api/secrets2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/quickstart2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/sdks2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/sdks/go2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/sdks/js2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/sdks/node2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/sdks/python2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/security2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/security/architecture2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/security/cryptography2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/aws2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/aws-eks2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/azure2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/configuration/envars2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/digitalocean2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/docker-compose2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/gcp2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/kubernetes2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/maintenance2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/railway2026-05-01T11:33:56.846Zdaily0.7 -https://docs.phase.dev/self-hosting/raspberrypi2026-05-01T11:33:56.846Zdaily0.7 +https://docs.phase.dev2026-05-28T08:30:16.702Zdaily0.7 +https://docs.phase.dev/access-control2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication/oauth-sso2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication/oidc-sso2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication/password2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication/sso2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/authentication/tokens2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/external-identities2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/network2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/provisioning/scim2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/roles2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/service-accounts2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/access-control/teams2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/cli2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/cli/commands2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/cli/install2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/cli/usage2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/apps2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/dynamic-secrets2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/environments2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/organisation2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/secrets2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/console/users2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/agents/claude-code2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/agents/codex2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/agents/cursor2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/agents/opencode2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/agents/vscode-copilot2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/frameworks2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/aws-codebuild2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/aws-elastic-container-service2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/aws-iam2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/aws-secrets-manager2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/azure-key-vault2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/azure-pipelines2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/bitbucket-pipelines2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/buildkite2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/circleci2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/cloudflare-pages2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/cloudflare-workers2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/docker2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/docker-compose2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/drone-ci2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/github-actions2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/github-dependabot2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/gitlab-ci2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/hashicorp-nomad2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/hashicorp-terraform2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/hashicorp-vault2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/jenkins2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/kubernetes2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/railway2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/render2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/teamcity2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/travis-ci2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/integrations/platforms/vercel2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/apps2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/dynamic-secrets2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/environments2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/errors2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/external-identities2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/invites2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/members2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/roles2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/secrets2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/service-accounts2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/public-api/teams2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/quickstart2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/sdks2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/sdks/go2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/sdks/js2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/sdks/node2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/sdks/python2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/security2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/security/architecture2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/security/cryptography2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/aws2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/aws-eks2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/azure2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/configuration/envars2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/digitalocean2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/docker-compose2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/gcp2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/kubernetes2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/maintenance2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/railway2026-05-28T08:30:16.703Zdaily0.7 +https://docs.phase.dev/self-hosting/raspberrypi2026-05-28T08:30:16.703Zdaily0.7 \ No newline at end of file diff --git a/src/components/Navigation.jsx b/src/components/Navigation.jsx index 9153584b..89f22c0d 100644 --- a/src/components/Navigation.jsx +++ b/src/components/Navigation.jsx @@ -243,8 +243,15 @@ export const navigation = [ title: 'API', links: [ { title: 'Overview', href: '/public-api' }, + { title: 'Apps', href: '/public-api/apps' }, + { title: 'Environments', href: '/public-api/environments' }, { title: 'Secrets', href: '/public-api/secrets' }, { title: 'Dynamic Secrets', href: '/public-api/dynamic-secrets' }, + { title: 'Members', href: '/public-api/members' }, + { title: 'Invites', href: '/public-api/invites' }, + { title: 'Service Accounts', href: '/public-api/service-accounts' }, + { title: 'Teams', href: '/public-api/teams' }, + { title: 'Roles', href: '/public-api/roles' }, { title: 'External Identities', href: '/public-api/external-identities' }, { title: 'Errors', href: '/public-api/errors' }, ], diff --git a/src/pages/public-api/apps.mdx b/src/pages/public-api/apps.mdx new file mode 100644 index 00000000..6a9fd843 --- /dev/null +++ b/src/pages/public-api/apps.mdx @@ -0,0 +1,397 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Apps API', + description: + 'Explore the Phase Apps API for managing applications programmatically.', +} + +API + +# Apps + +Apps are the top-level organizational unit in Phase. Each App contains Environments, which in turn hold Secrets. On this page, we'll look at the Apps API endpoints for listing, creating, updating, and deleting Apps. {{ className: 'lead' }} + + +Apps created via the API are SSE-enabled by default. The list endpoint returns metadata for all apps you have access to (SSE and E2EE) — check the `sseEnabled` field. Write operations against secrets and environments via the REST API require SSE; E2EE apps return `400 Bad Request`. + + + + +## The App model + +### Properties + + + + Unique identifier for the app. + + + The name of the app. + + + An optional description for the app. + + + Whether server-side encryption is enabled for this app. + + + Timestamp of when the app was created. + + + Timestamp of when the app was last updated. + + + +--- + +## List Apps {{ tag: 'GET', label: '/v1/apps' }} + + + + + Retrieve metadata for all apps (SSE and E2EE) that the authenticated account has access to. Use the `sseEnabled` field to distinguish; only SSE apps support secrets and environment writes via the REST API. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/apps/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "58006442-007b-4625-b8e2-80f7606484a0", + "name": "My App", + "description": "Production application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create App {{ tag: 'POST', label: '/v1/apps' }} + + + + + Create a new SSE-enabled App. By default, three environments are created automatically: Development, Staging, and Production. You can optionally supply a custom list of environment names. + + ### JSON Body + + #### Required fields + + + + The app name. Maximum 64 characters. + + + + #### Optional fields + + + + A description for the app. Maximum 10,000 characters. + + + A list of custom environment names to create instead of the defaults. Each name must contain only letters, numbers, hyphens, and underscores. Requires a paid plan. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My New App", + "description": "A new application" + }' + ``` + + ```fish {{ title: 'cURL (custom environments)' }} + curl -X POST https://api.phase.dev/v1/apps/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My New App", + "environments": ["dev", "test", "staging", "prod"] + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/apps/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'My New App', + 'description': 'A new application' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My New App", + "description": "A new application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "environments": [ + { + "id": "e7d9cc21-f83c-441f-8887-8720ceab4c7e", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "c5b998fb-09cf-48ef-808a-46ed08a1f0ab", + "name": "Staging", + "envType": "staging", + "index": 1, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "538ac0e3-236a-48af-962f-69fab6449c2e", + "name": "Production", + "envType": "prod", + "index": 2, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + The `environments` array reflects whatever was created — either the three defaults (Development/Staging/Production) or the custom names from the request body, in request order. Each entry has the same shape as `GET /v1/environments/:id`. + + + + +--- + +## Get App {{ tag: 'GET', label: '/v1/apps/:id' }} + + + + + Retrieve a single app by its ID. + + ### URL parameters + + + + The unique identifier of the app. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "description": "Production application", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update App {{ tag: 'PUT', label: '/v1/apps/:id' }} + + + + + Update an app's name and/or description. At least one field must be provided. + + ### URL parameters + + + + The unique identifier of the app. + + + + ### JSON Body + + + + The new app name. Maximum 64 characters. + + + The new app description. Maximum 10,000 characters. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Updated App Name", + "description": "Updated description" + }' + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Updated App Name' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "Updated App Name", + "description": "Updated description", + "sseEnabled": true, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T10:30:00Z" + } + ``` + + + + +--- + +## Delete App {{ tag: 'DELETE', label: '/v1/apps/:id' }} + + + + + Permanently delete an app and all its associated data. + + + This action is irreversible. All environments, secrets, and access configurations associated with the app will be permanently deleted. + + + ### URL parameters + + + + The unique identifier of the app. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/apps/72b9ddd5-8fce-49ab-89d9-c431d53a9552/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + app_id = '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + url = f'https://api.phase.dev/v1/apps/{app_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/src/pages/public-api/environments.mdx b/src/pages/public-api/environments.mdx new file mode 100644 index 00000000..e7cb7bfa --- /dev/null +++ b/src/pages/public-api/environments.mdx @@ -0,0 +1,380 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Environments API', + description: + 'Explore the Phase Environments API for managing environments programmatically.', +} + +API + +# Environments + +Environments represent deployment stages within an App (e.g. Development, Staging, Production). Each Environment contains its own set of Secrets. On this page, we'll look at the Environments API endpoints for listing, creating, updating, and deleting Environments. {{ className: 'lead' }} + + +The Environments API requires server-side encryption (SSE) to be enabled for the parent App. An `app_id` query parameter is required for list and create operations — omitting it returns `403` as the API cannot resolve the app context. + + + + +## The Environment model + +### Properties + + + + Unique identifier for the environment. + + + The name of the environment. + + + The type of environment: `dev`, `staging`, `prod`, or `custom`. + + + The display order of the environment within its app. + + + Timestamp of when the environment was created. + + + Timestamp of when the environment was last updated. + + + +--- + +## List Environments {{ tag: 'GET', label: '/v1/environments' }} + + + + + Retrieve all environments for a given app that the authenticated account has access to. + + ### Required parameters + + + + Unique identifier for the Phase App. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl "https://api.phase.dev/v1/environments/?app_id=72b9ddd5-8fce-49ab-89d9-c431d53a9552" \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/environments/' + params = { + 'app_id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + } + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, params=params, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", + "name": "Staging", + "envType": "staging", + "index": 1, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod", + "index": 2, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Environment {{ tag: 'POST', label: '/v1/environments' }} + + + + + Create a new environment within an app. The environment name must contain only letters, numbers, hyphens, and underscores (max 64 characters). + + ### Required parameters + + + + Unique identifier for the Phase App. + + + + ### JSON Body + + #### Required fields + + + + The environment name. Must match `[a-zA-Z0-9\-_]{1,64}`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST "https://api.phase.dev/v1/environments/?app_id=72b9ddd5-8fce-49ab-89d9-c431d53a9552" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "canary" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/environments/' + params = { + 'app_id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552' + } + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'canary' + } + + response = requests.post(url, params=params, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "d34e5f6a-7890-12cd-ef34-567890123456", + "name": "canary", + "envType": "custom", + "index": 3, + "createdAt": "2024-06-02T10:00:00Z", + "updatedAt": "2024-06-02T10:00:00Z" + } + ``` + + + + +--- + +## Get Environment {{ tag: 'GET', label: '/v1/environments/:id' }} + + + + + Retrieve a single environment by its ID. + + ### URL parameters + + + + The unique identifier of the environment. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update Environment {{ tag: 'PUT', label: '/v1/environments/:id' }} + + + + + Update an environment's name. + + ### URL parameters + + + + The unique identifier of the environment. + + + + ### JSON Body + + + + The new environment name. Must match `[a-zA-Z0-9\-_]{1,64}`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "dev-v2" + }' + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'dev-v2' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "dev-v2", + "envType": "dev", + "index": 0, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T11:00:00Z" + } + ``` + + + + +--- + +## Delete Environment {{ tag: 'DELETE', label: '/v1/environments/:id' }} + + + + + Permanently delete an environment and all its secrets. + + + This action is irreversible. All secrets and access keys in the environment will be permanently deleted. + + + ### URL parameters + + + + The unique identifier of the environment. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/environments/af6b7a8e-c268-48c2-967c-032e86e26110/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + env_id = 'af6b7a8e-c268-48c2-967c-032e86e26110' + url = f'https://api.phase.dev/v1/environments/{env_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/src/pages/public-api/errors.mdx b/src/pages/public-api/errors.mdx index e5fba145..cef16707 100644 --- a/src/pages/public-api/errors.mdx +++ b/src/pages/public-api/errors.mdx @@ -27,8 +27,17 @@ Here is a list of the different categories of status codes returned by the Proto A 200 status code indicates a successful response. + + A 201 status code indicates that a new resource was created successfully (returned by POST endpoints). + + + A 204 status code indicates that the request succeeded with no response body. Used for DELETE endpoints. + - A 400 status code indicates a bad request. This is typically due to a request that's missing some data requried to process the request, such as missing `id` fields. + A 400 status code indicates a bad request. This is typically due to missing required fields, invalid input types, values exceeding length limits, or invalid email formats. + + + A 401 status code indicates that no authentication credentials were provided or the token has expired or been deleted. A 403 status code indicates an authentication or access error. Check your [authentication](/public-api#authentication) credentials if you see this error, and make sure the token you're using has the approriate scope for the App and Environment you're trying to access. @@ -36,8 +45,17 @@ Here is a list of the different categories of status codes returned by the Proto This error may also occur due to a [Network Access Policy](/access-control/network#network-access-policies) that restricts access from your IP address. [Read more](https://docs.phase.dev/access-control/network#access-denied-exceptions) about Network Access Policy exceptions. + + A 404 status code indicates that the requested resource does not exist, has been deleted, or belongs to a different organisation. The API does not distinguish between these cases to avoid leaking cross-organisation information. + + + A 405 status code indicates that the HTTP method is not supported by the endpoint. The Phase API supports `GET`, `POST`, `PUT`, and `DELETE` only — `PATCH` is not supported on any endpoint. + - A 409 status code indicates that the requested operation will create a conflict in your secret configuration. This is generally due to attempting to set the `key` of a secret at a given path where this key already exists. + A 409 status code indicates that the requested operation would conflict with the current state of the resource. Examples: attempting to set a secret `key` at a path where it already exists; inviting an email that already has a pending invite; deleting a role that has members or service accounts assigned to it. + + + A 429 status code indicates that you have exceeded the rate limit for your plan. The `retry-after` response header indicates how long to wait before retrying. See [rate limits](/public-api#rate-limits) for per-plan thresholds. A 5xx status code indicates a server error — something went wrong with the Phase API. diff --git a/src/pages/public-api/index.mdx b/src/pages/public-api/index.mdx index 4ee5c4d2..a6b3b8ea 100644 --- a/src/pages/public-api/index.mdx +++ b/src/pages/public-api/index.mdx @@ -19,7 +19,9 @@ You can use the Phase public REST API to access and manage secrets via a simple The Phase API is organized around [REST](https://en.wikipedia.org/wiki/Representational_State_Transfer). The API accepts data in the request body only in JSON-encoded format. It uses standard HTTP methods and response codes. -The API also returns specific error messages when something goes wrong. Check out the API [errors page](/public-api/errors) for more details. +Supported HTTP methods are `GET`, `POST`, `PUT`, and `DELETE`. `PATCH` is not supported on any endpoint and returns `405 Method Not Allowed`. + +Error responses are always JSON of the form `{"error": ""}`. Check out the API [errors page](/public-api/errors) for more details. ## Base URL diff --git a/src/pages/public-api/invites.mdx b/src/pages/public-api/invites.mdx new file mode 100644 index 00000000..a0e41868 --- /dev/null +++ b/src/pages/public-api/invites.mdx @@ -0,0 +1,262 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Invites API', + description: + 'Explore the Phase Invites API for creating, listing, and cancelling pending organisation member invitations.', +} + +API + +# Invites + +Invites are pending membership requests sent to an email address. When an invite is accepted through the Phase console, the recipient becomes an organisation member with the assigned role. On this page, we'll look at the API endpoints for creating, listing, and cancelling pending invites. {{ className: 'lead' }} + + +Invites live under the Members resource — all endpoints are namespaced as `/v1/members/invites/`. The `POST /v1/members/` endpoint is reserved for the future direct-member-creation flow and currently returns `405 Method Not Allowed`. + + + + +## The Invite model + +### Properties + + + + Unique identifier for the invite. + + + The email address the invite was sent to. + + + The role that will be assigned on acceptance, with `id` and `name`. + + + Who sent the invite. Contains `type` (`"member"` or `"service_account"`) and either `email` (for members) or `name` (for service accounts). `null` if the sender has since been removed. + + + Timestamp of when the invite was created. + + + Timestamp of when the invite expires. Invites are valid for 14 days. + + + Whether the invite is still valid (has not been cancelled or accepted). + + + +--- + +## Create Invite {{ tag: 'POST', label: '/v1/members/invites' }} + + + + + Send an invitation to a new member. An invite email is sent to the specified address, and the invite expires after 14 days. + + + This endpoint creates an **invite**, not a direct membership. The invited user must accept the invite via the console to become a member. + + + ### Constraints + + - The role must not have global access (i.e. Owner and Admin roles cannot be invited to). + - The role must not permit creating service account tokens. + - The email is validated against RFC format; whitespace is trimmed and the local + domain parts are lowercased. Invalid emails return `400 Bad Request`. + - The email must not already belong to an active member or a pending invite. Duplicate invites return `409 Conflict` with `{"error": "An active invite already exists for ''."}`. + + + App and environment access cannot be scoped at invite time. Because Phase is end-to-end encrypted, granting access requires the invitee's identity key, which doesn't exist until they accept the invite and complete their key ceremony. Use [Manage Access](/public-api/members#manage-access) after acceptance. Sending an `apps` field returns `400 Bad Request`. + + + ### JSON Body + + #### Required fields + + + + The email address of the person to invite. + + + The ID of the role to assign on acceptance. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/members/invites/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "bob@example.com", + "role_id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/invites/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'email': 'bob@example.com', + 'role_id': '6aec9df5-cd75-4645-a9d0-8b6f6aff78d6' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "inviteeEmail": "bob@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "invitedBy": { + "type": "member", + "email": "alice@example.com" + }, + "createdAt": "2024-06-02T10:00:00Z", + "expiresAt": "2024-06-16T10:00:00Z", + "valid": true + } + ``` + + + + +--- + +## List Invites {{ tag: 'GET', label: '/v1/members/invites' }} + + + + + Retrieve all pending (valid, non-expired) invites for the organisation, ordered by most recent first. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/invites/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/invites/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "inviteeEmail": "bob@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "invitedBy": { + "type": "member", + "email": "alice@example.com" + }, + "createdAt": "2024-06-02T10:00:00Z", + "expiresAt": "2024-06-16T10:00:00Z", + "valid": true + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "inviteeEmail": "carol@example.com", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Manager" + }, + "invitedBy": { + "type": "service_account", + "name": "deploy-bot" + }, + "createdAt": "2024-06-01T08:00:00Z", + "expiresAt": "2024-06-15T08:00:00Z", + "valid": true + } + ] + } + ``` + + + + +--- + +## Cancel Invite {{ tag: 'DELETE', label: '/v1/members/invites/:id' }} + + + + + Cancel a pending invite. The invite is immediately invalidated and the invitee can no longer use the invite link to join the organisation. + + ### URL parameters + + + + The unique identifier of the invite. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/members/invites/a1b2c3d4-e5f6-7890-abcd-ef1234567890/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + invite_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + url = f'https://api.phase.dev/v1/members/invites/{invite_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/src/pages/public-api/members.mdx b/src/pages/public-api/members.mdx new file mode 100644 index 00000000..233b35af --- /dev/null +++ b/src/pages/public-api/members.mdx @@ -0,0 +1,513 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Members API', + description: + 'Explore the Phase Members API for managing organisation members and their access programmatically.', +} + +API + +# Members + +Organisation members are human users who belong to your Phase organisation, each assigned a Role that governs their permissions. On this page, we'll look at the API endpoints for listing members, updating roles, managing app and environment access, and removing members. {{ className: 'lead' }} + + +To add a new member, send an invite via the [Invites API](/public-api/invites) — the invitee accepts the invite and completes a client-side key ceremony before they become an active organisation member. The `POST /v1/members/` endpoint is reserved for the future direct-member-creation flow and currently returns `405 Method Not Allowed`. + + + + +## The Member model + +### Properties + + + + Unique identifier for the organisation membership. + + + The member's username. + + + The member's full name, populated from their OAuth profile if available. + + + The member's email address. + + + The assigned role, with `id` and `name`. + + + Timestamp of when the member joined the organisation. + + + Timestamp of when the membership was last updated. + + + +--- + +## List Members {{ tag: 'GET', label: '/v1/members' }} + + + + + Retrieve all active members of the organisation. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/members/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Get Member {{ tag: 'GET', label: '/v1/members/:id' }} + + + + + Retrieve a single member by their membership ID. + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ``` + + + + +--- + +## Update Member Role {{ tag: 'PUT', label: '/v1/members/:id' }} + + + + + Update a member's assigned role. + + ### Constraints + + - **The Owner's role is immutable via the API.** Any attempt to PUT the Owner's membership returns `403 Forbidden` with `{"error": "The Owner's role cannot be changed via the API. Use the ownership transfer flow."}`. Ownership transfer is a console-only flow. + - Users cannot update their own role (`403`). + - User callers cannot update a member who holds a global-access role (e.g. Admin) unless they themselves hold a global-access role (`403`). + - Service Account callers cannot update any member who holds a global-access role (`403`), nor can they assign a global-access role to any member (`403`). + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + ### JSON Body + + #### Required fields + + + + The ID of the new role to assign. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "role_id": "d3a2124c-9770-42d5-abf8-599b4a372e9d" + }' + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'role_id': 'd3a2124c-9770-42d5-abf8-599b4a372e9d' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Manager" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T09:00:00Z" + } + ``` + + + + +--- + +## Remove Member {{ tag: 'DELETE', label: '/v1/members/:id' }} + + + + + Remove a member from the organisation. The user retains their account and can be re-invited later. + + ### Constraints + + - **The Owner cannot be removed via the API** (`403`). Ownership must be transferred first via the console. + - Users cannot remove themselves from the organisation (`403`). + - Service Account callers cannot remove a member who holds a global-access role (`403`). + + + Members provisioned via SCIM are routed through the SCIM deactivation flow on removal. This revokes all of the member's team-granted environment keys, wipes their wrapped keyring, and detaches their SCIM identity so the next provider sync can cleanly re-adopt them. Org owners cannot be deactivated this way (`403`); transfer ownership first. + + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/members/:id/access' }} + + + + + Set the app and environment access for a member. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list will have their access revoked. + - Each app entry must include at least one environment. + - To revoke all access for a member, send an empty `apps` array. + + The server handles cryptographic key wrapping for each environment — it re-encrypts environment keys for the member's identity key using server-side encryption. + + + This endpoint only works for apps with **Server-side Encryption (SSE) enabled**. SSE can be enabled from the App settings page. Non-SSE apps return `400 Bad Request`. + + The target member must also have logged in to the Phase console at least once so their identity key is registered. If the member's identity key is missing or blank, the endpoint returns `400 Bad Request` with `{"error": "Member has not set up their identity key yet. They must log in to the console first."}`. + + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + To revoke all access, pass an empty array. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "c23d4e5f-6789-01bc-def2-3456789012cd" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all access)' }} + curl -X PUT https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [] + }' + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'c23d4e5f-6789-01bc-def2-3456789012cd' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T10:00:00Z", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Get Access {{ tag: 'GET', label: '/v1/members/:id/access' }} + + + + + Read the current app and environment access for a member. + + ### URL parameters + + + + The unique identifier of the organisation membership. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/access/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/members/{member_id}/access/' + headers = {'Authorization': f'Bearer {token}'} + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "username": "alice", + "fullName": "Alice Smith", + "email": "alice@example.com", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T10:00:00Z", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + } + ] + } + ] + } + ``` + + + diff --git a/src/pages/public-api/roles.mdx b/src/pages/public-api/roles.mdx new file mode 100644 index 00000000..1fcf5f42 --- /dev/null +++ b/src/pages/public-api/roles.mdx @@ -0,0 +1,491 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Roles API', + description: + 'Explore the Phase Roles API for managing roles and permissions programmatically.', +} + +API + +# Roles + +Roles define the set of permissions granted to users and service accounts within your organisation. Phase includes five default roles (Owner, Admin, Manager, Developer, Service) and supports creating custom roles on paid plans. On this page, we'll look at the Roles API endpoints for listing, creating, updating, and deleting roles. {{ className: 'lead' }} + + + +## The Role model + +### Properties + + + + Unique identifier for the role. + + + The name of the role. + + + An optional description for the role. + + + A hex color code for the role (e.g. `#FF0000`). + + + Whether this is a built-in default role. Default roles cannot be modified or deleted. + + + Timestamp of when the role was created. + + + +### Permissions object + +When fetching a single role, the full permissions object is included. The permissions structure contains: + + + + Organisation-level permissions. Keys are resource class names, values are arrays of allowed actions (`create`, `read`, `update`, `delete`). See [Roles & Permissions](/access-control/roles) for the full list of valid resource classes. + + + App-level permissions. Keys are resource class names, values are arrays of allowed actions. See [Roles & Permissions](/access-control/roles) for the full list. + + + Read-only. Returned as `true` for the built-in `Owner` and `Admin` roles and `false` otherwise. Cannot be set on custom roles — POST and PUT reject requests that include `global_access` (or `globalAccess`) under `permissions`. + + + + +Responses use camelCase keys (`appPermissions`, `globalAccess`). On POST and PUT, `app_permissions` snake_case is also accepted; the `permissions` payload must contain only `permissions` and `app_permissions` keys. + + +--- + +## List Roles {{ tag: 'GET', label: '/v1/roles' }} + + + + + Retrieve all roles in the organisation, including both default and custom roles. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/roles/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/roles/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "226bf078-74d5-406e-ba5b-dd68bf23326c", + "name": "Owner", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + }, + { + "id": "151e4e7b-6e39-4ede-a064-1f7d228723c5", + "name": "Admin", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + }, + { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z" + } + ] + } + ``` + + + + +--- + +## Get Role {{ tag: 'GET', label: '/v1/roles/:id' }} + + + + + Retrieve a single role with its full permissions object. For default roles, the permissions are resolved from the built-in role definitions. For custom roles, the stored permissions are returned directly. + + ### URL parameters + + + + The unique identifier of the role. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/roles/6aec9df5-cd75-4645-a9d0-8b6f6aff78d6/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + role_id = '6aec9df5-cd75-4645-a9d0-8b6f6aff78d6' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer", + "description": null, + "color": "", + "isDefault": true, + "createdAt": "2024-01-01T00:00:00Z", + "permissions": { + "permissions": { + "Organisation": [], + "Billing": [], + "Apps": ["read"], + "Members": ["read"], + "ServiceAccounts": [], + "Roles": ["read"] + }, + "appPermissions": { + "Environments": ["read", "create", "update"], + "Secrets": ["create", "read", "update", "delete"], + "Tokens": ["read", "create"], + "Members": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Create Role {{ tag: 'POST', label: '/v1/roles' }} + + + + + Create a custom role with a specified set of permissions. + + + Custom roles are not available on the Free plan. You must be on a Pro or Enterprise plan to create custom roles. + + + ### JSON Body + + #### Required fields + + + + The role name. Maximum 64 characters. Must be unique within the organisation (case-insensitive). + + + The permissions object. Must contain exactly two keys: `permissions` (org-level) and `app_permissions` (app-level). The `global_access` flag cannot be set on custom roles — POST and PUT reject requests that include `global_access` (or `globalAccess`) under `permissions` with `400 Bad Request`. + + + + #### Optional fields + + + + A description for the role. Maximum 500 characters. + + + A hex color code. Maximum 7 characters (e.g. `#FF0000`). + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/roles/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Read Only", + "description": "Read-only access to all resources", + "color": "#64748b", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"] + }, + "app_permissions": { + "Secrets": ["read"], + "Environments": ["read"] + } + } + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/roles/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Read Only', + 'description': 'Read-only access to all resources', + 'color': '#64748b', + 'permissions': { + 'permissions': { + 'Organisation': ['read'], + 'Apps': ['read'], + 'Members': ['read'], + 'Roles': ['read'], + }, + 'app_permissions': { + 'Secrets': ['read'], + 'Environments': ['read'], + }, + } + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Read Only", + "description": "Read-only access to all resources", + "color": "#64748b", + "isDefault": false, + "createdAt": "2024-06-02T10:00:00Z", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"] + }, + "appPermissions": { + "Secrets": ["read"], + "Environments": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Update Role {{ tag: 'PUT', label: '/v1/roles/:id' }} + + + + + Update a custom role's name, description, color, and/or permissions. At least one field must be provided. Default roles cannot be modified (`403 Forbidden`). + + ### URL parameters + + + + The unique identifier of the role. + + + + ### JSON Body + + When `permissions` is provided, the full object replaces the stored permissions and must contain exactly two keys: `permissions` and `app_permissions`. The camelCase variant `appPermissions` is also accepted on input. Sending `global_access` (or `globalAccess`) under `permissions` returns `400 Bad Request`. + + + + The new role name. Maximum 64 characters. Must be unique within the organisation (case-insensitive). + + + The new description. Maximum 500 characters. + + + The new hex color code. Maximum 7 characters. + + + The updated permissions object. Replaces the stored permissions in full. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/roles/f47ac10b-58cc-4372-a567-0e02b2c3d479/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Auditor", + "description": "Read-only auditor role", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"], + "ServiceAccounts": ["read"] + }, + "app_permissions": { + "Secrets": ["read"], + "Environments": ["read"], + "Logs": ["read"] + } + } + }' + ``` + + ```python + import requests + + role_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'Auditor', + 'description': 'Read-only auditor role' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Auditor", + "description": "Read-only auditor role", + "color": "#64748b", + "isDefault": false, + "createdAt": "2024-06-02T10:00:00Z", + "permissions": { + "permissions": { + "Organisation": ["read"], + "Apps": ["read"], + "Members": ["read"], + "Roles": ["read"], + "ServiceAccounts": ["read"] + }, + "appPermissions": { + "Secrets": ["read"], + "Environments": ["read"], + "Logs": ["read"] + }, + "globalAccess": false + } + } + ``` + + + + +--- + +## Delete Role {{ tag: 'DELETE', label: '/v1/roles/:id' }} + + + + + Delete a custom role. + + - Default roles (Owner, Admin, Manager, Developer, Service) cannot be deleted — returns `403 Forbidden` with `{"error": "Default roles cannot be deleted."}`. + - A role with members or service accounts currently assigned to it cannot be deleted — returns `409 Conflict`. Reassign the affected accounts to a different role first. + + ### URL parameters + + + + The unique identifier of the role. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/roles/f47ac10b-58cc-4372-a567-0e02b2c3d479/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + role_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' + url = f'https://api.phase.dev/v1/roles/{role_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/src/pages/public-api/secrets.mdx b/src/pages/public-api/secrets.mdx index 2c22d948..9510a8c9 100644 --- a/src/pages/public-api/secrets.mdx +++ b/src/pages/public-api/secrets.mdx @@ -54,6 +54,9 @@ The secret model contains the basic key / value pairs that define your environme The absolute path for the secret. + + Unique identifier of the folder that holds this secret, or `null` if the secret lives at the root path (`/`). Derived from `path` — you only set `path` on writes; `folder` is read-only in responses. + The secret version. @@ -139,7 +142,7 @@ The secret model contains the basic key / value pairs that define your environme tags: 'aws,postgres' }; - fetch(url + new URLSearchParams(params), { + fetch(`${url}?${new URLSearchParams(params)}`, { method: 'GET', headers: headers }) @@ -183,7 +186,7 @@ The secret model contains the basic key / value pairs that define your environme import ( "fmt" "net/http" - "io/ioutil" + "io" ) func main() { @@ -208,7 +211,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -412,7 +415,7 @@ The secret model contains the basic key / value pairs that define your environme ] }) headers = { - 'Authorization': f"Bearer {token} ", + 'Authorization': f"Bearer {token}", 'Content-Type': 'application/json' } @@ -426,7 +429,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -470,7 +473,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -692,7 +695,7 @@ The secret model contains the basic key / value pairs that define your environme ] }) headers = { - 'Authorization': f"Bearer {token} ", + 'Authorization': f"Bearer {token}", 'Content-Type': 'application/json' } @@ -706,7 +709,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -748,7 +751,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return @@ -919,7 +922,7 @@ The secret model contains the basic key / value pairs that define your environme "fmt" "strings" "net/http" - "io/ioutil" + "io" ) func main() { @@ -949,7 +952,7 @@ The secret model contains the basic key / value pairs that define your environme } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { fmt.Println(err) return diff --git a/src/pages/public-api/service-accounts.mdx b/src/pages/public-api/service-accounts.mdx new file mode 100644 index 00000000..1a6b84db --- /dev/null +++ b/src/pages/public-api/service-accounts.mdx @@ -0,0 +1,701 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Service Accounts API', + description: + 'Explore the Phase Service Accounts API for managing service accounts programmatically.', +} + +API + +# Service Accounts + +Service Accounts provide programmatic, non-human access to your Phase organisation. Each Service Account has its own Role, authentication tokens, and can be granted access to specific Apps and Environments. On this page, we'll look at the API endpoints for managing Service Accounts, their access, and their lifecycle. {{ className: 'lead' }} + + + +## The Service Account model + +### Properties + + + + Unique identifier for the service account. + + + The name of the service account. + + + The assigned role, with `id` and `name`. + + + Timestamp of when the service account was created. + + + Timestamp of when the service account was last updated. + + + +### Detail Properties + +When fetching a single service account, additional detail fields are included: + + + + Array of active tokens, each with `id`, `name`, `createdAt`, and `expiresAt`. + + + Array of accessible apps, each with `id`, `name`, and an `environments` array containing the environments the service account can access within that app. + + + +--- + +## List Service Accounts {{ tag: 'GET', label: '/v1/service-accounts' }} + + + + + Retrieve all active service accounts in the organisation. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/service-accounts/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/service-accounts/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Service Account {{ tag: 'POST', label: '/v1/service-accounts' }} + + + + + Create a new service account. The server generates all cryptographic keys and mints an initial authentication token, which is returned in the response. + + + The `initialToken.token` and `initialToken.bearerToken` strings are only returned once at creation time. Store them securely — they cannot be retrieved again. The token's `id` is returned alongside them so it can be referenced by the [Delete Token](#delete-token) endpoint later. + + + ### JSON Body + + #### Required fields + + + + The service account name. Maximum 64 characters. + + + The ID of the role to assign. Must not be a role with global access (e.g. Owner or Admin). + + + + #### Optional fields + + + + A name for the initial token. Defaults to `"Default"`. + + + Bind the service account to a [Team](/public-api/teams). Team-owned service accounts are visible only to team members (plus Owner / Admin), are auto-added as members of the team, are provisioned `EnvironmentKey` records for every SSE-enabled app the team has access to, and cannot later be transferred to a different team or be removed from the owning team's membership. Requires a Pro or Enterprise plan; the caller must be a member of the team (or hold global access). + + + + + Service accounts are visible to all org members with the `ServiceAccounts.read` permission **except** team-owned ones — those are only visible to members of the owning team and to Owner / Admin. The same scoping applies to `GET /v1/service-accounts/` and `GET /v1/service-accounts/:id/`. + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/service-accounts/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "deploy-bot", + "role_id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "token_name": "CI Token" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/service-accounts/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'deploy-bot', + 'role_id': 'd3a2124c-9770-42d5-abf8-599b4a372e9d', + 'token_name': 'CI Token' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "initialToken": { + "id": "f8621d1a-6903-4b60-8e8d-2085a2475871", + "name": "Default", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null, + "token": "pss_service:v2::::", + "bearerToken": "ServiceAccount " + } + } + ``` + + The `initialToken.token` and `initialToken.bearerToken` strings are only returned in this response — there's no way to recover them later. The `initialToken.id` is the same identifier used by the [Delete Token](#delete-token) endpoint to revoke this specific token. + + + + +--- + +## Get Service Account {{ tag: 'GET', label: '/v1/service-accounts/:id' }} + + + + + Retrieve a single service account with full detail, including tokens and app/environment access. + + ### URL parameters + + + + The unique identifier of the service account. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "tokens": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Update Service Account {{ tag: 'PUT', label: '/v1/service-accounts/:id' }} + + + + + Update a service account's name and/or role. At least one field must be provided. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + + + The new name. Maximum 64 characters. HTML tags and ASCII control characters are stripped; whitespace is trimmed. + + + The ID of the new role. Must not be a global-access role — service accounts cannot hold roles with `global_access: true`. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "deploy-bot-v2", + "role_id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6" + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'deploy-bot-v2' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot-v2", + "role": { + "id": "6aec9df5-cd75-4645-a9d0-8b6f6aff78d6", + "name": "Developer" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T14:00:00Z", + "tokens": [], + "apps": [] + } + ``` + + + + +--- + +## Delete Service Account {{ tag: 'DELETE', label: '/v1/service-accounts/:id' }} + + + + + Delete a service account. All associated tokens are immediately invalidated (subsequent requests with those tokens return `401 Unauthorized` with `{"error": "Token expired or deleted"}`), and all app/environment access grants are removed. + + ### URL parameters + + + + The unique identifier of the service account. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/service-accounts/:id/access' }} + + + + + Set the app and environment access for a service account. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list will have their access revoked. + - Each app entry must include at least one environment. + - To revoke all access for a service account, send an empty `apps` array. + - Only apps with **Server-side Encryption (SSE)** enabled are supported; the endpoint returns `400 Bad Request` for non-SSE apps. + - The service account's `identity_key` must be set (server-generated at creation). The endpoint returns `400 Bad Request` if it is missing or blank. + + The server automatically handles cryptographic key wrapping for each environment — decrypting environment keys with the server key and re-encrypting them for the service account's identity key. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "c23d4e5f-6789-01bc-def2-3456789012cd" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all access)' }} + curl -X PUT https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [] + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'c23d4e5f-6789-01bc-def2-3456789012cd' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "8ab27128-02d8-42c1-b893-12acaffbbd4b", + "name": "deploy-bot", + "role": { + "id": "d3a2124c-9770-42d5-abf8-599b4a372e9d", + "name": "Service" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T15:00:00Z", + "tokens": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": null + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "My App", + "environments": [ + { + "id": "af6b7a8e-c268-48c2-967c-032e86e26110", + "name": "Development", + "envType": "dev" + }, + { + "id": "c23d4e5f-6789-01bc-def2-3456789012cd", + "name": "Production", + "envType": "prod" + } + ] + } + ] + } + ``` + + + + +--- + +## Create Token {{ tag: 'POST', label: '/v1/service-accounts/:id/tokens' }} + + + + + Mint an additional bearer token for an existing service account. The server uses its keyring to generate the token end-to-end, so the caller only needs to supply a name and an optional expiry. + + - Requires the service account to have **server-side key management (SSK)** enabled. SAs created via this API always do; client-side-only SAs return `400 Bad Request`. + - The `token` and `bearerToken` values in the response are only ever returned at creation time — store them securely. + - Expiry can be set as either an absolute timestamp (`expires_at`) or a relative TTL (`expires_in`). If both are supplied, `expires_at` takes priority. If neither is supplied, the token does not expire. + + ### URL parameters + + + + The unique identifier of the service account. + + + + ### JSON Body + + #### Required fields + + + + A human-readable name for the token. Maximum 64 characters. + + + + #### Optional fields + + + + Absolute expiry as an ISO-8601 datetime **with a timezone offset** (e.g. `2026-12-31T23:59:59Z` or `2026-12-31T23:59:59+00:00`). Must be in the future. Naive datetimes (no offset) are rejected. + + + Token lifetime in seconds (positive integer). The server converts this to an absolute expiry at request time as `now + expires_in`. Ignored if `expires_at` is also supplied. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/tokens/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "CI Token", + "expires_at": "2026-12-31T23:59:59Z" + }' + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/tokens/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + } + payload = { + 'name': 'CI Token', + 'expires_at': '2026-12-31T23:59:59Z', + # Or use a relative TTL instead: + # 'expires_in': 2592000, # 30 days + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "f8621d1a-6903-4b60-8e8d-2085a2475871", + "name": "CI Token", + "createdAt": "2024-06-01T12:00:00Z", + "expiresAt": "2025-12-31T00:00:00Z", + "token": "pss_service:v2::::", + "bearerToken": "ServiceAccount " + } + ``` + + + + +--- + +## Delete Token {{ tag: 'DELETE', label: '/v1/service-accounts/:id/tokens/:token_id' }} + + + + + Revoke a service account token. Any subsequent requests using the token return `401 Unauthorized`. + + Returns `404 Not Found` if the token belongs to a different service account than the `:id` in the path — the API does not reveal token existence across service accounts. + + ### URL parameters + + + + The unique identifier of the service account. + + + The unique identifier of the token to revoke. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/service-accounts/8ab27128-02d8-42c1-b893-12acaffbbd4b/tokens/f8621d1a-6903-4b60-8e8d-2085a2475871/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + sa_id = '8ab27128-02d8-42c1-b893-12acaffbbd4b' + token_id = 'f8621d1a-6903-4b60-8e8d-2085a2475871' + url = f'https://api.phase.dev/v1/service-accounts/{sa_id}/tokens/{token_id}/' + headers = {'Authorization': f'Bearer {token}'} + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + diff --git a/src/pages/public-api/teams.mdx b/src/pages/public-api/teams.mdx new file mode 100644 index 00000000..2e95d46f --- /dev/null +++ b/src/pages/public-api/teams.mdx @@ -0,0 +1,728 @@ +import { Tag } from '@/components/Tag' +import { DocActions } from '@/components/DocActions' + +export const metadata = { + title: 'Teams API', + description: + 'Explore the Phase Teams API for managing teams, team membership, and team-scoped app access programmatically.', +} + +API + +# Teams + +Teams group users and service accounts together and grant shared, scoped access to specific app environments. Each team can carry optional role overrides that apply only to apps accessed via that team's grants, so the same member can hold different effective permissions in different teams. On this page, we'll look at the Teams API endpoints for managing teams, their membership, and their app-environment scope. {{ className: 'lead' }} + + +Teams require a Pro or Enterprise plan. `POST /v1/teams/` returns `403` on the Free plan. + + + + +## The Team model + +### Properties + + + + Unique identifier for the team. + + + The team name. Maximum 64 characters. + + + An optional team description. + + + Whether this team is provisioned and synced by your SCIM identity provider. SCIM-managed teams cannot be renamed, deleted, or have user members added or removed via this API — those operations must be performed through the SCIM provider. Service account membership is unaffected and remains manageable via the API. + + + The optional role override for human members of the team. When set, the role unions with each member's organisation role for app-level permissions on apps the team has access to. `null` means no override — members keep their organisation role on team-accessed apps. + + + The optional role override for service-account members of the team. Same semantics as `memberRole`, applied to service accounts. + + + The OrganisationMember that owns the team (the team creator by default). Team owners can transfer access scope and add other members. Team owners retain their organisation role on team-accessed apps regardless of `memberRole`. + + + Timestamp of when the team was created. + + + Timestamp of when the team was last updated. + + + +### Permission model + +Team access is **additive**: granting team membership never reduces a member's effective permissions. For each app the team has access to, the request is permitted if **either** the member's individual organisation role grants the action **or** the team's effective role (override → org role) grants it. The same member can be in multiple teams; permissions union across all of them. + +Server-side Encryption (SSE) is required for an app to be granted to a team — the server provisions per-member `EnvironmentKey` records when teams are attached to apps or members are added to teams. Apps without SSE return `400 Bad Request` when added to a team's scope. + +--- + +## List Teams {{ tag: 'GET', label: '/v1/teams' }} + + + + + Retrieve all teams in the organisation. Requires the `Teams.read` org permission. Teams are returned without `members` or `apps` detail — fetch the [Team Detail](#get-team) endpoint for those. + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/teams/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/teams/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "data": [ + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": null, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z" + } + ] + } + ``` + + + + +--- + +## Create Team {{ tag: 'POST', label: '/v1/teams' }} + + + + + Create a new team. The calling user becomes the team's owner and is automatically added as a member. Service-account callers create the team without an owner and no auto-membership. + + Requires the `Teams.create` org permission and a Pro or Enterprise plan. + + ### JSON Body + + #### Required fields + + + + The team name. Maximum 64 characters. HTML tags and ASCII control characters are stripped; whitespace is trimmed. + + + + #### Optional fields + + + + A description for the team. Maximum 10,000 characters. + + + Role ID to apply as the team's `memberRole` override. Must reference a role in the same organisation. + + + Role ID to apply as the team's `serviceAccountRole` override. Must reference a role in the same organisation. + + + + + The `is_scim_managed` flag is set automatically by the SCIM provisioning flow. Passing it in the request body returns `400 Bad Request`. + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/teams/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "backend-eng", + "description": "Backend engineering team" + }' + ``` + + ```python + import requests + + url = 'https://api.phase.dev/v1/teams/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'backend-eng', + 'description': 'Backend engineering team' + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response', statusCode: '201' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": null, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-01T12:00:00Z", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + } + ], + "apps": [] + } + ``` + + + + +--- + +## Get Team {{ tag: 'GET', label: '/v1/teams/:id' }} + + + + + Retrieve a single team with its members and app-environment scope. Non-team-members can list teams via [List Teams](#list-teams) but cannot fetch detail unless they hold a global-access role (Owner or Admin). + + ### URL parameters + + + + The unique identifier of the team. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.get(url, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-eng", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": { + "id": "5d880011-2fc9-4e78-9be2-80f4183c0eea", + "name": "Developer" + }, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-02T09:00:00Z", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + }, + { + "type": "service_account", + "id": "0df24d46-e057-4695-a519-eee3d34e291c", + "name": "deploy-bot" + } + ], + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "web-frontend", + "environments": [ + { "id": "af6b7a8e-c268-48c2-967c-032e86e26110", "name": "Development" }, + { "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", "name": "Staging" } + ] + } + ] + } + ``` + + + + +--- + +## Update Team {{ tag: 'PUT', label: '/v1/teams/:id' }} + + + + + Update a team's name, description, or role overrides. At least one field must be provided. + + - SCIM-managed teams reject all field updates — name and description are synced from the SCIM provider, and role overrides are managed via the console. + - Pass an empty string (`""`) for `member_role_id` or `service_account_role_id` to clear an existing override and fall back to the org role on team-accessed apps. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + The new team name. Maximum 64 characters. + + + The new description. Maximum 10,000 characters. + + + New role override for human members. Pass `""` to clear the existing override. + + + New role override for service-account members. Pass `""` to clear the existing override. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "backend-engineering", + "member_role_id": "5d880011-2fc9-4e78-9be2-80f4183c0eea" + }' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'name': 'backend-engineering', + 'member_role_id': '5d880011-2fc9-4e78-9be2-80f4183c0eea' + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "description": "Backend engineering team", + "isScimManaged": false, + "memberRole": { + "id": "5d880011-2fc9-4e78-9be2-80f4183c0eea", + "name": "Developer" + }, + "serviceAccountRole": null, + "owner": { + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com" + }, + "createdAt": "2024-06-01T12:00:00Z", + "updatedAt": "2024-06-03T14:00:00Z", + "members": [], + "apps": [] + } + ``` + + + + +--- + +## Delete Team {{ tag: 'DELETE', label: '/v1/teams/:id' }} + + + + + Delete a team. This soft-deletes the team and cascades: + + - All team-granted `EnvironmentKey` rows are revoked. Keys carrying an additional individual grant survive (their team grant is removed but the key itself is preserved). + - All team-owned service accounts are soft-deleted, including their tokens. + + SCIM-managed teams cannot be deleted via this endpoint — delete the corresponding group in your SCIM provider instead. + + ### URL parameters + + + + The unique identifier of the team. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/ \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + + response = requests.delete(url, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Add Team Members {{ tag: 'POST', label: '/v1/teams/:id/members' }} + + + + + Add one or more members (users or service accounts) to a team. The server provisions per-member `EnvironmentKey` records for every SSE-enabled app the team already has access to. + + - **SCIM-managed teams** reject `user` additions — user membership is controlled by the SCIM provider. Service-account additions are permitted on SCIM-managed teams since service accounts are outside the SCIM scope. + - **Team-owned service accounts** (SAs created with a `team_id`) cannot be added to a different team — returns `409 Conflict`. The SA must first be transferred to org-level ownership. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + Either `user` or `service_account`. Defaults to `user`. + + + Array of OrganisationMember IDs (for `user`) or ServiceAccount IDs (for `service_account`). Must be a non-empty list. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X POST https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/members/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "member_type": "user", + "member_ids": [ + "3f2e1d0c-9b8a-7654-3210-fedcba987654" + ] + }' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/members/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'member_type': 'user', + 'member_ids': ['3f2e1d0c-9b8a-7654-3210-fedcba987654'] + } + + response = requests.post(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "members": [ + { + "type": "user", + "id": "99e37555-108d-4331-a385-6db971bbd617", + "email": "alice@example.com", + "fullName": "Alice Smith" + }, + { + "type": "user", + "id": "3f2e1d0c-9b8a-7654-3210-fedcba987654", + "email": "bob@example.com", + "fullName": "Bob Jones" + } + ] + } + ``` + + + + +--- + +## Remove Team Member {{ tag: 'DELETE', label: '/v1/teams/:id/members/:member_id' }} + + + + + Remove a single member from a team. The server revokes the member's team-granted `EnvironmentKey` records; keys carrying an additional individual grant are preserved. + + - **SCIM-managed teams** reject `user` removals — user membership is controlled by the SCIM provider. + - **Team-owned service accounts** cannot be removed from their owning team — returns `409 Conflict`. Delete the service account or transfer ownership to org-level first. + + ### URL parameters + + + + The unique identifier of the team. + + + The OrganisationMember ID or ServiceAccount ID to remove. + + + + ### Query parameters + + + + Either `user` or `service_account`. Defaults to `user`. Required to disambiguate when the same ID exists in both tables. + + + + + + + + + ```fish {{ title: 'cURL' }} + curl -X DELETE "https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/members/3f2e1d0c-9b8a-7654-3210-fedcba987654/?member_type=user" \ + -H "Authorization: Bearer {token}" + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + member_id = '3f2e1d0c-9b8a-7654-3210-fedcba987654' + url = f'https://api.phase.dev/v1/teams/{team_id}/members/{member_id}/' + headers = { + 'Authorization': f'Bearer {token}' + } + params = {'member_type': 'user'} + + response = requests.delete(url, params=params, headers=headers) + # Returns 204 No Content on success + ``` + + + + ```text {{ title: 'Response' }} + 204 No Content + ``` + + + + +--- + +## Manage Access {{ tag: 'PUT', label: '/v1/teams/:id/access' }} + + + + + Set the apps and environments the team has access to. This is a **declarative** endpoint — the request body represents the entire desired access state. + + - Apps not in the list have their team grants revoked; orphan `EnvironmentKey` records (no remaining grants) are soft-deleted. + - Each app entry must include at least one environment. To revoke a team's access to an app entirely, omit it from the body. + - To revoke all team access, send an empty `apps` array. + - Only apps with **Server-side Encryption (SSE)** enabled can be granted to a team. Non-SSE apps return `400 Bad Request`. + - The caller must individually have access to each app being granted to the team — returns `403 Forbidden` otherwise. + + For new (app, environment) pairs added by this call, the server provisions per-member `EnvironmentKey` records for every existing team member. + + ### URL parameters + + + + The unique identifier of the team. + + + + ### JSON Body + + + + An array of app access objects. Each object must have: + - `id` (string): The app ID. + - `environments` (array): A list of environment IDs to grant access to. Must not be empty. + + To revoke all team access, pass an empty array. + + + + + + + + + ```fish {{ title: 'cURL (grant access)' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "environments": [ + "af6b7a8e-c268-48c2-967c-032e86e26110", + "b12c3d4e-5678-90ab-cdef-1234567890ab" + ] + } + ] + }' + ``` + + ```fish {{ title: 'cURL (revoke all)' }} + curl -X PUT https://api.phase.dev/v1/teams/cf3c159d-3edb-4da6-8dbf-0af4959dabf4/access/ \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{"apps": []}' + ``` + + ```python + import requests + + team_id = 'cf3c159d-3edb-4da6-8dbf-0af4959dabf4' + url = f'https://api.phase.dev/v1/teams/{team_id}/access/' + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + payload = { + 'apps': [ + { + 'id': '72b9ddd5-8fce-49ab-89d9-c431d53a9552', + 'environments': [ + 'af6b7a8e-c268-48c2-967c-032e86e26110', + 'b12c3d4e-5678-90ab-cdef-1234567890ab' + ] + } + ] + } + + response = requests.put(url, json=payload, headers=headers) + data = response.json() + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "cf3c159d-3edb-4da6-8dbf-0af4959dabf4", + "name": "backend-engineering", + "apps": [ + { + "id": "72b9ddd5-8fce-49ab-89d9-c431d53a9552", + "name": "web-frontend", + "environments": [ + { "id": "af6b7a8e-c268-48c2-967c-032e86e26110", "name": "Development" }, + { "id": "b12c3d4e-5678-90ab-cdef-1234567890ab", "name": "Staging" } + ] + } + ] + } + ``` + + +