> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Invite teammates and clients

> Learn how to grant access in Cactal: invite teammates with organization roles, invite clients to a single website with editor roles, manage direct editor grants, and remove access.

Cactal has two invitation flows: organization membership for your team, and website-scoped editors for clients who should see only their website.

## Prerequisites

* An API key with `admin` organization access — see [Create an API key](/docs/create-an-api-key)
* A website, for the website-scoped flow — see the [Quickstart](/docs/quickstart)

## Choose the flow

| Flow                      | Scope                             | Roles                                                   | Typical use                                      |
| ------------------------- | --------------------------------- | ------------------------------------------------------- | ------------------------------------------------ |
| Organization invitation   | Every website in the organization | `admin`, `full_editor`, `content_editor`                | Your own team.                                   |
| Website editor invitation | One website                       | `full` (source and content), `content` (CMS and assets) | An agency's client, a freelancer on one project. |

The `owner` role is not invitable; ownership moves only through a member-role update performed by the current owner. See [Access control](/docs/concepts/access-control) for the capability each role carries.

<Steps>
  <Step title="Invite a teammate to the organization">
    `POST /v1/organizations/{organizationId}/invitations` emails an invitation. `role` defaults to `full_editor` when omitted; invitations expire after 48 hours unless you pass `expiresAt`.

    ```bash Invite an admin theme={null}
    curl -X POST 'https://api.cactal.ai/v1/organizations/oG8hJ0kL2mN4pQ6rS7tU9/invitations' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "email": "dana@example.com", "role": "admin" }'
    ```

    ```json Response theme={null}
    {
      "id": "iV3wX5yZ7aB9cD1eF2gH4",
      "email": "dana@example.com",
      "role": "admin",
      "status": "pending",
      "expiresAt": "2026-07-10T15:04:05.000Z",
      "createdAt": "2026-07-08T15:04:05.000Z",
      "url": "https://app.cactal.ai/accept-invitation?invitationId=iV3wX5yZ7aB9cD1eF2gH4"
    }
    ```

    A pending invitation for the same email returns `409`; pass `"resend": true` to cancel it and issue a fresh one. The response always includes the acceptance `url`. To deliver the link yourself — for example from an agent — pass `"sendEmail": false`, which creates the invitation without emailing the recipient.

    The recipient accepts from the email and becomes a member with the invited role. Adjust roles later with `PATCH /v1/organizations/{organizationId}/members/{memberPrincipalId}` (body `{ "role": "..." }`); assigning `owner` requires the current owner.
  </Step>

  <Step title="Invite a client to one website">
    This is the agency pattern: the client gets access to their website and nothing else. `POST /v1/websites/{websiteId}/editors` takes an email and a website role — `full` or `content`.

    ```bash Invite a client as a content editor theme={null}
    curl -X POST 'https://api.cactal.ai/v1/websites/V1StGXR8_Z5jdHi6B-myT/editors' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "email": "client@acme.com", "role": "content" }'
    ```

    The response depends on whether the email already has a Cactal account:

    ```json Response when the email is new theme={null}
    {
      "status": "pending",
      "invitation": {
        "id": "wE5rT7yU9iO1pA3sD4fG6",
        "email": "client@acme.com",
        "role": "content",
        "status": "pending",
        "expiresAt": "2026-07-10T15:04:05.000Z",
        "createdAt": "2026-07-08T15:04:05.000Z"
      }
    }
    ```

    * `"status": "pending"` — an invitation email was sent; it expires after 48 hours by default.
    * `"status": "granted"` — the account exists, so the editor grant was applied immediately; the response includes the `grant`.
    * `"status": "already_inherited"` — that person is already an organization member and inherits access from their organization role.

    A `content` editor manages CMS items and assets on that website. A `full` editor also edits source code and publishes. Neither can see other websites, organization settings, members, or API keys.

    After accepting, the client lands directly on the shared website. It appears in their dashboard's unified Sites list alongside any sites they already have, under a "Shared with you" sidebar group; clients without an organization of their own get the same Sites page with a "Recently opened" group. In the API, `GET /v1/websites?scope=shared` returns exactly this shared set.
  </Step>

  <Step title="Grant or change access directly">
    When you already know a person's principal id — for example from the editors list — set their role without an email round-trip:

    ```bash Upsert a direct editor grant theme={null}
    curl -X PUT 'https://api.cactal.ai/v1/websites/V1StGXR8_Z5jdHi6B-myT/editors/pZ2xC4vB6nM8qW0eR1tY3' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "role": "full" }'
    ```

    ```json Response theme={null}
    {
      "websiteId": "V1StGXR8_Z5jdHi6B-myT",
      "principalId": "pZ2xC4vB6nM8qW0eR1tY3",
      "role": "full",
      "createdAt": "2026-07-08T15:12:44.000Z"
    }
    ```

    The call is an upsert — repeat it with a different `role` to change access. Granting to an existing organization member returns `400`, because members already inherit website access from their organization role.
  </Step>

  <Step title="Remove access and cancel invitations">
    Remove a website editor:

    ```bash Remove an editor theme={null}
    curl -X DELETE 'https://api.cactal.ai/v1/websites/V1StGXR8_Z5jdHi6B-myT/editors' \
      -H "Authorization: Bearer $CACTAL_API_KEY" \
      -H 'Content-Type: application/json' \
      -d '{ "principalId": "pZ2xC4vB6nM8qW0eR1tY3" }'
    ```

    Access ends immediately; a missing grant returns `404`. The same endpoint cancels a pending website invitation: pass `{ "invitationId": "..." }` instead of `principalId`. Get that id from `GET /v1/websites/{websiteId}/editor-invitations?status=pending`. Organization invitations cancel with `POST /v1/organizations/invitations/{invitationId}/cancel`; list their ids with `GET /v1/organizations/{organizationId}/invitations?status=pending`. Remove an organization member with `DELETE /v1/organizations/{organizationId}/members/{memberPrincipalId}`.
  </Step>

  <Step title="Verify who has access">
    ```bash List website editors theme={null}
    curl 'https://api.cactal.ai/v1/websites/V1StGXR8_Z5jdHi6B-myT/editors' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    ```bash List pending website invitations theme={null}
    curl 'https://api.cactal.ai/v1/websites/V1StGXR8_Z5jdHi6B-myT/editor-invitations?status=pending&limit=50' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    ```bash List pending organization invitations theme={null}
    curl 'https://api.cactal.ai/v1/organizations/oG8hJ0kL2mN4pQ6rS7tU9/invitations?status=pending&limit=50' \
      -H "Authorization: Bearer $CACTAL_API_KEY"
    ```

    <Check>
      The editors endpoint and each invitation endpoint return `{ "items": [...], "nextCursor": "..." | null }`. An editor entry looks like `{ "principalId": "...", "role": "full" | "content", "name": "...", "email": "...", "createdAt": "..." }`. Pass `nextCursor` back as `cursor` until it is `null`; use `status=all` to retrieve accepted and canceled history as well as pending invitations. Organization members remain separate at `GET /v1/organizations/{organizationId}/members`.
    </Check>

    Every grant, invitation, and removal is recorded in the organization's audit log.
  </Step>
</Steps>

## Troubleshooting

| Symptom                                                                               | Cause                                                                  | Fix                                                                                                 |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `409` `An active invitation already exists for this email`                            | A pending organization invitation exists.                              | Retry with `"resend": true`, or cancel the old one.                                                 |
| `400` `Organization team members already inherit website access from their org role.` | Direct editor grants are for people outside the organization.          | Change the member's organization role instead.                                                      |
| Invitation email never arrives                                                        | The invitation expired (48-hour default) or went to the wrong address. | Cancel and re-invite, or re-create with `"sendEmail": false` and share the returned `url` directly. |
| `403` on invitation endpoints                                                         | Your API key's role lacks member management.                           | Use a key with `admin` organization access.                                                         |

## Next steps

* [Manage API keys](/docs/guides/manage-api-keys) — the same scoping model, applied to keys and agents
* [Access control concepts](/docs/concepts/access-control) for the full role and capability matrix
