---
title: "Workspaces API"
description: "The tenant, its members, and its plan limits."
canonical: https://docs.notifiable.sh/api/workspaces
source: https://docs.notifiable.sh/api/workspaces.md
---

# Workspaces API

`{workspace}` is the workspace id. Two workspaces can share a name. An old `/w/{slug}` address redirects to the id. A person who is not a member receives 404. A member who cannot do the action receives 403.

## GET /workspaces/new

- **Auth:** signed-in session.
- **Name:** `workspace.create`
- **Request:** none.
- **Response:** the workspace name page when the user has no workspace. Otherwise redirect to app home.
- **Errors:** guests are redirected to login.

## POST /workspaces

- **Auth:** signed-in session with no workspace.
- **Name:** `workspace.store`
- **Request:** `name` (required, max 100), `icon` (optional image, png, jpg, jpeg, or webp, max 2048 KB).
- **Response:** redirect to How to create an app? in the new workspace. The workspace starts on Free with an empty usage period.
- **Errors:** validation errors redirect back. A user who already has a workspace is redirected to app home.

## GET /workspaces/{workspace}

- **Auth:** signed-in member.
- **Name:** `workspace.home`
- **Request:** optional `notifiable_last_app` cookie.
- **Response:** redirect to the last app in this workspace, otherwise its oldest app, otherwise app creation.
- **Errors:** 404 when the user is not a member.

## GET /workspaces/{workspace}/settings

- **Auth:** signed-in member.
- **Name:** `workspace.settings`
- **Request:** none.
- **Response:** the workspace settings page: people, connections, usage from the open period, the plan catalog, and API keys. Connections includes the Apple team and key identifier when one is saved, and never the signing key. `aiKeys` and `appKeys` list each saved key as `id`, `provider`, and `name`. A developer, admin, or owner also receives `api_key` with the first 8 characters visible and the rest replaced by asterisks. A viewer does not. `appKeys` providers are `revenuecat`, `stripe`, `discord`, `slack`, `resend`, and `http`. `serverKeys` lists workspace server API keys as `{ id, name, prefix, expires, lastUsed, usage, limit }`. The prefix is not the full secret. `createdKey` is the full `ntf_key_` secret only on the visit immediately after create, otherwise null. Billing actions are for the owner.
- **Errors:** 404 when the user is not a member.

## POST /workspaces/{workspace}/ai-keys

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.ai-keys.store`
- **Request:** `provider` (`openai`, `openrouter`, `anthropic`, or `gemini`), `name` (required, max 80, unique for that provider on this workspace), and `api_key` (required, max 500).
- **Response:** redirect back. The key is stored for this workspace and does not replace another key. Later pages list the name, and a developer, admin, or owner sees the first 8 characters of the value.
- **Errors:** 403 for a viewer. 404 when the user is not a member. 422 when the provider, name, or key is missing, or the name is already used for that provider.

## PATCH /workspaces/{workspace}/ai-keys/{aiKey}

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.ai-keys.update`
- **Request:** `name` (required, max 80, unique for that provider on this workspace, ignoring this key) and `api_key` (optional, max 500). A blank `api_key` keeps the saved value.
- **Response:** redirect back.
- **Errors:** 403 for a viewer. 404 when the user is not a member, or the key is not on this workspace. 422 when the name is missing or already used.

## POST /workspaces/{workspace}/app-keys

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.app-keys.store`
- **Request:** `provider` (`revenuecat`, `stripe`, `discord`, `slack`, `resend`, or `http`), `name` (required, max 80, unique for that provider on this workspace), and `api_key` (required, max 500).
- **Response:** redirect back. The credential is stored for this workspace.
- **Errors:** 403 for a viewer. 404 when the user is not a member. 422 when the provider, name, or key is missing, or the name is already used for that provider.

## PATCH /workspaces/{workspace}/app-keys/{appKey}

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.app-keys.update`
- **Request:** `name` (required, max 80, unique for that provider on this workspace, ignoring this key) and `api_key` (optional, max 500). A blank `api_key` keeps the saved value.
- **Response:** redirect back.
- **Errors:** 403 for a viewer. 404 when the user is not a member, or the key is not on this workspace. 422 when the name is missing or already used.

## DELETE /workspaces/{workspace}/app-keys/{appKey}

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.app-keys.destroy`
- **Request:** none.
- **Response:** redirect back. The credential is deleted.
- **Errors:** 403 for a viewer. 404 when the user is not a member, or the key is not on this workspace.

## DELETE /workspaces/{workspace}/ai-keys/{aiKey}

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.ai-keys.destroy`
- **Request:** none.
- **Response:** redirect back. The key is deleted.
- **Errors:** 403 for a viewer. 404 when the user is not a member, or the key is not on this workspace.

## DELETE /workspaces/{workspace}

- **Auth:** signed-in owner.
- **Name:** `workspace.destroy`
- **Request:** none.
- **Response:** redirect to app home. The workspace is deleted.
- **Errors:** 403 for other members. 404 when the user is not a member.

## POST /workspaces/{workspace}/members

- **Auth:** signed-in admin or owner.
- **Name:** `workspace.members.store`
- **Request:** `email` (required email), `role` (`viewer`, `developer`, or `admin`).
- **Response:** redirect back. Sends an invitation email. Replaces a pending invite for the same workspace and email.
- **Errors:** 403 for a viewer or developer. Validation when the email already belongs, the role is owner, or the seat cap is full.

## PATCH /workspaces/{workspace}/members/{member}

- **Auth:** signed-in admin or owner.
- **Name:** `workspace.members.update`
- **Request:** `role` (`viewer`, `developer`, or `admin`).
- **Response:** redirect back.
- **Errors:** 403 for a viewer or developer. 404 when the member is not in that workspace.

## DELETE /workspaces/{workspace}/members/{member}

- **Auth:** signed-in member when removing themself. Admin or owner when removing someone else.
- **Name:** `workspace.members.destroy`
- **Request:** none.
- **Response:** redirect to app home when leaving, otherwise back.
- **Errors:** 403 when a viewer or developer removes someone else. The owner cannot leave.

## DELETE /workspaces/{workspace}/invitations/{invitation}

- **Auth:** signed-in admin or owner.
- **Name:** `workspace.invitations.destroy`
- **Request:** none.
- **Response:** redirect back. The pending invite is cancelled.
- **Errors:** 403 for a viewer or developer. 404 when the invitation is not for that workspace.

## POST /workspaces/{workspace}/ownership

- **Auth:** signed-in owner.
- **Name:** `workspace.ownership.transfer`
- **Request:** `member` (required UUID of an existing member).
- **Response:** redirect back. That member becomes owner. The previous owner becomes an admin.
- **Errors:** 403 for other members. 404 when the member is not in that workspace.

## GET /invitations/{invitation}/{token}

- **Auth:** signed URL. Guest or signed-in session.
- **Name:** `workspace.invitations.accept`
- **Request:** the token in the path, plus the signature.
- **Response:** when the signed-in email matches, accept and redirect to the workspace. When the visitor is a guest, store the invite and redirect to login. An unknown email continues through registration first.
- **Errors:** 403 when the signature is invalid. Redirect home when the signed-in email does not match or the invite has expired.

## POST /invitations/{invitation}/accept

- **Auth:** signed-in session. The account email must match the invite.
- **Name:** `workspace.invitations.accept.account`
- **Request:** none.
- **Response:** accept and redirect to the workspace.
- **Errors:** redirect to app home when the invite is expired, already accepted, or sent to a different email.

## POST /workspaces/{workspace}/api-keys

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.api-keys.store`
- **Request:** `name` (required, max 80), `expiration` (`never`, `30`, `90`, or `365`), `request_limit` (`unlimited`, `1000`, or `10000`), `limit_period` (`none`, `daily`, or `monthly`). The key belongs to the workspace. `none` with a numeric limit does not reset. `unlimited` ignores the period.
- **Response:** redirect back. The next settings page shows the full `ntf_key_` secret once in `createdKey`. Later visits do not.
- **Errors:** guests are redirected to login. 403 for a viewer. 404 when the user is not a member. Validation error when a field is missing or not one of the allowed values.

## POST /workspaces/{workspace}/api-keys/bulk

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.api-keys.bulk`
- **Request:** `action` must be `revoke`. `ids` is 1 to 500 API key ids.
- **Response:** redirect back. Only keys in this workspace are revoked.
- **Errors:** 403 for a viewer. 404 when the user is not a member. 422 when the action or ids are invalid.

## DELETE /workspaces/{workspace}/api-keys/{apiKey}

- **Auth:** signed-in developer, admin, or owner.
- **Name:** `workspace.api-keys.destroy`
- **Request:** API key id in the path.
- **Response:** redirect back. The key is deleted.
- **Errors:** guests are redirected to login. 403 for a viewer. 404 when the user is not a member or the key belongs to another workspace.

## GET /api/v1/public

- **Auth:** `Authorization: Bearer` with a workspace server API key.
- **Name:** `api.v1.public.show`
- **Request:** none.
- **Response:** `200` `WORKSPACE_FETCHED`. `data` has `id`, `name`, `plan`, and `limits`.
- **Errors:** `401` `INVALID_API_KEY`. `429` `API_KEY_LIMIT_REACHED` or `RATE_LIMIT_EXCEEDED`. `500` `SERVER_ERROR`.
