Skip to content

[New Endpoints] Project Management REST APIs #442

Description

@cassieemb

references https://github.com/signalwire/cloud-product/issues/19427

Summary

We are shipping a native REST API for managing projects and subprojects under /api/projects. Previously the only way to list/show/update projects and create subprojects programmatically was through the LaML (Accounts) API. This is expected to be live in production with the June 29th afternoon deploy.

Note

The existing /api/project (singular) token-management endpoints are unchanged and unrelated. That path was not a fit for project management: it takes no project ID and only operates on the project the request is authenticated as, so there is no way to act on a specific subproject.

The new /api/projects endpoints accept an{id} and let you manage any subproject under the authenticated root project (as well as the root project itself), which is exactly what project management requires.

Authentication & scope

  • Auth: HTTP Basic auth using your Project ID (username) and an API token (password), the same as other SignalWire REST APIs.
  • Required scope: the API token must have the Management scope. A token without it receives 403 Forbidden.
  • Base URL: https://<your-space>.signalwire.com

All endpoints operate only within the caller's project tree - the authenticated root project and the subprojects beneath it. Any project ID outside that tree returns 404 Not Found.

Endpoints

Method Path Description
GET /api/projects List the authenticated root project and its subprojects.
GET /api/projects/{id} Retrieve a single project or subproject.
POST /api/projects Create a subproject under the authenticated root project. Returns the signing key.
PATCH /api/projects/{id} Update a project's name and settings.
DELETE /api/projects/{id} Delete a subproject.
POST /api/projects/{id}/signing-key/rotate Rotate a project's signing key. Returns the new signing key.

Limitations

  • Create is only allowed for subprojects. A subproject cannot itself contain subprojects, so creating a project while authenticated as a subproject fails with 422 nested_subprojects_not_allowed. Subprojects can only be created under a top-level (root) project.
  • Delete is only allowed for subprojects. The root/parent project cannot be deleted through this API; attempting it returns 422 only_subprojects_can_be_deleted. Root-project lifecycle is intentionally managed in the UI as it must be tied to an individual user, not a shared project token. We do have a planned follow up to implement APIs with PAT authentication for performing company admin actions - this may include extending the project create/delete API to support root projects if a PAT is supplied.
  • A project must have no phone numbers before it can be deleted. All phone numbers must be removed first, otherwise the request returns 422 phone_numbers_must_be_removed. (On a successful delete, the subproject' registry brands/campaigns are migrated up to the parent project.)
  • The signing_key is only returned on create and rotate. All other responses omit it. Users must capture it from the create/rotate response - it is not retrievable via API later.

Fields

Project object (returned by list / show / update / create / rotate)

Field Type Notes
id string (UUID) Project identifier.
name string Project name.
parent_project_id string (UUID) | null The root project's ID; null for a root project.
subproject boolean true if this is a subproject.
region_preference string Effective region preference. Returned in all responses; not currently settable via this API.
protect_recordings boolean Setting toggle.
protect_message_media boolean Setting toggle.
protect_fax_media boolean Setting toggle.
force_https_requests boolean Setting toggle.
created_at string (ISO 8601)
updated_at string (ISO 8601)
signing_key string Only present on POST /api/projects and the signing-key rotate response.

Writable fields (create / update request body)

Field Type Notes
name string Required. Max 250 characters.
protect_recordings boolean Optional.
protect_message_media boolean Optional.
protect_fax_media boolean Optional.
force_https_requests boolean Optional.

Request / response examples

List projects — GET /api/projects

Optional query parameters:

  • name — filter projects by name.
  • page_size — number of results per page (1–1000, default 50).
  • page_token — cursor token for the next/previous page.
  • page_number — page index (used together with page_token).

Response — 200 OK

{
  "links": {
    "self": "https://example-space.signalwire.com/api/projects?page_size=50",
    "first": "https://example-space.signalwire.com/api/projects?page_size=50",
    "next": "https://example-space.signalwire.com/api/projects?page_size=50&page_number=1&page_token=PA8f14e45f"
  },
  "data": [
    {
      "id": "b3877739-5c7e-4d4f-9d1a-2f0c8c2f1a11",
      "name": "Acme Production",
      "parent_project_id": null,
      "subproject": false,
      "region_preference": "us-west",
      "protect_recordings": false,
      "protect_message_media": false,
      "protect_fax_media": false,
      "force_https_requests": true,
      "created_at": "2026-06-24T19:00:00Z",
      "updated_at": "2026-06-24T19:00:00Z"
    },
    {
      "id": "8f14e45f-ceea-467d-9c2b-7a1d3a9b2c34",
      "name": "Acme Staging",
      "parent_project_id": "b3877739-5c7e-4d4f-9d1a-2f0c8c2f1a11",
      "subproject": true,
      "region_preference": "us-west",
      "protect_recordings": false,
      "protect_message_media": false,
      "protect_fax_media": false,
      "force_https_requests": false,
      "created_at": "2026-06-24T19:05:00Z",
      "updated_at": "2026-06-24T19:05:00Z"
    }
  ]
}

Show a project — GET /api/projects/{id}

Returns a single project object (200 OK). An ID outside the caller's tree returns 404 Not Found.

Create a subproject — POST /api/projects

Request

{
  "name": "Acme Staging",
  "protect_recordings": true,
  "force_https_requests": true
}

Response — 201 Created (includes signing_key)

{
  "id": "8f14e45f-ceea-467d-9c2b-7a1d3a9b2c34",
  "name": "Acme Staging",
  "parent_project_id": "b3877739-5c7e-4d4f-9d1a-2f0c8c2f1a11",
  "subproject": true,
  "region_preference": "us-west",
  "protect_recordings": true,
  "protect_message_media": false,
  "protect_fax_media": false,
  "force_https_requests": true,
  "created_at": "2026-06-24T19:05:00Z",
  "updated_at": "2026-06-24T19:05:00Z",
  "signing_key": "PSK_4d8c2b1a9f3e7c6d5b4a3e2f1d0c9b8a"
}

Update a project — PATCH /api/projects/{id}

Request

{
  "name": "Acme Staging (EU)",
  "protect_message_media": true
}

Response — 200 OK — returns the updated project object (no signing_key).

Delete a subproject — DELETE /api/projects/{id}

Response — 204 No Content (empty body).

Rotate signing key — POST /api/projects/{id}/signing-key/rotate

Response — 200 OK — returns the project object with the new signing_key. The previous key may take about 1-2 minutes to stop working.

{
  "id": "8f14e45f-ceea-467d-9c2b-7a1d3a9b2c34",
  "name": "Acme Staging",
  "parent_project_id": "b3877739-5c7e-4d4f-9d1a-2f0c8c2f1a11",
  "subproject": true,
  "region_preference": "us-west",
  "protect_recordings": true,
  "protect_message_media": false,
  "protect_fax_media": false,
  "force_https_requests": true,
  "created_at": "2026-06-24T19:05:00Z",
  "updated_at": "2026-06-24T20:10:00Z",
  "signing_key": "PSK_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
}

Error responses

Validation/business-rule failures return 422 Unprocessable Entity with a structured error body. Each entry has a type (category), code, human-readable message, the offending attribute (or null for project-level errors), and a url pointing at the error-code reference.

{
  "errors": [
    {
      "type": "validation_error",
      "code": "nested_subprojects_not_allowed",
      "message": "Subprojects can only be created under a top-level project.",
      "attribute": null,
      "url": "https://developer.signalwire.com/rest/error-codes#nested_subprojects_not_allowed"
    }
  ]
}

Other status codes:

  • 401 Unauthorized — missing/invalid credentials.
  • 403 Forbidden — the API token lacks the Management scope.
  • 404 Not Found — the project ID does not exist within the caller's project tree.

Error codes to document

HTTP Code When it happens
422 nested_subprojects_not_allowed POST /api/projects while authenticated as a subproject — subprojects can only be created under a top-level project.
422 only_subprojects_can_be_deleted DELETE targeting a root/parent project — only subprojects can be deleted through this API.
422 phone_numbers_must_be_removed DELETE of a project that still has phone numbers assigned — remove all numbers first.
422 (validation) name blank or > 250 chars name is required and limited to 250 characters.

The three new codes (nested_subprojects_not_allowed, only_subprojects_can_be_deleted, phone_numbers_must_be_removed) must be added to the public REST error-code reference.

OpenAPI Spec

openapi: 3.0.3
info:
  title: SignalWire Project Management API
  description: >
    Native REST API for managing projects and subprojects under `/api/projects`.
    All endpoints require an API token with the `Management` scope and operate only
    within the caller's project tree (the authenticated root project and its
    subprojects). Create and delete are only allowed for subprojects.
  version: "1.0.0"
servers:
  - url: https://{space}.signalwire.com
    description: Your SignalWire Space
    variables:
      space:
        default: example-space
        description: Your SignalWire Space subdomain
security:
  - basicAuth: []
tags:
  - name: Projects
    description: Manage projects and subprojects.

paths:
  /api/projects:
    get:
      tags: [Projects]
      operationId: listProjects
      summary: List projects
      description: Lists the authenticated root project and its subprojects.
      parameters:
        - name: name
          in: query
          description: Filter projects by name.
          required: false
          schema:
            type: string
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageToken'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: A page of projects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Projects]
      operationId: createSubproject
      summary: Create a subproject
      description: >
        Creates a subproject under the authenticated root project. Only allowed
        when authenticated as a top-level project. The response includes the
        `signing_key`, which is not retrievable afterward.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectCreate'
      responses:
        '201':
          description: The created subproject, including its signing key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectWithSigningKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: >
            Validation error. Possible codes include `nested_subprojects_not_allowed`
            (authenticated as a subproject) and standard `name` validation errors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/projects/{id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    get:
      tags: [Projects]
      operationId: getProject
      summary: Retrieve a project
      responses:
        '200':
          description: The requested project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Projects]
      operationId: updateProject
      summary: Update a project
      description: Updates the project's name and settings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectUpdate'
      responses:
        '200':
          description: The updated project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation error (e.g. blank or too-long `name`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags: [Projects]
      operationId: deleteSubproject
      summary: Delete a subproject
      description: >
        Deletes a subproject. Only subprojects can be deleted, and all phone
        numbers must be removed first.
      responses:
        '204':
          description: The subproject was deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: >
            Business-rule error. Possible codes: `only_subprojects_can_be_deleted`
            (target is a root project) and `phone_numbers_must_be_removed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/projects/{id}/signing-key/rotate:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    post:
      tags: [Projects]
      operationId: rotateSigningKey
      summary: Rotate a project's signing key
      description: >
        Rotates the project's signing key and returns the project with the new
        `signing_key`. The previous key may take 1-2 minutes to stop working.
      responses:
        '200':
          description: The project with its new signing key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectWithSigningKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: >
        HTTP Basic auth. Username is your Project ID; password is an API token
        with the `Management` scope.

  parameters:
    ProjectId:
      name: id
      in: path
      required: true
      description: The project or subproject ID.
      schema:
        type: string
        format: uuid
    PageSize:
      name: page_size
      in: query
      required: false
      description: Number of results per page.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 50
    PageToken:
      name: page_token
      in: query
      required: false
      description: Cursor token for paging (from the `links` in a previous response).
      schema:
        type: string
    PageNumber:
      name: page_number
      in: query
      required: false
      description: Page index, used together with `page_token`.
      schema:
        type: integer
        minimum: 0
        default: 0

  responses:
    Unauthorized:
      description: Missing or invalid credentials.
    Forbidden:
      description: The API token lacks the required `Management` scope.
    NotFound:
      description: The project ID does not exist within the caller's project tree.

  schemas:
    Project:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        parent_project_id:
          type: string
          format: uuid
          nullable: true
          description: The root project's ID; null for a root project.
        subproject:
          type: boolean
          description: True if this project is a subproject.
        region_preference:
          type: string
          readOnly: true
          description: >
            Effective region preference. Returned in all responses; not currently
            settable via this API.
        protect_recordings:
          type: boolean
        protect_message_media:
          type: boolean
        protect_fax_media:
          type: boolean
        force_https_requests:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - name
        - parent_project_id
        - subproject
        - region_preference
        - protect_recordings
        - protect_message_media
        - protect_fax_media
        - force_https_requests
        - created_at
        - updated_at

    ProjectWithSigningKey:
      allOf:
        - $ref: '#/components/schemas/Project'
        - type: object
          properties:
            signing_key:
              type: string
              description: >
                The project's signing key. Only returned on create and rotate;
                not retrievable afterward.
          required:
            - signing_key

    ProjectCreate:
      type: object
      properties:
        name:
          type: string
          maxLength: 250
          description: Project name (required).
        protect_recordings:
          type: boolean
        protect_message_media:
          type: boolean
        protect_fax_media:
          type: boolean
        force_https_requests:
          type: boolean
      required:
        - name

    ProjectUpdate:
      allOf:
        - $ref: '#/components/schemas/ProjectCreate'

    ProjectList:
      type: object
      properties:
        links:
          type: object
          properties:
            self:
              type: string
              format: uri
            first:
              type: string
              format: uri
            next:
              type: string
              format: uri
              description: Present only when more results exist.
            prev:
              type: string
              format: uri
              description: Present only when a previous page exists.
          required:
            - self
            - first
        data:
          type: array
          items:
            $ref: '#/components/schemas/Project'
      required:
        - links
        - data

    Error:
      type: object
      properties:
        type:
          type: string
          description: Error category (e.g. "validation_error").
        code:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable error message.
        attribute:
          type: string
          nullable: true
          description: The offending field, or null for project-level errors.
        url:
          type: string
          format: uri
          description: Link to the error-code reference.
      required:
        - type
        - code
        - message

    ErrorResponse:
      type: object
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/Error'
      required:
        - errors

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions